Defs.java
package io.brunoborges.jairosvg.draw;
import static io.brunoborges.jairosvg.util.Helpers.clipMarkerBox;
import static io.brunoborges.jairosvg.util.Helpers.pointAngle;
import static io.brunoborges.jairosvg.util.Helpers.preserveRatio;
import static io.brunoborges.jairosvg.util.Helpers.size;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.brunoborges.jairosvg.css.Colors;
import io.brunoborges.jairosvg.dom.BoundingBox;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.dom.SvgFont;
import io.brunoborges.jairosvg.surface.Surface;
import io.brunoborges.jairosvg.util.Helpers;
import io.brunoborges.jairosvg.util.UrlHelper;
/**
* SVG definitions: gradients, patterns, clips, masks, filters, markers, use.
* Port of CairoSVG defs.py
*/
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;
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 drawGradient(surface, node, name, opacity);
}
if (surface.patterns.containsKey(name)) {
return drawPattern(surface, node, name, opacity);
}
return false;
}
/** Draw a gradient. */
public static boolean drawGradient(Surface surface, Node node, String name, double opacity) {
Node gradientNode = surface.gradients.get(name);
if (gradientNode == null)
return false;
// Follow href chain
String href = gradientNode.getHref();
if (href != null && !href.isEmpty()) {
String refId = UrlHelper.parseUrl(href).fragment();
if (refId != null && surface.gradients.containsKey(refId)) {
Node refNode = surface.gradients.get(refId);
// Inherit stops if this gradient has none
if (gradientNode.children.isEmpty()) {
gradientNode.children = new ArrayList<>(refNode.children);
}
// Inherit attributes
for (var entry : refNode.entries()) {
if (!gradientNode.has(entry.getKey())) {
gradientNode.set(entry.getKey(), entry.getValue());
}
}
}
}
boolean userSpace = "userSpaceOnUse".equals(gradientNode.get("gradientUnits"));
// Collect stops
java.util.List<float[]> stops = new ArrayList<>();
java.util.List<Color> colors = new ArrayList<>();
float lastOffset = 0;
for (Node child : gradientNode.children) {
if (!"stop".equals(child.tag))
continue;
float offset = parsePercent(child.get("offset", "0"));
offset = Math.max(lastOffset, Math.min(1, offset));
lastOffset = offset;
Colors.RGBA rgba = surface.mapColor(child.get("stop-color", "black"),
parseDouble(child.get("stop-opacity", "1")) * opacity);
stops.add(new float[]{offset});
colors.add(new Color((float) rgba.r(), (float) rgba.g(), (float) rgba.b(), (float) rgba.a()));
}
if (stops.isEmpty())
return false;
// Ensure we have at least 2 stops
if (stops.size() == 1) {
stops.add(new float[]{1.0f});
colors.add(colors.get(0));
}
Paint paint;
if ("linearGradient".equals(gradientNode.tag)) {
float x1, y1, x2, y2;
if (userSpace) {
x1 = (float) size(surface, gradientNode.get("x1", "0%"), "x");
y1 = (float) size(surface, gradientNode.get("y1", "0%"), "y");
x2 = (float) size(surface, gradientNode.get("x2", "100%"), "x");
y2 = (float) size(surface, gradientNode.get("y2", "0%"), "y");
} else {
BoundingBox.Box bb = BoundingBox.calculate(surface, node);
if (bb == null || !BoundingBox.isNonEmpty(bb))
return false;
float pctX1 = parsePercent(gradientNode.get("x1", "0%"));
float pctY1 = parsePercent(gradientNode.get("y1", "0%"));
float pctX2 = parsePercent(gradientNode.get("x2", "100%"));
float pctY2 = parsePercent(gradientNode.get("y2", "0%"));
x1 = (float) (bb.minX() + pctX1 * bb.width());
y1 = (float) (bb.minY() + pctY1 * bb.height());
x2 = (float) (bb.minX() + pctX2 * bb.width());
y2 = (float) (bb.minY() + pctY2 * bb.height());
}
if (x1 == x2 && y1 == y2) {
// Degenerate gradient - use last color
surface.context.setColor(colors.get(colors.size() - 1));
return true;
}
float[] fractions = new float[stops.size()];
Color[] colorArr = colors.toArray(new Color[0]);
for (int i = 0; i < stops.size(); i++)
fractions[i] = stops.get(i)[0];
// Fix duplicate fractions, clamp to [0,1], ensure strictly increasing
for (int i = 1; i < fractions.length; i++) {
if (fractions[i] <= fractions[i - 1]) {
fractions[i] = fractions[i - 1] + 0.0001f;
}
}
for (int i = 0; i < fractions.length; i++) {
fractions[i] = Math.min(1.0f, Math.max(0.0f, fractions[i]));
}
for (int i = fractions.length - 2; i >= 0; i--) {
if (fractions[i] >= fractions[i + 1]) {
fractions[i] = Math.nextDown(fractions[i + 1]);
}
}
paint = new LinearGradientPaint(x1, y1, x2, y2, fractions, colorArr,
getSpreadMethod(gradientNode.get("spreadMethod", "pad")));
} else if ("radialGradient".equals(gradientNode.tag)) {
float cx, cy, r, fx, fy;
if (userSpace) {
r = (float) size(surface, gradientNode.get("r", "50%"), "xy");
cx = (float) size(surface, gradientNode.get("cx", "50%"), "x");
cy = (float) size(surface, gradientNode.get("cy", "50%"), "y");
fx = (float) size(surface, gradientNode.get("fx", String.valueOf(cx)), "x");
fy = (float) size(surface, gradientNode.get("fy", String.valueOf(cy)), "y");
} else {
BoundingBox.Box bb = BoundingBox.calculate(surface, node);
if (bb == null || !BoundingBox.isNonEmpty(bb))
return false;
float pctR = parsePercent(gradientNode.get("r", "50%"));
float pctCx = parsePercent(gradientNode.get("cx", "50%"));
float pctCy = parsePercent(gradientNode.get("cy", "50%"));
cx = (float) (bb.minX() + pctCx * bb.width());
cy = (float) (bb.minY() + pctCy * bb.height());
r = (float) (pctR * Math.max(bb.width(), bb.height()));
String fxStr = gradientNode.get("fx");
String fyStr = gradientNode.get("fy");
fx = fxStr != null ? (float) (bb.minX() + parsePercent(fxStr) * bb.width()) : cx;
fy = fyStr != null ? (float) (bb.minY() + parsePercent(fyStr) * bb.height()) : cy;
}
if (r <= 0)
return false;
// Ensure focus is within radius
double dist = Math.hypot(fx - cx, fy - cy);
if (dist >= r) {
double scale = (r * 0.99) / dist;
fx = (float) (cx + (fx - cx) * scale);
fy = (float) (cy + (fy - cy) * scale);
}
float[] fractions = new float[stops.size()];
Color[] colorArr = colors.toArray(new Color[0]);
for (int i = 0; i < stops.size(); i++)
fractions[i] = stops.get(i)[0];
for (int i = 1; i < fractions.length; i++) {
if (fractions[i] <= fractions[i - 1]) {
fractions[i] = fractions[i - 1] + 0.0001f;
}
}
for (int i = 0; i < fractions.length; i++) {
fractions[i] = Math.min(1.0f, Math.max(0.0f, fractions[i]));
}
for (int i = fractions.length - 2; i >= 0; i--) {
if (fractions[i] >= fractions[i + 1]) {
fractions[i] = Math.nextDown(fractions[i + 1]);
}
}
paint = new RadialGradientPaint(new Point2D.Float(cx, cy), r, new Point2D.Float(fx, fy), fractions,
colorArr, getSpreadMethod(gradientNode.get("spreadMethod", "pad")));
} else {
return false;
}
surface.context.setPaint(paint);
return true;
}
/** Draw a pattern. */
public static boolean drawPattern(Surface surface, Node node, String name, double opacity) {
Node patternNode = surface.patterns.get(name);
if (patternNode == null)
return false;
// Follow href chain
String href = patternNode.getHref();
if (href != null && !href.isEmpty()) {
String refId = UrlHelper.parseUrl(href).fragment();
if (refId != null && surface.patterns.containsKey(refId)) {
Node refNode = surface.patterns.get(refId);
if (patternNode.children.isEmpty()) {
patternNode.children = new ArrayList<>(refNode.children);
}
for (var entry : refNode.entries()) {
if (!patternNode.has(entry.getKey())) {
patternNode.set(entry.getKey(), entry.getValue());
}
}
}
}
if (patternNode.children.isEmpty())
return false;
boolean userSpace = "userSpaceOnUse".equals(patternNode.get("patternUnits"));
double patX, patY, patW, patH;
if (userSpace) {
patX = size(surface, patternNode.get("x", "0"), "x");
patY = size(surface, patternNode.get("y", "0"), "y");
patW = size(surface, patternNode.get("width", "0"), "x");
patH = size(surface, patternNode.get("height", "0"), "y");
} else {
BoundingBox.Box bb = BoundingBox.calculate(surface, node);
if (bb == null || !BoundingBox.isNonEmpty(bb))
return false;
double pctX = parsePercent(patternNode.get("x", "0"));
double pctY = parsePercent(patternNode.get("y", "0"));
double pctW = parsePercent(patternNode.get("width", "0"));
double pctH = parsePercent(patternNode.get("height", "0"));
patX = bb.minX() + pctX * bb.width();
patY = bb.minY() + pctY * bb.height();
patW = pctW * bb.width();
patH = pctH * bb.height();
}
if (patW <= 0 || patH <= 0)
return false;
// Render pattern content into a BufferedImage
int imgW = Math.min(4096, Math.max(1, (int) Math.ceil(patW)));
int imgH = Math.min(4096, Math.max(1, (int) Math.ceil(patH)));
BufferedImage patImage = new BufferedImage(imgW, imgH, BufferedImage.TYPE_INT_ARGB);
Graphics2D patG2d = patImage.createGraphics();
patG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
patG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// Apply overall opacity to all pattern content
patG2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) opacity));
// Save surface state
Graphics2D savedContext = surface.context;
GeneralPath savedPath = surface.path;
double savedWidth = surface.contextWidth;
double savedHeight = surface.contextHeight;
surface.context = patG2d;
surface.path = new GeneralPath();
surface.contextWidth = patW;
surface.contextHeight = patH;
// Translate so pattern children (in user space) are drawn relative to the tile
// origin
if (patX != 0 || patY != 0) {
patG2d.translate(-patX, -patY);
}
// Draw pattern children
for (Node child : patternNode.children) {
surface.draw(child);
}
patG2d.dispose();
// Restore surface state
surface.context = savedContext;
surface.path = savedPath;
surface.contextWidth = savedWidth;
surface.contextHeight = savedHeight;
// Create anchor rectangle (untransformed pattern region)
Rectangle2D anchor = new Rectangle2D.Double(patX, patY, patW, patH);
// Apply patternTransform by pre-transforming the pattern image and anchor,
// so that rotation/skew are preserved in the tile pixels.
BufferedImage paintImage = patImage;
String ptStr = patternNode.get("patternTransform");
if (ptStr != null && !ptStr.isEmpty()) {
AffineTransform pt = Helpers.parseTransform(surface, ptStr);
if (pt != null && !pt.isIdentity()) {
// Compute transformed anchor bounds in user space
Rectangle2D dstRect = pt.createTransformedShape(anchor).getBounds2D();
int dstW = Math.max(1, (int) Math.ceil(dstRect.getWidth()));
int dstH = Math.max(1, (int) Math.ceil(dstRect.getHeight()));
BufferedImage transformedImage = new BufferedImage(dstW, dstH, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = transformedImage.createGraphics();
try {
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// Map: input pixel → pattern space → user space → output pixel
AffineTransform at = new AffineTransform();
at.translate(-dstRect.getX(), -dstRect.getY());
at.concatenate(pt);
at.translate(patX, patY);
g2.drawImage(patImage, at, null);
} finally {
g2.dispose();
}
paintImage = transformedImage;
anchor = dstRect;
}
}
// Create TexturePaint
TexturePaint texturePaint = new TexturePaint(paintImage, anchor);
surface.context.setPaint(texturePaint);
return true;
}
/** 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 tree
Node refNode = findNodeById(surface.rootNode, fragment);
if (refNode == null)
return;
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;
refNode.tag = "svg";
if (node.has("width") && node.has("height")) {
refNode.set("width", node.get("width"));
refNode.set("height", node.get("height"));
}
surface.draw(refNode);
refNode.tag = origTag;
} else {
surface.draw(refNode);
}
surface.context.setTransform(savedTransform);
}
/** Handle filter preparation. */
public static void prepareFilter(Surface surface, Node node, String name) {
// Filter processing is handled after node rendering.
}
/** Apply filter primitives to an image. */
public static BufferedImage applyFilter(Surface surface, String name, BufferedImage sourceGraphic) {
Node filterNode = surface.filters.get(name);
if (filterNode == null) {
return sourceGraphic;
}
Map<String, BufferedImage> results = new HashMap<>();
results.put("SourceGraphic", sourceGraphic);
BufferedImage last = sourceGraphic;
for (Node child : filterNode.children) {
BufferedImage input = resolveInput(results, child.get("in"), last, sourceGraphic);
BufferedImage output = switch (child.tag) {
case "feGaussianBlur" -> gaussianBlur(input, parseDoubleOr(child.get("stdDeviation"), 0));
case "feOffset" ->
offset(input, size(surface, child.get("dx", "0")), size(surface, child.get("dy", "0")));
case "feFlood" -> flood(sourceGraphic.getWidth(), sourceGraphic.getHeight(),
child.get("flood-color", "black"), parseDoubleOr(child.get("flood-opacity"), 1));
case "feMerge" -> merge(results, child, sourceGraphic.getWidth(), sourceGraphic.getHeight(), last);
case "feDropShadow" -> dropShadow(surface, input, child);
default -> input;
};
String resultName = child.get("result");
if (resultName != null && !resultName.isEmpty()) {
results.put(resultName, output);
}
last = output;
results.put("last", last);
}
return last;
}
/** Handle mask (simplified). */
public static void paintMask(Surface surface, Node node, String name, double opacity) {
// Simplified mask - just apply opacity
if (opacity < 1) {
surface.context.setComposite(
java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, (float) opacity));
}
}
/** Handle markers. */
public static void marker(Surface surface, Node node) {
parseDef(surface, node);
}
public static void drawMarkers(Surface surface, Node node) {
if (node.vertices == null || node.vertices.isEmpty()) {
return;
}
List<MarkerVertex> markerVertices = collectMarkerVertices(node.vertices);
if (markerVertices.isEmpty()) {
return;
}
String startId = markerId(node.get("marker-start"));
String midId = markerId(node.get("marker-mid"));
String endId = markerId(node.get("marker-end"));
drawMarkerAtVertex(surface, node, surface.markers.get(startId), markerVertices.getFirst(), true,
markerVertices.size() == 1);
if (markerVertices.size() == 1) {
drawMarkerAtVertex(surface, node, surface.markers.get(endId), markerVertices.getFirst(), false, true);
return;
}
for (int i = 1; i < markerVertices.size() - 1; i++) {
drawMarkerAtVertex(surface, node, surface.markers.get(midId), markerVertices.get(i), false, false);
}
drawMarkerAtVertex(surface, node, surface.markers.get(endId), markerVertices.getLast(), false, true);
}
/** 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;
}
private static MultipleGradientPaint.CycleMethod getSpreadMethod(String method) {
return switch (method) {
case "reflect" -> MultipleGradientPaint.CycleMethod.REFLECT;
case "repeat" -> MultipleGradientPaint.CycleMethod.REPEAT;
default -> MultipleGradientPaint.CycleMethod.NO_CYCLE;
};
}
private static float parsePercent(String s) {
if (s == null)
return 0;
s = s.strip();
if (s.endsWith("%")) {
return Float.parseFloat(s.substring(0, s.length() - 1)) / 100f;
}
return Float.parseFloat(s);
}
private static double parseDouble(String s) {
try {
return Double.parseDouble(s);
} catch (Exception e) {
return 0;
}
}
private static List<MarkerVertex> collectMarkerVertices(List<Object> vertices) {
List<MarkerVertex> markerVertices = new ArrayList<>();
boolean expectsPoint = true;
MarkerVertex previous = null;
for (Object vertex : vertices) {
if (vertex == null) {
expectsPoint = true;
previous = null;
continue;
}
if (!(vertex instanceof double[] values) || values.length != 2) {
continue;
}
if (expectsPoint) {
MarkerVertex current = new MarkerVertex(values[0], values[1]);
markerVertices.add(current);
if (previous != null) {
previous.outAngle = pointAngle(previous.x, previous.y, current.x, current.y);
current.inAngle = previous.outAngle;
}
previous = current;
expectsPoint = false;
} else {
expectsPoint = true;
}
}
return markerVertices;
}
private static String markerId(String markerValue) {
if (markerValue == null || markerValue.isEmpty()) {
return null;
}
return UrlHelper.parseUrl(markerValue).fragment();
}
private static void drawMarkerAtVertex(Surface surface, Node node, Node markerNode, MarkerVertex vertex,
boolean isStart, boolean isEnd) {
if (markerNode == null || vertex == null) {
return;
}
double angle = markerAngle(markerNode, vertex, isStart, isEnd);
AffineTransform savedTransform = surface.context.getTransform();
Shape savedClip = surface.context.getClip();
surface.context.translate(vertex.x, vertex.y);
surface.context.rotate(angle);
if (!"userSpaceOnUse".equals(markerNode.get("markerUnits"))) {
double strokeWidth = size(surface, node.get("stroke-width", "1"));
surface.context.scale(strokeWidth, strokeWidth);
}
double[] ratio = preserveRatio(surface, markerNode);
surface.context.scale(ratio[0], ratio[1]);
surface.context.translate(ratio[2], ratio[3]);
if (!"visible".equals(markerNode.get("overflow", "hidden"))) {
double[] clipBox = clipMarkerBox(surface, markerNode, ratio[0], ratio[1]);
surface.context.clip(new Rectangle2D.Double(clipBox[0], clipBox[1], clipBox[2], clipBox[3]));
}
for (Node child : markerNode.children) {
surface.draw(child);
}
surface.context.setTransform(savedTransform);
surface.context.setClip(savedClip);
}
private static double markerAngle(Node markerNode, MarkerVertex vertex, boolean isStart, boolean isEnd) {
String orient = markerNode.get("orient", "0");
if ("auto".equals(orient) || "auto-start-reverse".equals(orient)) {
double angle = isEnd ? vertex.inAngle : vertex.outAngle;
if (Double.isNaN(angle)) {
angle = isEnd ? vertex.outAngle : vertex.inAngle;
}
if (Double.isNaN(angle)) {
angle = 0;
}
if (isStart && "auto-start-reverse".equals(orient)) {
angle += Math.PI;
}
return angle;
}
try {
return Math.toRadians(Double.parseDouble(orient));
} catch (NumberFormatException e) {
return 0;
}
}
private static final class MarkerVertex {
final double x;
final double y;
double inAngle = Double.NaN;
double outAngle = Double.NaN;
MarkerVertex(double x, double y) {
this.x = x;
this.y = y;
}
}
private static double parseDoubleOr(String s, double def) {
if (s == null) {
return def;
}
try {
return Double.parseDouble(s);
} catch (Exception e) {
return def;
}
}
private static BufferedImage resolveInput(Map<String, BufferedImage> results, String in, BufferedImage last,
BufferedImage sourceGraphic) {
if (in == null || in.isEmpty()) {
return last;
}
return switch (in) {
case "SourceGraphic" -> sourceGraphic;
default -> results.getOrDefault(in, last);
};
}
private static BufferedImage offset(BufferedImage input, double dx, double dy) {
BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = output.createGraphics();
g.drawImage(input, (int) Math.round(dx), (int) Math.round(dy), null);
g.dispose();
return output;
}
private static BufferedImage flood(int width, int height, String color, double opacity) {
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = output.createGraphics();
Colors.RGBA floodColor = Colors.color(color, opacity);
g.setColor(new Color((float) floodColor.r(), (float) floodColor.g(), (float) floodColor.b(),
(float) floodColor.a()));
g.fillRect(0, 0, width, height);
g.dispose();
return output;
}
private static BufferedImage merge(Map<String, BufferedImage> results, Node mergeNode, int width, int height,
BufferedImage last) {
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = output.createGraphics();
for (Node mergeChild : mergeNode.children) {
if (!"feMergeNode".equals(mergeChild.tag)) {
continue;
}
BufferedImage input = resolveInput(results, mergeChild.get("in"), last, results.get("SourceGraphic"));
g.drawImage(input, 0, 0, null);
}
g.dispose();
return output;
}
private static BufferedImage dropShadow(Surface surface, BufferedImage input, Node node) {
double stdDeviation = parseDouble(node.get("stdDeviation"));
double dx = size(surface, node.get("dx", "0"));
double dy = size(surface, node.get("dy", "0"));
String floodColor = node.get("flood-color", "black");
double floodOpacity = parseDoubleOr(node.get("flood-opacity"), 1);
BufferedImage shadow = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = shadow.createGraphics();
g.drawImage(input, 0, 0, null);
g.setComposite(AlphaComposite.SrcIn);
Colors.RGBA rgba = Colors.color(floodColor, floodOpacity);
g.setColor(new Color((float) rgba.r(), (float) rgba.g(), (float) rgba.b(), (float) rgba.a()));
g.fillRect(0, 0, input.getWidth(), input.getHeight());
g.dispose();
BufferedImage blurredShadow = gaussianBlur(shadow, stdDeviation);
BufferedImage offsetShadow = offset(blurredShadow, dx, dy);
BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D out = output.createGraphics();
out.drawImage(offsetShadow, 0, 0, null);
out.drawImage(input, 0, 0, null);
out.dispose();
return output;
}
private static BufferedImage gaussianBlur(BufferedImage input, double stdDeviation) {
if (stdDeviation <= 0) {
return input;
}
float[] kernelValues = gaussianKernel(stdDeviation);
Kernel horizontalKernel = new Kernel(kernelValues.length, 1, kernelValues);
Kernel verticalKernel = new Kernel(1, kernelValues.length, kernelValues);
ConvolveOp horizontalOp = new ConvolveOp(horizontalKernel, ConvolveOp.EDGE_NO_OP, null);
ConvolveOp verticalOp = new ConvolveOp(verticalKernel, ConvolveOp.EDGE_NO_OP, null);
BufferedImage temp = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
horizontalOp.filter(input, temp);
verticalOp.filter(temp, output);
return output;
}
private static float[] gaussianKernel(double stdDeviation) {
int radius = Math.min(128, Math.max(1, (int) Math.ceil(stdDeviation * 3)));
int size = radius * 2 + 1;
float[] kernel = new float[size];
double sigma2 = stdDeviation * stdDeviation * 2;
double sum = 0;
for (int i = -radius; i <= radius; i++) {
double value = Math.exp(-(i * i) / sigma2);
kernel[i + radius] = (float) value;
sum += value;
}
for (int i = 0; i < size; i++) {
kernel[i] /= (float) sum;
}
return kernel;
}
}