Use webview svg

This commit is contained in:
2026-03-11 13:45:48 -06:00
parent 2777ca2a74
commit fa89073272
5 changed files with 56 additions and 204 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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) {
}
}

View File

@@ -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);
}
}
}