Defs.java
package io.brunoborges.jairosvg.draw;
import static io.brunoborges.jairosvg.util.Helpers.size;
import java.util.HashMap;
import java.util.Map;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.dom.SvgFont;
import io.brunoborges.jairosvg.surface.Surface;
import io.brunoborges.jairosvg.util.UrlHelper;
/**
* SVG definitions: registration, use element, clip paths, and dispatch to
* specialized renderers.
*/
public final class Defs {
private Defs() {
}
/** Recursively parse all definition elements. */
public static void parseAllDefs(Surface surface, Node node) {
parseDef(surface, node);
for (Node child : node.children) {
parseAllDefs(surface, child);
}
}
/** Parse a single definition element. */
public static void parseDef(Surface surface, Node node) {
String tag = node.tag.toLowerCase();
// SVG fonts are keyed by font-family, not by id, so handle before the id check
if (tag.equals("font")) {
SvgFont svgFont = SvgFont.parse(node);
if (svgFont != null) {
surface.fonts.put(svgFont.family, svgFont);
}
}
String id = node.get("id");
if (id == null)
return;
// Build global ID index for O(1) <use> and cross-reference lookups
surface.nodeById.put(id, node);
if (tag.contains("marker"))
surface.markers.put(id, node);
if (tag.contains("gradient"))
surface.gradients.put(id, node);
if (tag.contains("pattern"))
surface.patterns.put(id, node);
if (tag.contains("mask"))
surface.masks.put(id, node);
if (tag.contains("filter"))
surface.filters.put(id, node);
if (tag.contains("image"))
surface.images.put(id, node);
if (tag.equals("clippath"))
surface.paths.put(id, node);
}
/**
* Apply gradient or pattern color. Returns true if a gradient/pattern was
* applied.
*/
public static boolean gradientOrPattern(Surface surface, Node node, String name, double opacity) {
if (name == null)
return false;
if (surface.gradients.containsKey(name)) {
return GradientPainter.drawGradient(surface, node, name, opacity);
}
if (surface.patterns.containsKey(name)) {
return PatternPainter.drawPattern(surface, node, name, opacity);
}
return false;
}
/** Handle clip-path. */
public static void clipPath(Surface surface, Node node) {
String id = node.get("id");
if (id != null) {
surface.paths.put(id, node);
}
}
/** Handle use element. */
public static void use(Surface surface, Node node) {
double x = size(surface, node.get("x"), "x");
double y = size(surface, node.get("y"), "y");
String href = node.getHref();
if (href == null)
return;
String fragment = UrlHelper.parseUrl(href).fragment();
if (fragment == null)
return;
// Find referenced element by ID in the index (O(1) instead of O(n) tree walk)
Node refNode = surface.nodeById.get(fragment);
if (refNode == null)
return;
// Propagate inheritable presentation attributes from <use> to the referenced
// node (SVG spec: <use> acts as parent for inheritance). Save originals to
// restore after drawing.
var notInherited = Node.notInheritedAttributes();
Map<String, String> saved = new HashMap<>();
for (var entry : node.entries()) {
String key = entry.getKey();
if (!notInherited.contains(key) && !refNode.has(key)) {
saved.put(key, null);
refNode.set(key, entry.getValue());
}
}
var savedTransform = surface.context.getTransform();
surface.context.translate(x, y);
// If it's an svg or symbol, treat as svg
if ("svg".equals(refNode.tag) || "symbol".equals(refNode.tag)) {
String origTag = refNode.tag;
String origWidth = refNode.get("width");
String origHeight = refNode.get("height");
refNode.tag = "svg";
if (node.has("width") && node.has("height")) {
refNode.set("width", node.get("width"));
refNode.set("height", node.get("height"));
} else if ("symbol".equals(origTag)) {
// SVG 2: without explicit width/height on <use>, default to the
// symbol's viewBox dimensions (browser "auto" behaviour) instead
// of falling back to 100% of the parent viewport.
String viewBox = refNode.get("viewBox");
if (viewBox != null) {
String[] vbParts = viewBox.strip().split("[\\s,]+");
if (vbParts.length == 4) {
refNode.set("width", vbParts[2]);
refNode.set("height", vbParts[3]);
}
}
}
surface.draw(refNode);
refNode.tag = origTag;
// Restore original width/height to prevent pollution across
// multiple <use> elements referencing the same symbol/svg
if (origWidth != null)
refNode.set("width", origWidth);
else
refNode.remove("width");
if (origHeight != null)
refNode.set("height", origHeight);
else
refNode.remove("height");
} else {
surface.draw(refNode);
}
// Clear any path data left by the referenced element's draw() so
// the caller's draw() does not re-fill/stroke it with <use>'s own
// default paint (black).
surface.path.reset();
surface.context.setTransform(savedTransform);
// Restore: remove attributes that were injected
for (var key : saved.keySet()) {
refNode.remove(key);
}
}
/** Handle markers. */
public static void marker(Surface surface, Node node) {
parseDef(surface, node);
}
/** Handle mask definition. */
public static void mask(Surface surface, Node node) {
parseDef(surface, node);
}
/** Handle filter definition. */
public static void filter(Surface surface, Node node) {
parseDef(surface, node);
}
/** Handle gradient definitions. */
public static void linearGradient(Surface surface, Node node) {
parseDef(surface, node);
}
public static void radialGradient(Surface surface, Node node) {
parseDef(surface, node);
}
/** Handle pattern definition. */
public static void pattern(Surface surface, Node node) {
parseDef(surface, node);
}
static Node findNodeById(Node root, String id) {
if (root == null || id == null)
return null;
if (id.equals(root.get("id")))
return root;
for (Node child : root.children) {
Node found = findNodeById(child, id);
if (found != null)
return found;
}
return null;
}
}