Adding an extension handler to the SVG library to allow linear and radial gradients to export as SVG

The extension handler is a copy of https://gist.github.com/msteiger/4509119 by Martin Steiger with minor changes for Processing
Several classes and methods in PShapeSVG and PShapeJava2D have been made public or modified to support the export of gradients
This commit is contained in:
Benjamin Fox
2025-01-19 15:38:54 -05:00
parent 628b69db24
commit c8a85e3a58
4 changed files with 329 additions and 8 deletions
@@ -0,0 +1,209 @@
package processing.svg;
import static org.apache.batik.util.SVGConstants.*;
import processing.awt.PShapeJava2D.LinearGradientPaint;
import processing.awt.PShapeJava2D.RadialGradientPaint;
import java.awt.Color;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.Objects;
import org.apache.batik.svggen.DefaultExtensionHandler;
import org.apache.batik.svggen.SVGColor;
import org.apache.batik.svggen.SVGGeneratorContext;
import org.apache.batik.svggen.SVGPaintDescriptor;
import org.w3c.dom.Element;
/**
* Extension of Batik's {@link DefaultExtensionHandler} which handles different kinds of Paint objects
* based on the extenstion by Martin Steiger https://gist.github.com/msteiger/4509119
* modified to work with Processing's SVG export library, by Benjamin Fox https://github.com/tracerstar
*/
public class GradientExtensionHandler extends DefaultExtensionHandler {
@Override
public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext genCtx) {
// Handle LinearGradientPaint
if (paint instanceof LinearGradientPaint) {
return getLgpDescriptor((LinearGradientPaint) paint, genCtx);
}
// Handle RadialGradientPaint
if (paint instanceof RadialGradientPaint) {
return getRgpDescriptor((RadialGradientPaint) paint, genCtx);
}
return super.handlePaint(paint, genCtx);
}
private SVGPaintDescriptor getLgpDescriptor(LinearGradientPaint gradient, SVGGeneratorContext genCtx) {
Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_LINEAR_GRADIENT_TAG);
// Create and set unique XML id
String id = genCtx.getIDGenerator().generateID("gradient");
gradElem.setAttribute(SVG_ID_ATTRIBUTE, id);
// Set x,y pairs
Point2D startPt = gradient.getStartPoint();
gradElem.setAttribute("x1", String.valueOf(startPt.getX()));
gradElem.setAttribute("y1", String.valueOf(startPt.getY()));
Point2D endPt = gradient.getEndPoint();
gradElem.setAttribute("x2", String.valueOf(endPt.getX()));
gradElem.setAttribute("y2", String.valueOf(endPt.getY()));
//TODO: change this to be: addMgpAttributes after refactoring the paint methods
addLgpAttributes(gradElem, genCtx, gradient);
return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem);
}
private SVGPaintDescriptor getRgpDescriptor(RadialGradientPaint gradient, SVGGeneratorContext genCtx) {
Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_RADIAL_GRADIENT_TAG);
// Create and set unique XML id
String id = genCtx.getIDGenerator().generateID("gradient");
gradElem.setAttribute(SVG_ID_ATTRIBUTE, id);
// Set x,y pairs
Point2D centerPt = gradient.getCenterPoint();
gradElem.setAttribute("cx", String.valueOf(centerPt.getX()));
gradElem.setAttribute("cy", String.valueOf(centerPt.getY()));
Point2D focusPt = gradient.getFocusPoint();
gradElem.setAttribute("fx", String.valueOf(focusPt.getX()));
gradElem.setAttribute("fy", String.valueOf(focusPt.getY()));
gradElem.setAttribute("r", String.valueOf(gradient.getRadius()));
//TODO: change this to be: addMgpAttributes after refactoring the paint methods
addRgpAttributes(gradElem, genCtx, gradient);
return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem);
}
/*
Being lazy here to duplicate the methods so we don't have to refactor the two gradient paints
to implement java.awt.MultipleGradientPaint
TODO: make the effort to refactor them to properly implement java.awt.MultipleGradientPaint
*/
private void addLgpAttributes(Element gradElem, SVGGeneratorContext genCtx, LinearGradientPaint gradient) {
gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE);
// Set cycle method
switch (gradient.getCycleMethod()) {
case REFLECT:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE);
break;
case REPEAT:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE);
break;
case NO_CYCLE:
default:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default
break;
}
// Set color space
switch (gradient.getColorSpace()) {
case LINEAR_RGB:
gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE);
break;
case SRGB:
default:
gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE);
break;
}
// Set transform matrix if not identity
AffineTransform tf = gradient.getTransform();
if (!Objects.isNull(tf) && !tf.isIdentity()) {
String matrix = "matrix(" +
tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " +
tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")";
gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix);
}
// Convert gradient stops
Color[] colors = gradient.getColors();
float[] fracs = gradient.getFractions();
for (int i = 0; i < colors.length; i++) {
Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx);
stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%");
stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue());
if (colors[i].getAlpha() != 255) {
stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue());
}
gradElem.appendChild(stop);
}
}
private void addRgpAttributes(Element gradElem, SVGGeneratorContext genCtx, RadialGradientPaint gradient) {
gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE);
// Set cycle method
switch (gradient.getCycleMethod()) {
case REFLECT:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REFLECT_VALUE);
break;
case REPEAT:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_REPEAT_VALUE);
break;
case NO_CYCLE:
default:
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, SVG_PAD_VALUE); // this is the default
break;
}
// Set color space
switch (gradient.getColorSpace()) {
case LINEAR_RGB:
gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_LINEAR_RGB_VALUE);
break;
case SRGB:
default:
gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, SVG_SRGB_VALUE);
break;
}
// Set transform matrix if not identity
AffineTransform tf = gradient.getTransform();
if (!Objects.isNull(tf) && !tf.isIdentity()) {
String matrix = "matrix(" +
tf.getScaleX() + " " + tf.getShearX() + " " + tf.getTranslateX() + " " +
tf.getScaleY() + " " + tf.getShearY() + " " + tf.getTranslateY() + ")";
gradElem.setAttribute(SVG_TRANSFORM_ATTRIBUTE, matrix);
}
// Convert gradient stops
Color[] colors = gradient.getColors();
float[] fracs = gradient.getFractions();
for (int i = 0; i < colors.length; i++) {
Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx);
stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%");
stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue());
if (colors[i].getAlpha() != 255) {
stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue());
}
gradElem.appendChild(stop);
}
}
}
@@ -87,6 +87,10 @@ public class PGraphicsSVG extends PGraphicsJava2D {
g2 = new SVGGraphics2D(document);
((SVGGraphics2D) g2).setSVGCanvasSize(new Dimension(width, height));
//set the extension handler to allow linear and radial gradients to be exported as svg
GradientExtensionHandler gradH = new GradientExtensionHandler();
((SVGGraphics2D) g2).setExtensionHandler(gradH);
// Done with our work, let's check on defaults and the rest
//super.beginDraw();
// Can't call super.beginDraw() because it'll nuke our g2