SvgGraphicsTeaVM.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2025, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 *
 * If you like this project or if you find it useful, you can support us at:
 *
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://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.teavm;

// ::uncomment when __TEAVM__
//import org.teavm.jso.JSBody;
//import org.teavm.jso.dom.html.HTMLDocument;
//import org.teavm.jso.dom.xml.Element;
//import org.teavm.jso.dom.xml.Document;
// ::done

/**
 * SVG Graphics implementation for TeaVM. Uses browser's native DOM API instead
 * of javax.xml.
 */
public class SvgGraphicsTeaVM {

	// ::uncomment when __TEAVM__
	
//	private static final String SVG_NS = "http://www.w3.org/2000/svg";
//
//	private final Element svgRoot;
//	private final Element defs;
//	private final Element mainGroup;
//	private final Document document;
//
//	private String fillColor = "#000000";
//	private String strokeColor = "#000000";
//	private double strokeWidth = 1.0;
//	private double[] strokeDasharray = null;
//
//	public SvgGraphicsTeaVM(int width, int height) {
//		this.document = HTMLDocument.current();
//
//		// Create SVG root element
//		this.svgRoot = createSvgElement("svg");
//		svgRoot.setAttribute("xmlns", SVG_NS);
//		svgRoot.setAttribute("version", "1.1");
//		svgRoot.setAttribute("width", String.valueOf(width));
//		svgRoot.setAttribute("height", String.valueOf(height));
//		svgRoot.setAttribute("viewBox", "0 0 " + width + " " + height);
//
//		// Create defs for gradients, filters, etc.
//		this.defs = createSvgElement("defs");
//		svgRoot.appendChild(defs);
//
//		// Create main group for all drawings
//		this.mainGroup = createSvgElement("g");
//		svgRoot.appendChild(mainGroup);
//	}
//
//	@JSBody(params = { "tagName" }, script = "return document.createElementNS('http://www.w3.org/2000/svg', tagName);")
//	private static native Element createSvgElement(String tagName);
//
//	public Element getSvgRoot() {
//		return svgRoot;
//	}
//
//	public void setFillColor(String color) {
//		this.fillColor = color != null ? color : "none";
//	}
//
//	public void setStrokeColor(String color) {
//		this.strokeColor = color != null ? color : "none";
//	}
//
//	public void setStrokeWidth(double width) {
//		this.strokeWidth = width;
//		this.strokeDasharray = null;
//	}
//
//	public void setStrokeWidth(double width, double[] dasharray) {
//		this.strokeWidth = width;
//		this.strokeDasharray = dasharray;
//	}
//
//	public void drawRectangle(double x, double y, double width, double height) {
//		drawRectangle(x, y, width, height, 0, 0);
//	}
//
//	public void drawRectangle(double x, double y, double width, double height, double rx, double ry) {
//		Element rect = createSvgElement("rect");
//		rect.setAttribute("x", format(x));
//		rect.setAttribute("y", format(y));
//		rect.setAttribute("width", format(width));
//		rect.setAttribute("height", format(height));
//		if (rx > 0)
//			rect.setAttribute("rx", format(rx));
//		if (ry > 0)
//			rect.setAttribute("ry", format(ry));
//		applyStyles(rect);
//		mainGroup.appendChild(rect);
//	}
//
//	public void drawCircle(double cx, double cy, double r) {
//		Element circle = createSvgElement("circle");
//		circle.setAttribute("cx", format(cx));
//		circle.setAttribute("cy", format(cy));
//		circle.setAttribute("r", format(r));
//		applyStyles(circle);
//		mainGroup.appendChild(circle);
//	}
//
//	public void drawEllipse(double cx, double cy, double rx, double ry) {
//		Element ellipse = createSvgElement("ellipse");
//		ellipse.setAttribute("cx", format(cx));
//		ellipse.setAttribute("cy", format(cy));
//		ellipse.setAttribute("rx", format(rx));
//		ellipse.setAttribute("ry", format(ry));
//		applyStyles(ellipse);
//		mainGroup.appendChild(ellipse);
//	}
//
//	public void drawLine(double x1, double y1, double x2, double y2) {
//		Element line = createSvgElement("line");
//		line.setAttribute("x1", format(x1));
//		line.setAttribute("y1", format(y1));
//		line.setAttribute("x2", format(x2));
//		line.setAttribute("y2", format(y2));
//		applyStrokeStyle(line);
//		mainGroup.appendChild(line);
//	}
//
//	public void drawPolyline(double... points) {
//		Element polyline = createSvgElement("polyline");
//		polyline.setAttribute("points", formatPoints(points));
//		polyline.setAttribute("fill", "none");
//		applyStrokeStyle(polyline);
//		mainGroup.appendChild(polyline);
//	}
//
//	public void drawPolygon(double... points) {
//		Element polygon = createSvgElement("polygon");
//		polygon.setAttribute("points", formatPoints(points));
//		applyStyles(polygon);
//		mainGroup.appendChild(polygon);
//	}
//
//	public void drawPath(String pathData) {
//		Element path = createSvgElement("path");
//		path.setAttribute("d", pathData);
//		applyStyles(path);
//		mainGroup.appendChild(path);
//	}
//
//	public void drawText(String text, double x, double y, String fontFamily, int fontSize) {
//		drawText(text, x, y, fontFamily, fontSize, null, null, null, null);
//	}
//
//	public void drawText(String text, double x, double y, String fontFamily, int fontSize, String fontWeight,
//			String fontStyle, String textDecoration, String backColor) {
//		// Draw background rectangle if backColor is specified
//		if (backColor != null) {
//			double[] metrics = measureTextCanvas(text, fontFamily, fontSize, fontWeight != null ? fontWeight : "normal");
//			double width = metrics[0];
//			double height = metrics[1];
//			Element rect = createSvgElement("rect");
//			rect.setAttribute("x", format(x));
//			rect.setAttribute("y", format(y - height + 2));
//			rect.setAttribute("width", format(width));
//			rect.setAttribute("height", format(height));
//			rect.setAttribute("fill", backColor);
//			rect.setAttribute("stroke", "none");
//			mainGroup.appendChild(rect);
//		}
//
//		Element textElem = createSvgElement("text");
//		textElem.setAttribute("x", format(x));
//		textElem.setAttribute("y", format(y));
//		textElem.setAttribute("font-size", String.valueOf(fontSize));
//		textElem.setAttribute("fill", fillColor);
//		// Preserve whitespace (multiple spaces, tabs, etc.)
//		textElem.setAttribute("xml:space", "preserve");
//		textElem.setAttribute("style", "white-space: pre");
//		if (fontWeight != null) {
//			textElem.setAttribute("font-weight", fontWeight);
//		}
//		if (fontStyle != null) {
//			textElem.setAttribute("font-style", fontStyle);
//		}
//		if (textDecoration != null) {
//			textElem.setAttribute("text-decoration", textDecoration);
//		}
//		if (fontFamily != null) {
//			textElem.setAttribute("font-family", fontFamily);
//
//			// For monospace fonts, replace spaces with non-breaking spaces
//			// to ensure consistent character width
//			// if (fontFamily.equalsIgnoreCase("monospace") || fontFamily.equalsIgnoreCase("courier"))
//			//	text = text.replace(' ', (char) 160);
//		}
//		textElem.setTextContent(text);
//		mainGroup.appendChild(textElem);
//	}
//
//	public Element createGroup() {
//		Element group = createSvgElement("g");
//		mainGroup.appendChild(group);
//		return group;
//	}
//
//	public String createLinearGradient(String id, String color1, String color2, boolean horizontal) {
//		Element gradient = createSvgElement("linearGradient");
//		gradient.setAttribute("id", id);
//		if (horizontal) {
//			gradient.setAttribute("x1", "0%");
//			gradient.setAttribute("y1", "0%");
//			gradient.setAttribute("x2", "100%");
//			gradient.setAttribute("y2", "0%");
//		} else {
//			gradient.setAttribute("x1", "0%");
//			gradient.setAttribute("y1", "0%");
//			gradient.setAttribute("x2", "0%");
//			gradient.setAttribute("y2", "100%");
//		}
//
//		Element stop1 = createSvgElement("stop");
//		stop1.setAttribute("offset", "0%");
//		stop1.setAttribute("stop-color", color1);
//		gradient.appendChild(stop1);
//
//		Element stop2 = createSvgElement("stop");
//		stop2.setAttribute("offset", "100%");
//		stop2.setAttribute("stop-color", color2);
//		gradient.appendChild(stop2);
//
//		defs.appendChild(gradient);
//		return "url(#" + id + ")";
//	}
//
//	private void applyStyles(Element element) {
//		element.setAttribute("fill", fillColor);
//		applyStrokeStyle(element);
//	}
//
//	private void applyStrokeStyle(Element element) {
//		element.setAttribute("stroke", strokeColor);
//		element.setAttribute("stroke-width", format(strokeWidth));
//		if (strokeDasharray != null && strokeDasharray.length >= 2)
//			element.setAttribute("stroke-dasharray", format(strokeDasharray[0]) + "," + format(strokeDasharray[1]));
//	}
//
//	private String format(double value) {
//		if (value == (int) value)
//			return String.valueOf((int) value);
//		return String.format("%.2f", value).replace(',', '.');
//	}
//
//	private String formatPoints(double... points) {
//		StringBuilder sb = new StringBuilder();
//		for (int i = 0; i < points.length; i += 2) {
//			if (sb.length() > 0)
//				sb.append(" ");
//			sb.append(format(points[i])).append(",").append(format(points[i + 1]));
//		}
//		return sb.toString();
//	}
//
//	/**
//	 * Returns the SVG as a string (serialized XML).
//	 */
//	@JSBody(params = { "element" }, script = "return new XMLSerializer().serializeToString(element);")
//	public static native String serializeToString(Element element);
//
//	public String toSvgString() {
//		return serializeToString(svgRoot);
//	}
//
//	// ========================================================================
//	// Text measurement methods
//	// ========================================================================
//
//	/**
//	 * Initializes the shared canvas for text measurement. Call once at startup for
//	 * best performance.
//	 */
//	@JSBody(script = "if (!window._measureCanvas) {" + "  window._measureCanvas = document.createElement('canvas');"
//			+ "  window._measureCtx = window._measureCanvas.getContext('2d');" + "}")
//	public static native void initMeasureCanvas();
//
//	/**
//	 * Measures text dimensions using Canvas API (optimized with shared canvas).
//	 * 
//	 * @param text       The text to measure
//	 * @param fontFamily Font family (e.g., "Arial")
//	 * @param fontSize   Font size in pixels
//	 * @param fontWeight Font weight (e.g., "normal", "bold")
//	 * @return Array with [width, height]
//	 */
//	@JSBody(params = { "text", "fontFamily", "fontSize", "fontWeight" }, script = "if (!window._measureCtx) {"
//			+ "  window._measureCanvas = document.createElement('canvas');"
//			+ "  window._measureCtx = window._measureCanvas.getContext('2d');" + "}" + "var ctx = window._measureCtx;"
//			+ "ctx.font = fontWeight + ' ' + fontSize + 'px ' + fontFamily;" + "var metrics = ctx.measureText(text);"
//			+ "var width = metrics.width;"
//			+ "var height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;"
//			+ "if (!height) height = fontSize * 1.2;" + "return [width, height];")
//	public static native double[] measureTextCanvas(String text, String fontFamily, int fontSize, String fontWeight);
//
//	/**
//	 * Measures text dimensions using Canvas API (creates new canvas each time). Use
//	 * measureTextCanvas() instead for better performance.
//	 */
//	@JSBody(params = { "text", "fontFamily", "fontSize",
//			"fontWeight" }, script = "var canvas = document.createElement('canvas');"
//					+ "var ctx = canvas.getContext('2d');"
//					+ "ctx.font = fontWeight + ' ' + fontSize + 'px ' + fontFamily;"
//					+ "var metrics = ctx.measureText(text);" + "var width = metrics.width;"
//					+ "var height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;"
//					+ "if (!height) height = fontSize * 1.2;" + "return [width, height];")
//	public static native double[] measureTextCanvasNoCache(String text, String fontFamily, int fontSize,
//			String fontWeight);
//
//	/**
//	 * Measures text using Canvas with normal weight.
//	 */
//	public static double[] measureText(String text, String fontFamily, int fontSize) {
//		return measureTextCanvas(text, fontFamily, fontSize, "normal");
//	}
//
//	/**
//	 * Gets text width only.
//	 */
//	public static double getTextWidth(String text, String fontFamily, int fontSize) {
//		return measureText(text, fontFamily, fontSize)[0];
//	}
//
//	/**
//	 * Gets text height only.
//	 */
//	public static double getTextHeight(String text, String fontFamily, int fontSize) {
//		return measureText(text, fontFamily, fontSize)[1];
//	}
//
//	/**
//	 * Measures text using SVG getBBox() method. More accurate but requires the SVG
//	 * to be in the DOM.
//	 * 
//	 * @param text       The text to measure
//	 * @param fontFamily Font family
//	 * @param fontSize   Font size in pixels
//	 * @return Array with [width, height, x, y] from bounding box
//	 */
//	@JSBody(params = { "text", "fontFamily",
//			"fontSize" }, script = "var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');"
//					+ "svg.style.position = 'absolute';" + "svg.style.visibility = 'hidden';"
//					+ "document.body.appendChild(svg);"
//					+ "var textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');"
//					+ "textEl.setAttribute('font-family', fontFamily);" + "textEl.setAttribute('font-size', fontSize);"
//					+ "textEl.textContent = text;" + "svg.appendChild(textEl);" + "var bbox = textEl.getBBox();"
//					+ "var result = [bbox.width, bbox.height, bbox.x, bbox.y];" + "document.body.removeChild(svg);"
//					+ "return result;")
//	public static native double[] measureTextSvgBBox(String text, String fontFamily, int fontSize);
//
//	/**
//	 * Detailed text metrics using Canvas API. Returns more information about text
//	 * positioning.
//	 * 
//	 * @return Array with [width, actualBoundingBoxAscent, actualBoundingBoxDescent,
//	 *         fontBoundingBoxAscent, fontBoundingBoxDescent]
//	 */
//	@JSBody(params = { "text", "fontFamily", "fontSize",
//			"fontWeight" }, script = "var canvas = document.createElement('canvas');"
//					+ "var ctx = canvas.getContext('2d');"
//					+ "ctx.font = fontWeight + ' ' + fontSize + 'px ' + fontFamily;" + "var m = ctx.measureText(text);"
//					+ "return [" + "  m.width," + "  m.actualBoundingBoxAscent || fontSize * 0.8,"
//					+ "  m.actualBoundingBoxDescent || fontSize * 0.2," + "  m.fontBoundingBoxAscent || fontSize * 0.8,"
//					+ "  m.fontBoundingBoxDescent || fontSize * 0.2" + "];")
//	public static native double[] getDetailedTextMetrics(String text, String fontFamily, int fontSize,
//			String fontWeight);
//
//	// ========================================================================
//	// Centered character drawing
//	// ========================================================================
//
//	/**
//	 * Draws a single character centered at the specified position.
//	 * Uses SVG text-anchor and dominant-baseline for centering.
//	 * 
//	 * @param c          The character to draw
//	 * @param x          Center X position
//	 * @param y          Center Y position
//	 * @param fontFamily Font family
//	 * @param fontSize   Font size in pixels
//	 */
//	public void drawCenteredCharacter(char c, double x, double y, String fontFamily, int fontSize) {
//		Element textElem = createSvgElement("text");
//		textElem.setAttribute("x", format(x));
//		textElem.setAttribute("y", format(y));
//		textElem.setAttribute("font-family", fontFamily);
//		textElem.setAttribute("font-size", String.valueOf(fontSize));
//		textElem.setAttribute("fill", fillColor);
//		// Center horizontally
//		textElem.setAttribute("text-anchor", "middle");
//		// Center vertically (dominant-baseline: central centers on x-height)
//		textElem.setAttribute("dominant-baseline", "central");
//		textElem.setTextContent(String.valueOf(c));
//		mainGroup.appendChild(textElem);
//	}
//
//	// ========================================================================
//	// Image drawing
//	// ========================================================================
//
//	/**
//	 * Draws an image at the specified position using a data URL.
//	 * The image is embedded directly in the SVG as a base64-encoded PNG.
//	 * 
//	 * @param dataUrl PNG data URL (data:image/png;base64,...)
//	 * @param x       X position
//	 * @param y       Y position
//	 * @param width   Image width
//	 * @param height  Image height
//	 */
//	public void drawImage(String dataUrl, double x, double y, int width, int height) {
//		Element image = createSvgElement("image");
//		image.setAttribute("x", format(x));
//		image.setAttribute("y", format(y));
//		image.setAttribute("width", String.valueOf(width));
//		image.setAttribute("height", String.valueOf(height));
//		// Use href for SVG 2.0 compatibility (xlink:href is deprecated)
//		image.setAttribute("href", dataUrl);
//		// Also set xlink:href for older browser compatibility
//		setXlinkHref(image, dataUrl);
//		mainGroup.appendChild(image);
//	}
//
//	@JSBody(params = { "element", "href" }, script = "element.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', href);")
//	private static native void setXlinkHref(Element element, String href);
//
//	// ========================================================================
//	// SVG image embedding
//	// ========================================================================
//
//	/**
//	 * Embeds an SVG image at the specified position.
//	 * The SVG content is parsed and inserted as a nested SVG element.
//	 * 
//	 * @param image UImageSvg containing the SVG data
//	 * @param x     X position
//	 * @param y     Y position
//	 */
//	public void drawSvgImage(net.sourceforge.plantuml.klimt.shape.UImageSvg image, double x, double y) {
//		final double svgScale = image.getScale();
//		String svg = image.getSvg(false);
//
//		// Handle scaling if needed
//		if (svgScale != 1) {
//			svg = wrapWithScaleTransform(svg, svgScale);
//		}
//
//		// Create a group to position the embedded SVG
//		Element wrapper = createSvgElement("g");
//		wrapper.setAttribute("transform", "translate(" + format(x) + "," + format(y) + ")");
//
//		// Parse and insert the SVG content
//		insertSvgContent(wrapper, svg);
//
//		mainGroup.appendChild(wrapper);
//	}
//
//	/**
//	 * Wraps SVG content with a scale transform.
//	 */
//	private String wrapWithScaleTransform(String svg, double scale) {
//		String svg2 = svg.replace('\n', ' ').replace('\r', ' ');
//		if (!svg2.contains("<g ") && !svg2.contains("<g>")) {
//			svg = svg.replaceFirst("\\<svg\\>", "<svg><g>");
//			svg = svg.replaceFirst("\\</svg\\>", "</g></svg>");
//		}
//		final String factor = format(scale);
//		svg = svg.replaceFirst("\\<g\\b", "<g transform=\"scale(" + factor + "," + factor + ")\" ");
//		return svg;
//	}
//
//	/**
//	 * Parses SVG string and inserts its content into the parent element.
//	 */
//	@JSBody(params = { "parent", "svgContent" }, script = 
//		"var parser = new DOMParser();" +
//		"var doc = parser.parseFromString(svgContent, 'image/svg+xml');" +
//		"var svgElem = doc.documentElement;" +
//		"if (svgElem && svgElem.tagName.toLowerCase() === 'svg') {" +
//		"  var imported = document.importNode(svgElem, true);" +
//		"  parent.appendChild(imported);" +
//		"}")
//	private static native void insertSvgContent(Element parent, String svgContent);
	
	// ::done

}