Use webview svg
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -25,6 +25,11 @@
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module org.example.petshopdesktop {
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires javafx.web;
|
||||
requires java.sql;
|
||||
requires java.net.http;
|
||||
requires java.xml;
|
||||
requires com.fasterxml.jackson.databind;
|
||||
requires com.fasterxml.jackson.core;
|
||||
requires com.fasterxml.jackson.annotation;
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.example.petshopdesktop.api.dto.auth.LoginResponse;
|
||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||
import org.example.petshopdesktop.auth.Role;
|
||||
import org.example.petshopdesktop.auth.UserSession;
|
||||
import org.example.petshopdesktop.ui.SvgNodeLoader;
|
||||
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
|
||||
public class LoginController {
|
||||
@@ -36,7 +36,7 @@ public class LoginController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
lblError.setText("");
|
||||
logoContainer.getChildren().setAll(SvgNodeLoader.loadSquare("/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg", 132));
|
||||
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg", 132));
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
package org.example.petshopdesktop.ui;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.FillRule;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class SvgNodeLoader {
|
||||
private SvgNodeLoader() {
|
||||
}
|
||||
|
||||
public static Pane loadSquare(String resourcePath, double size) {
|
||||
SvgDocument svg = readSvg(resourcePath);
|
||||
double inset = size * 0.08;
|
||||
double available = size - (inset * 2);
|
||||
double scale = Math.min(available / svg.width(), available / svg.height());
|
||||
|
||||
Group graphic = svg.content();
|
||||
graphic.setScaleX(scale);
|
||||
graphic.setScaleY(scale);
|
||||
graphic.setLayoutX(inset);
|
||||
graphic.setLayoutY(inset);
|
||||
|
||||
Pane pane = new Pane(graphic);
|
||||
pane.setMinSize(size, size);
|
||||
pane.setPrefSize(size, size);
|
||||
pane.setMaxSize(size, size);
|
||||
pane.setClip(new Rectangle(size, size));
|
||||
return pane;
|
||||
}
|
||||
|
||||
private static SvgDocument readSvg(String resourcePath) {
|
||||
try (Reader reader = requireResource(resourcePath)) {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(false);
|
||||
Document document = factory.newDocumentBuilder().parse(new InputSource(reader));
|
||||
Element root = document.getDocumentElement();
|
||||
double[] viewBox = parseViewBox(root.getAttribute("viewBox"));
|
||||
Map<String, Map<String, String>> styles = parseStyles(root);
|
||||
Group content = new Group();
|
||||
appendChildren(root, content, styles);
|
||||
content.setTranslateX(-viewBox[0]);
|
||||
content.setTranslateY(-viewBox[1]);
|
||||
return new SvgDocument(content, viewBox[2], viewBox[3]);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to load SVG: " + resourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStreamReader requireResource(String resourcePath) {
|
||||
var stream = SvgNodeLoader.class.getResourceAsStream(resourcePath);
|
||||
if (stream == null) {
|
||||
throw new IllegalArgumentException("Resource not found: " + resourcePath);
|
||||
}
|
||||
return new InputStreamReader(stream, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static double[] parseViewBox(String viewBox) {
|
||||
String[] parts = viewBox.trim().split("\\s+");
|
||||
if (parts.length != 4) {
|
||||
throw new IllegalArgumentException("Invalid viewBox: " + viewBox);
|
||||
}
|
||||
return new double[] {
|
||||
Double.parseDouble(parts[0]),
|
||||
Double.parseDouble(parts[1]),
|
||||
Double.parseDouble(parts[2]),
|
||||
Double.parseDouble(parts[3])
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, String>> parseStyles(Element root) {
|
||||
Map<String, Map<String, String>> styles = new HashMap<>();
|
||||
NodeList styleNodes = root.getElementsByTagName("style");
|
||||
for (int i = 0; i < styleNodes.getLength(); i++) {
|
||||
String css = styleNodes.item(i).getTextContent();
|
||||
for (String rule : css.split("}")) {
|
||||
String trimmedRule = rule.trim();
|
||||
if (trimmedRule.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] selectorAndBody = trimmedRule.split("\\{", 2);
|
||||
if (selectorAndBody.length != 2) {
|
||||
continue;
|
||||
}
|
||||
String selector = selectorAndBody[0].trim();
|
||||
if (!selector.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> properties = new LinkedHashMap<>();
|
||||
for (String declaration : selectorAndBody[1].split(";")) {
|
||||
String trimmedDeclaration = declaration.trim();
|
||||
if (trimmedDeclaration.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] keyValue = trimmedDeclaration.split(":", 2);
|
||||
if (keyValue.length == 2) {
|
||||
properties.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
}
|
||||
}
|
||||
styles.put(selector.substring(1), properties);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
private static void appendChildren(Element parent, Group target, Map<String, Map<String, String>> styles) {
|
||||
NodeList childNodes = parent.getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
org.w3c.dom.Node child = childNodes.item(i);
|
||||
if (!(child instanceof Element element)) {
|
||||
continue;
|
||||
}
|
||||
switch (element.getTagName()) {
|
||||
case "g" -> {
|
||||
Group group = new Group();
|
||||
appendChildren(element, group, styles);
|
||||
target.getChildren().add(group);
|
||||
}
|
||||
case "circle" -> target.getChildren().add(buildCircle(element, styles));
|
||||
case "path" -> target.getChildren().add(buildPath(element, styles));
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Circle buildCircle(Element element, Map<String, Map<String, String>> styles) {
|
||||
Circle circle = new Circle(
|
||||
Double.parseDouble(element.getAttribute("cx")),
|
||||
Double.parseDouble(element.getAttribute("cy")),
|
||||
Double.parseDouble(element.getAttribute("r"))
|
||||
);
|
||||
applyShapeStyle(circle, element, styles);
|
||||
return circle;
|
||||
}
|
||||
|
||||
private static SVGPath buildPath(Element element, Map<String, Map<String, String>> styles) {
|
||||
SVGPath path = new SVGPath();
|
||||
path.setContent(element.getAttribute("d"));
|
||||
applyShapeStyle(path, element, styles);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void applyShapeStyle(Shape shape, Element element, Map<String, Map<String, String>> styles) {
|
||||
Map<String, String> styleMap = new LinkedHashMap<>();
|
||||
String className = element.getAttribute("class");
|
||||
if (!className.isBlank()) {
|
||||
for (String cssClass : className.trim().split("\\s+")) {
|
||||
Map<String, String> classStyles = styles.get(cssClass);
|
||||
if (classStyles != null) {
|
||||
styleMap.putAll(classStyles);
|
||||
}
|
||||
}
|
||||
}
|
||||
copyAttribute(element, styleMap, "fill");
|
||||
copyAttribute(element, styleMap, "stroke");
|
||||
copyAttribute(element, styleMap, "stroke-width");
|
||||
copyAttribute(element, styleMap, "fill-rule");
|
||||
|
||||
if (styleMap.containsKey("fill") && !"none".equalsIgnoreCase(styleMap.get("fill"))) {
|
||||
shape.setFill(Paint.valueOf(styleMap.get("fill")));
|
||||
} else {
|
||||
shape.setFill(null);
|
||||
}
|
||||
if (styleMap.containsKey("stroke") && !"none".equalsIgnoreCase(styleMap.get("stroke"))) {
|
||||
shape.setStroke(Paint.valueOf(styleMap.get("stroke")));
|
||||
}
|
||||
if (styleMap.containsKey("stroke-width")) {
|
||||
shape.setStrokeWidth(Double.parseDouble(styleMap.get("stroke-width")));
|
||||
}
|
||||
if (styleMap.containsKey("fill-rule") && shape instanceof SVGPath svgPath) {
|
||||
svgPath.setFillRule("evenodd".equalsIgnoreCase(styleMap.get("fill-rule")) ? FillRule.EVEN_ODD : FillRule.NON_ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyAttribute(Element element, Map<String, String> styleMap, String attribute) {
|
||||
String value = element.getAttribute(attribute);
|
||||
if (!value.isBlank()) {
|
||||
styleMap.put(attribute, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record SvgDocument(Group content, double width, double height) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.example.petshopdesktop.ui;
|
||||
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class SvgWebViewFactory {
|
||||
private SvgWebViewFactory() {
|
||||
}
|
||||
|
||||
public static StackPane build(String resourcePath, double size) {
|
||||
WebView webView = new WebView();
|
||||
webView.setContextMenuEnabled(false);
|
||||
webView.setPrefSize(size, size);
|
||||
webView.setMinSize(size, size);
|
||||
webView.setMaxSize(size, size);
|
||||
webView.setZoom(1.0);
|
||||
webView.setStyle("-fx-background-color: transparent;");
|
||||
|
||||
String svg = loadSvg(resourcePath)
|
||||
.replaceFirst("<svg\\b", "<svg width=\"100%\" height=\"100%\" preserveAspectRatio=\"xMidYMid meet\"");
|
||||
|
||||
String html = "<html><body style='margin:0;display:flex;align-items:center;justify-content:center;background:transparent;overflow:hidden;'>"
|
||||
+ svg
|
||||
+ "</body></html>";
|
||||
webView.getEngine().loadContent(html, "text/html");
|
||||
|
||||
StackPane container = new StackPane(webView);
|
||||
container.setPrefSize(size, size);
|
||||
container.setMinSize(size, size);
|
||||
container.setMaxSize(size, size);
|
||||
return container;
|
||||
}
|
||||
|
||||
private static String loadSvg(String resourcePath) {
|
||||
try (InputStream stream = SvgWebViewFactory.class.getResourceAsStream(resourcePath)) {
|
||||
if (stream == null) {
|
||||
throw new IllegalArgumentException("Resource not found: " + resourcePath);
|
||||
}
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to read SVG: " + resourcePath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user