/* ======================================================================== * PlantUML : a free UML diagram generator * ======================================================================== * * (C) Copyright 2009-2023, Arnaud Roques * * Project Info: http://plantuml.com * * If you like this project or if you find it useful, you can support us at: * * http://plantuml.com/patreon (only 1$ per month!) * http://plantuml.com/paypal * * This file is part of PlantUML. * * PlantUML is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PlantUML distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public * License for more details. * * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * * Original Author: Arnaud Roques * * */ package net.sourceforge.plantuml.svg; import java.awt.geom.PathIterator; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.CDATASection; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Element; import net.sourceforge.plantuml.FileUtils; import net.sourceforge.plantuml.Log; import net.sourceforge.plantuml.SignatureUtils; import net.sourceforge.plantuml.awt.geom.Dimension2D; import net.sourceforge.plantuml.code.Base64Coder; import net.sourceforge.plantuml.security.SImageIO; import net.sourceforge.plantuml.security.SecurityUtils; import net.sourceforge.plantuml.tikz.TikzGraphics; import net.sourceforge.plantuml.ugraphic.UGroupType; import net.sourceforge.plantuml.ugraphic.UImageSvg; import net.sourceforge.plantuml.ugraphic.UPath; import net.sourceforge.plantuml.ugraphic.USegment; import net.sourceforge.plantuml.ugraphic.USegmentType; import net.sourceforge.plantuml.ugraphic.color.ColorMapper; import net.sourceforge.plantuml.ugraphic.color.HColorGradient; import net.sourceforge.plantuml.xml.XmlFactories; public class SvgGraphics { // http://tutorials.jenkov.com/svg/index.html // http://www.svgbasics.com/ // http://apike.ca/prog_svg_text.html // http://www.w3.org/TR/SVG11/shapes.html // http://en.wikipedia.org/wiki/Scalable_Vector_Graphics // Animation: // http://srufaculty.sru.edu/david.dailey/svg/ // Shadow: // http://www.svgbasics.com/filters3.html // http://www.w3schools.com/svg/svg_feoffset.asp // http://www.adobe.com/svg/demos/samples.html private static final String XLINK_TITLE1 = "title"; private static final String XLINK_TITLE2 = "xlink:title"; private static final String XLINK_HREF1 = "href"; private static final String XLINK_HREF2 = "xlink:href"; final private Document document; final private Element root; final private Element defs; final private Element gRoot; private String fill = "black"; private String stroke = "black"; // private Collection classesForDarkness = new LinkedHashSet<>(); // private String strokeDark = "black"; // private String fillDark = "black"; private String strokeWidth; private String strokeDasharray = null; private final String backcolor; private int maxX = 10; private int maxY = 10; private final String preserveAspectRatio; private final double scale; private final String filterUid; private final String shadowId; private final String gradientId; private final boolean svgDimensionStyle; private final LengthAdjust lengthAdjust; private final boolean interactive; final protected void ensureVisible(double x, double y) { if (x > maxX) { maxX = (int) (x + 1); } if (y > maxY) { maxY = (int) (y + 1); } } public SvgGraphics(String backcolor, boolean svgDimensionStyle, Dimension2D minDim, double scale, String hover, long seed, String preserveAspectRatio, LengthAdjust lengthAdjust, DarkStrategy darkStrategy, boolean interactive) { try { this.lengthAdjust = lengthAdjust; this.svgDimensionStyle = svgDimensionStyle; this.scale = scale; this.document = getDocument(); this.backcolor = backcolor; this.preserveAspectRatio = preserveAspectRatio; this.interactive = interactive; ensureVisible(minDim.getWidth(), minDim.getHeight()); this.root = getRootNode(); // Create a node named defs, which will be the parent // for a pair of linear gradient definitions. defs = simpleElement("defs"); gRoot = simpleElement("g"); strokeWidth = "" + scale; this.filterUid = "b" + getSeed(seed); this.shadowId = "f" + getSeed(seed); this.gradientId = "g" + getSeed(seed); if (hover != null) defs.appendChild(getPathHover(hover)); if (interactive) { final Element styles = getStylesForInteractiveMode(); if (styles != null) defs.appendChild(styles); final Element script = getScriptForInteractiveMode(); if (script != null) defs.appendChild(script); } } catch (ParserConfigurationException e) { e.printStackTrace(); throw new IllegalStateException(e); } } private Element getStylesForInteractiveMode() { final Element style = simpleElement("style"); final String text = getData("default.css"); if (text == null) return null; final CDATASection cdata = document.createCDATASection(text); style.setAttribute("type", "text/css"); style.appendChild(cdata); return style; } // private Element getStylesForDarkness() { // final Element style = simpleElement("style"); // final StringBuilder text1 = new StringBuilder(); // final StringBuilder text2 = new StringBuilder("@media (prefers-color-scheme:dark) {"); // final Pattern p = Pattern.compile("^(\\w)_(\\w+)_(\\w+)$"); // for (String s : this.classesForDarkness) { // final Matcher m = p.matcher(s); // if (m.matches() == false) // throw new IllegalStateException(); // final String color1 = m.group(2); // final String color2 = m.group(3); // final String type = m.group(1); // if ("f".equals(type)) { // text1.append("*." + s + " {fill:#" + color1 + ";}"); // text2.append("*." + s + " {fill:#" + color2 + ";}"); // } else if ("s".equals(type)) { // text1.append("*." + s + " {stroke:#" + color1 + ";}"); // text2.append("*." + s + " {stroke:#" + color2 + ";}"); // } else // throw new IllegalStateException(); // } // text2.append("}"); // final CDATASection cdata = document.createCDATASection(text1.toString() + text2.toString()); // style.setAttribute("type", "text/css"); // style.appendChild(cdata); // return style; // } private Element getScriptForInteractiveMode() { final Element script = document.createElement("script"); final String text = getData("default.js"); if (text == null) return null; script.setTextContent(text); return script; } private static String getData(final String name) { try { final InputStream is = SvgGraphics.class.getResourceAsStream("/svg/" + name); return FileUtils.readText(is); } catch (IOException e) { e.printStackTrace(); return null; } } private Element getPathHover(String hover) { final Element style = simpleElement("style"); final CDATASection cdata = document.createCDATASection("path:hover { stroke: " + hover + " !important;}"); style.setAttribute("type", "text/css"); style.appendChild(cdata); return style; } private static String getSeed(final long seed) { return Long.toString(Math.abs(seed), 36); } private Element pendingBackground; public void paintBackcolorGradient(ColorMapper mapper, HColorGradient gr) { final String id = createSvgGradient(mapper.toRGB(gr.getColor1()), mapper.toRGB(gr.getColor2()), gr.getPolicy()); setFillColor("url(#" + id + ")"); setStrokeColor(null); pendingBackground = createRectangleInternal(0, 0, 0, 0); getG().appendChild(pendingBackground); } // This method returns a reference to a simple XML // element node that has no attributes. private Element simpleElement(String type) { final Element theElement = (Element) document.createElement(type); root.appendChild(theElement); return theElement; } private Document getDocument() throws ParserConfigurationException { final DocumentBuilder builder = XmlFactories.newDocumentBuilder(); final Document document = builder.newDocument(); document.setXmlStandalone(true); return document; } // This method returns a reference to a root node that // has already been appended to the document. private Element getRootNode() { // Create the root node named svg and append it to // the document. final Element svg = (Element) document.createElement("svg"); document.appendChild(svg); // Set some attributes on the root node that are // required for proper rendering. Note that the // approach used here is somewhat different from the // approach used in the earlier program named Svg01, // particularly with regard to the style. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); svg.setAttribute("version", "1.1"); return svg; } public void svgEllipse(double x, double y, double xRadius, double yRadius, double deltaShadow) { manageShadow(deltaShadow); if (hidden == false) { final Element elt = (Element) document.createElement("ellipse"); elt.setAttribute("cx", format(x)); elt.setAttribute("cy", format(y)); elt.setAttribute("rx", format(xRadius)); elt.setAttribute("ry", format(yRadius)); fillMe(elt); elt.setAttribute("style", getStyle()); manageDarkStroke(elt); addFilterShadowId(elt, deltaShadow); getG().appendChild(elt); } ensureVisible(x + xRadius + deltaShadow * 2, y + yRadius + deltaShadow * 2); } public void svgArcEllipse(double rx, double ry, double x1, double y1, double x2, double y2) { if (hidden == false) { final String path = "M" + format(x1) + "," + format(y1) + " A" + format(rx) + "," + format(ry) + " 0 0 0 " + format(x2) + " " + format(y2); final Element elt = (Element) document.createElement("path"); elt.setAttribute("d", path); fillMe(elt); elt.setAttribute("style", getStyle()); getG().appendChild(elt); } ensureVisible(x1, y1); ensureVisible(x2, y2); } private Map, String> gradients = new HashMap, String>(); public String createSvgGradient(String color1, String color2, char policy) { final List key = Arrays.asList((Object) color1, color2, policy); String id = gradients.get(key); if (id == null) { final Element elt = (Element) document.createElement("linearGradient"); if (policy == '|') { elt.setAttribute("x1", "0%"); elt.setAttribute("y1", "50%"); elt.setAttribute("x2", "100%"); elt.setAttribute("y2", "50%"); } else if (policy == '\\') { elt.setAttribute("x1", "0%"); elt.setAttribute("y1", "100%"); elt.setAttribute("x2", "100%"); elt.setAttribute("y2", "0%"); } else if (policy == '-') { elt.setAttribute("x1", "50%"); elt.setAttribute("y1", "0%"); elt.setAttribute("x2", "50%"); elt.setAttribute("y2", "100%"); } else { elt.setAttribute("x1", "0%"); elt.setAttribute("y1", "0%"); elt.setAttribute("x2", "100%"); elt.setAttribute("y2", "100%"); } id = gradientId + gradients.size(); gradients.put(key, id); elt.setAttribute("id", id); final Element stop1 = (Element) document.createElement("stop"); stop1.setAttribute("stop-color", color1); stop1.setAttribute("offset", "0%"); final Element stop2 = (Element) document.createElement("stop"); stop2.setAttribute("stop-color", color2); stop2.setAttribute("offset", "100%"); elt.appendChild(stop1); elt.appendChild(stop2); defs.appendChild(elt); } return id; } public final void setFillColor(String fill) { this.fill = fill == null ? "none" : fill; // this.fillDark = this.fill; } public final void setFillColor(String fill, String fillDark) { this.fill = fill == null ? "none" : fill; // this.fillDark = fillDark == null ? "none" : fillDark; } public final void setStrokeColor(String stroke) { this.stroke = stroke == null ? "none" : stroke; // this.strokeDark = stroke; } public final void setStrokeColor(String stroke, String strokeDark) { this.stroke = stroke == null ? "none" : stroke; // this.strokeDark = strokeDark == null ? "none" : strokeDark; } public final void setStrokeWidth(double strokeWidth, String strokeDasharray) { this.strokeWidth = "" + (scale * strokeWidth); this.strokeDasharray = strokeDasharray; } private final List pendingAction = new ArrayList<>(); public final Element getG() { if (pendingAction.size() == 0) return gRoot; return pendingAction.get(0); } public void svgRectangle(double x, double y, double width, double height, double rx, double ry, double deltaShadow, String id, String codeLine) { if (height <= 0 || width <= 0) { return; // To be restored when Teoz will be finished // throw new IllegalArgumentException(); } manageShadow(deltaShadow); if (hidden == false) { final Element elt = createRectangleInternal(x, y, width, height); addFilterShadowId(elt, deltaShadow); if (rx > 0 && ry > 0) { elt.setAttribute("rx", format(rx)); elt.setAttribute("ry", format(ry)); } if (id != null) elt.setAttribute("id", id); if (codeLine != null) elt.setAttribute("codeLine", codeLine); getG().appendChild(elt); } ensureVisible(x + width + 2 * deltaShadow, y + height + 2 * deltaShadow); } private Element createRectangleInternal(double x, double y, double width, double height) { final Element elt = (Element) document.createElement("rect"); elt.setAttribute("x", format(x)); elt.setAttribute("y", format(y)); elt.setAttribute("width", format(width)); elt.setAttribute("height", format(height)); fillMe(elt); elt.setAttribute("style", getStyleSpecial()); manageDarkStroke(elt); return elt; } private void manageDarkStroke(final Element elt) { // if (strokeDark != null && stroke.equals(strokeDark) == false) { // final String attribute = elt.getAttribute("class"); // if (attribute == null || attribute.length() == 0) // elt.setAttribute("class", getStrokeClassForDark()); // else // elt.setAttribute("class", attribute + " " + getStrokeClassForDark()); // } } public void svgLine(double x1, double y1, double x2, double y2, double deltaShadow) { manageShadow(deltaShadow); if (hidden == false) { final Element elt = (Element) document.createElement("line"); elt.setAttribute("x1", format(x1)); elt.setAttribute("y1", format(y1)); elt.setAttribute("x2", format(x2)); elt.setAttribute("y2", format(y2)); elt.setAttribute("style", getStyle()); manageDarkStroke(elt); addFilterShadowId(elt, deltaShadow); getG().appendChild(elt); } ensureVisible(x1 + 2 * deltaShadow, y1 + 2 * deltaShadow); ensureVisible(x2 + 2 * deltaShadow, y2 + 2 * deltaShadow); } private String getStyle() { final StringBuilder style = new StringBuilder(); // if (stroke.equals(strokeDark)) style.append("stroke:" + stroke + ";"); style.append("stroke-width:" + strokeWidth + ";"); if (fill.equals("#00000000")) style.append("fill:none;"); if (strokeDasharray != null) style.append("stroke-dasharray:" + strokeDasharray + ";"); return style.toString(); } // https://forum.plantuml.net/12469/package-background-transparent-package-default-background?show=12479#c12479 private String getStyleSpecial() { final StringBuilder style = new StringBuilder(); // if (stroke.equals(strokeDark)) style.append("stroke:" + stroke + ";"); style.append("stroke-width:" + strokeWidth + ";"); if (fill.equals("#00000000")) style.append("fill:none;"); if (strokeDasharray != null) style.append("stroke-dasharray:" + strokeDasharray + ";"); return style.toString(); } public void svgPolygon(double deltaShadow, double... points) { assert points.length % 2 == 0; manageShadow(deltaShadow); if (hidden == false) { final Element elt = (Element) document.createElement("polygon"); final StringBuilder sb = new StringBuilder(); for (double coord : points) { if (sb.length() > 0) sb.append(","); sb.append(format(coord)); } elt.setAttribute("points", sb.toString()); fillMe(elt); elt.setAttribute("style", getStyleSpecial()); manageDarkStroke(elt); addFilterShadowId(elt, deltaShadow); getG().appendChild(elt); } for (int i = 0; i < points.length; i += 2) { ensureVisible(points[i] + 2 * deltaShadow, points[i + 1] + 2 * deltaShadow); } } public void text(String text, double x, double y, String fontFamily, int fontSize, String fontWeight, String fontStyle, String textDecoration, double textLength, Map attributes, String textBackColor) { if (hidden == false) { final Element elt = (Element) document.createElement("text"); // required for web-kit based browsers // elt.setAttribute("text-rendering", "geometricPrecision"); elt.setAttribute("x", format(x)); elt.setAttribute("y", format(y)); fillMe(elt); elt.setAttribute("font-size", format(fontSize)); // elt.setAttribute("text-anchor", "middle"); if (lengthAdjust == LengthAdjust.SPACING) { elt.setAttribute("lengthAdjust", "spacing"); elt.setAttribute("textLength", format(textLength)); } else if (lengthAdjust == LengthAdjust.SPACING_AND_GLYPHS) { elt.setAttribute("lengthAdjust", "spacingAndGlyphs"); elt.setAttribute("textLength", format(textLength)); } if (fontWeight != null) elt.setAttribute("font-weight", fontWeight); if (fontStyle != null) elt.setAttribute("font-style", fontStyle); if (textDecoration != null) elt.setAttribute("text-decoration", textDecoration); if (fontFamily != null) { // http://plantuml.sourceforge.net/qa/?qa=5432/svg-monospace-output-has-wrong-font-family if ("monospaced".equalsIgnoreCase(fontFamily)) fontFamily = "monospace"; elt.setAttribute("font-family", fontFamily); if (fontFamily.equalsIgnoreCase("monospace") || fontFamily.equalsIgnoreCase("courier")) text = text.replace(' ', (char) 160); } if (textBackColor != null) { final String backFilterId = getFilterBackColor(textBackColor); elt.setAttribute("filter", "url(#" + backFilterId + ")"); } for (Map.Entry ent : attributes.entrySet()) elt.setAttribute(ent.getKey(), ent.getValue()); elt.setTextContent(text); getG().appendChild(elt); // http://forum.plantuml.net/9158/hyperlink-without-underline // if (textDecoration != null && textDecoration.contains("underline")) { // final double delta = 2; // final Element elt2 = (Element) document.createElement("line"); // elt2.setAttribute("x1", format(x)); // elt2.setAttribute("y1", format(y + delta)); // elt2.setAttribute("x2", format(x + textLength)); // elt2.setAttribute("y2", format(y + delta)); // elt2.setAttribute("style", getStyleInternal(fill, "1.0", null)); // getG().appendChild(elt2); // } } ensureVisible(x, y); ensureVisible(x + textLength, y); } private final Map filterBackColor = new HashMap(); private String getIdFilterBackColor(String color) { String result = filterBackColor.get(color); if (result == null) { result = filterUid + filterBackColor.size(); filterBackColor.put(color, result); } return result; } private String getFilterBackColor(String color) { String id = filterBackColor.get(color); if (id != null) return id; id = getIdFilterBackColor(color); final Element filter = (Element) document.createElement("filter"); filter.setAttribute("id", id); filter.setAttribute("x", "0"); filter.setAttribute("y", "0"); filter.setAttribute("width", "1"); filter.setAttribute("height", "1"); addFilter(filter, "feFlood", "flood-color", color, "result", "flood"); addFilter(filter, "feComposite", "in", "SourceGraphic", "in2", "flood", "operator", "over"); defs.appendChild(filter); return id; } private Transformer getTransformer() throws TransformerException { final Transformer transformer = XmlFactories.newTransformer(); Log.info("Transformer=" + transformer.getClass()); // // Sets the standalone property in the first line of // // the output file. transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); // transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "SVG 1.1"); // transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); return transformer; } public void createXml(OutputStream os) throws TransformerException, IOException { createXmlInternal(os); // s = removeXmlHeader(s); } private String removeXmlHeader(String s) { s = s.replaceFirst("^<\\?xml [^<>]+?\\>", ""); return s; } private void createXmlInternal(OutputStream os) throws TransformerException { // if (this.classesForDarkness.size() > 0) // defs.appendChild(getStylesForDarkness()); // Get a DOMSource object that represents the // Document object final DOMSource source = new DOMSource(document); final int maxXscaled = (int) (maxX * scale); final int maxYscaled = (int) (maxY * scale); String style = "width:" + maxXscaled + "px;height:" + maxYscaled + "px;"; if (/* this.classesForDarkness.size() == 0 && */ backcolor != null) style += "background:" + backcolor + ";"; if (svgDimensionStyle) { root.setAttribute("style", style); root.setAttribute("width", format(maxX) + "px"); root.setAttribute("height", format(maxY) + "px"); } root.setAttribute("viewBox", "0 0 " + maxXscaled + " " + maxYscaled); root.setAttribute("zoomAndPan", "magnify"); root.setAttribute("preserveAspectRatio", preserveAspectRatio); // root.setAttribute("contentScriptType", "application/ecmascript"); root.setAttribute("contentStyleType", "text/css"); if (pendingBackground != null) { pendingBackground.setAttribute("width", format(maxX)); pendingBackground.setAttribute("height", format(maxY)); } // Get a StreamResult object that points to the // screen. Then transform the DOM sending XML to // the screen. final StreamResult scrResult = new StreamResult(os); getTransformer().transform(source, scrResult); } public void svgPath(double x, double y, UPath path, double deltaShadow) { manageShadow(deltaShadow); ensureVisible(x, y); final StringBuilder sb = new StringBuilder(); for (USegment seg : path) { final USegmentType type = seg.getSegmentType(); final double coord[] = seg.getCoord(); if (type == USegmentType.SEG_MOVETO) { sb.append("M" + format(coord[0] + x) + "," + format(coord[1] + y) + " "); ensureVisible(coord[0] + x + 2 * deltaShadow, coord[1] + y + 2 * deltaShadow); } else if (type == USegmentType.SEG_LINETO) { sb.append("L" + format(coord[0] + x) + "," + format(coord[1] + y) + " "); ensureVisible(coord[0] + x + 2 * deltaShadow, coord[1] + y + 2 * deltaShadow); } else if (type == USegmentType.SEG_QUADTO) { sb.append("Q" + format(coord[0] + x) + "," + format(coord[1] + y) + " " + format(coord[2] + x) + "," + format(coord[3] + y) + " "); ensureVisible(coord[0] + x + 2 * deltaShadow, coord[1] + y + 2 * deltaShadow); ensureVisible(coord[2] + x + 2 * deltaShadow, coord[3] + y + 2 * deltaShadow); } else if (type == USegmentType.SEG_CUBICTO) { sb.append("C" + format(coord[0] + x) + "," + format(coord[1] + y) + " " + format(coord[2] + x) + "," + format(coord[3] + y) + " " + format(coord[4] + x) + "," + format(coord[5] + y) + " "); ensureVisible(coord[0] + x + 2 * deltaShadow, coord[1] + y + 2 * deltaShadow); ensureVisible(coord[2] + x + 2 * deltaShadow, coord[3] + y + 2 * deltaShadow); ensureVisible(coord[4] + x + 2 * deltaShadow, coord[5] + y + 2 * deltaShadow); } else if (type == USegmentType.SEG_ARCTO) { // A25,25 0,0 5,395,40 sb.append("A" + format(coord[0]) + "," + format(coord[1]) + " " + formatBoolean(coord[2]) + " " + formatBoolean(coord[3]) + " " + formatBoolean(coord[4]) + " " + format(coord[5] + x) + "," + format(coord[6] + y) + " "); ensureVisible(coord[5] + coord[0] + x + 2 * deltaShadow, coord[6] + coord[1] + y + 2 * deltaShadow); } else if (type == USegmentType.SEG_CLOSE) { // Nothing } else { Log.println("unknown3 " + seg); } } if (hidden == false) { final Element elt = (Element) document.createElement("path"); elt.setAttribute("d", sb.toString()); elt.setAttribute("style", getStyle()); fillMe(elt); manageDarkStroke(elt); final String id = path.getComment(); if (id != null) elt.setAttribute("id", id); final String codeLine = path.getCodeLine(); if (codeLine != null) elt.setAttribute("codeLine", codeLine); addFilterShadowId(elt, deltaShadow); getG().appendChild(elt); } } // private String getFillClassForDark() { // final String result = "f_" + fill.toLowerCase().replaceAll("\\#", "") + "_" // + fillDark.toLowerCase().replaceAll("\\#", ""); // this.classesForDarkness.add(result); // return result; // } // // private String getStrokeClassForDark() { // final String result = "s_" + stroke.toLowerCase().replaceAll("\\#", "") + "_" // + strokeDark.toLowerCase().replaceAll("\\#", ""); // this.classesForDarkness.add(result); // return result; // } private void fillMe(Element elt) { if (fill.equals("#00000000")) return; // if (fill.equals(fillDark) == false) { // if (elt.getAttribute("class") != null && elt.getAttribute("class").length() != 0) // throw new IllegalStateException(); // // elt.setAttribute("class", getFillClassForDark()); // return; // } if (fill.matches("#[0-9A-Fa-f]{8}")) { elt.setAttribute("fill", fill.substring(0, 7)); final double opacity = Integer.parseInt(fill.substring(7), 16) / 255.0; elt.setAttribute("fill-opacity", String.format(Locale.US, "%1.5f", opacity)); } else { elt.setAttribute("fill", fill); } } private void addFilterShadowId(final Element elt, double deltaShadow) { if (deltaShadow > 0) elt.setAttribute("filter", "url(#" + shadowId + ")"); } private StringBuilder currentPath = null; public void newpath() { currentPath = new StringBuilder(); } public void moveto(double x, double y) { currentPath.append("M" + format(x) + "," + format(y) + " "); ensureVisible(x, y); } public void lineto(double x, double y) { currentPath.append("L" + format(x) + "," + format(y) + " "); ensureVisible(x, y); } public void closepath() { currentPath.append("Z "); } public void curveto(double x1, double y1, double x2, double y2, double x3, double y3) { currentPath.append("C" + format(x1) + "," + format(y1) + " " + format(x2) + "," + format(y2) + " " + format(x3) + "," + format(y3) + " "); ensureVisible(x1, y1); ensureVisible(x2, y2); ensureVisible(x3, y3); } public void quadto(double x1, double y1, double x2, double y2) { currentPath.append("Q" + format(x1) + "," + format(y1) + " " + format(x2) + "," + format(y2) + " "); ensureVisible(x1, y1); ensureVisible(x2, y2); } private String format(double x) { return TikzGraphics.format(x * scale); } private String formatBoolean(double x) { return x == 0 ? "0" : "1"; } public void fill(int windingRule) { if (hidden == false) { final Element elt = (Element) document.createElement("path"); elt.setAttribute("d", currentPath.toString()); fillMe(elt); // elt elt.setAttribute("style", getStyle()); getG().appendChild(elt); } currentPath = null; } public void drawPathIterator(double x, double y, PathIterator path) { this.newpath(); final double coord[] = new double[6]; while (path.isDone() == false) { final int code = path.currentSegment(coord); if (code == PathIterator.SEG_MOVETO) this.moveto(coord[0] + x, coord[1] + y); else if (code == PathIterator.SEG_LINETO) this.lineto(coord[0] + x, coord[1] + y); else if (code == PathIterator.SEG_CLOSE) this.closepath(); else if (code == PathIterator.SEG_CUBICTO) this.curveto(coord[0] + x, coord[1] + y, coord[2] + x, coord[3] + y, coord[4] + x, coord[5] + y); else if (code == PathIterator.SEG_QUADTO) this.quadto(coord[0] + x, coord[1] + y, coord[2] + x, coord[3] + y); else throw new UnsupportedOperationException("code=" + code); path.next(); } this.fill(path.getWindingRule()); } public void svgImage(BufferedImage image, double x, double y) throws IOException { if (hidden == false) { final Element elt = (Element) document.createElement("image"); elt.setAttribute("width", format(image.getWidth())); elt.setAttribute("height", format(image.getHeight())); elt.setAttribute("x", format(x)); elt.setAttribute("y", format(y)); final String s = toBase64(image); elt.setAttribute("xlink:href", "data:image/png;base64," + s); getG().appendChild(elt); } ensureVisible(x, y); ensureVisible(x + image.getWidth(), y + image.getHeight()); } public void svgImage(UImageSvg image, double x, double y) { // https://developer.mozilla.org/fr/docs/Web/SVG/Element/image if (hidden == false) { final Element elt = (Element) document.createElement("image"); elt.setAttribute("width", format(image.getWidth())); elt.setAttribute("height", format(image.getHeight())); elt.setAttribute("x", format(x)); elt.setAttribute("y", format(y)); String svg = manageScale(image); final String svgHeader; if (image.containsXlink()) svgHeader = ""; else svgHeader = ""; svg = svgHeader + svg.substring(5); final String s = toBase64(svg); elt.setAttribute("xlink:href", "data:image/svg+xml;base64," + s); getG().appendChild(elt); } ensureVisible(x, y); ensureVisible(x + image.getData("width"), y + image.getData("height")); } private String manageScale(UImageSvg svgImage) { final double svgScale = svgImage.getScale(); String svg = svgImage.getSvg(false); if (svgScale * scale == 1) return svg; final String svg2 = svg.replace('\n', ' ').replace('\r', ' '); if (svg2.contains("") == false) { svg = svg.replaceFirst("\\", ""); svg = svg.replaceFirst("\\", ""); } final String factor = format(svgScale); final String s1 = "\\ final Element filter = (Element) document.createElement("filter"); filter.setAttribute("id", shadowId); filter.setAttribute("x", "-1"); filter.setAttribute("y", "-1"); filter.setAttribute("width", "300%"); filter.setAttribute("height", "300%"); addFilter(filter, "feGaussianBlur", "result", "blurOut", "stdDeviation", "" + (2 * scale)); addFilter(filter, "feColorMatrix", "type", "matrix", "in", "blurOut", "result", "blurOut2", "values", "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"); addFilter(filter, "feOffset", "result", "blurOut3", "in", "blurOut2", "dx", "" + (4 * scale), "dy", "" + (4 * scale)); addFilter(filter, "feBlend", "in", "SourceGraphic", "in2", "blurOut3", "mode", "normal"); defs.appendChild(filter); } withShadow = true; } } private void addFilter(Element filter, String name, String... data) { assert data.length % 2 == 0; final Element elt = (Element) document.createElement(name); for (int i = 0; i < data.length; i += 2) elt.setAttribute(data[i], data[i + 1]); filter.appendChild(elt); } private boolean hidden; public void setHidden(boolean hidden) { this.hidden = hidden; } public static final String MD5_HEADER = "