Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds gradient export to SVG library #929

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions core/src/processing/awt/PShapeJava2D.java
Original file line number Diff line number Diff line change
@@ -33,6 +33,9 @@
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

import java.util.Arrays;
import java.awt.Color;

import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PShapeSVG;
@@ -96,16 +99,29 @@ public void setColor(String colorText, boolean isFill) {
*/


static class LinearGradientPaint implements Paint {
public static class LinearGradientPaint implements Paint {
float x1, y1, x2, y2;
float[] offset;
int[] color;
Color[] colors;
int count;
float opacity;
AffineTransform xform;

public static enum CycleMethod {
NO_CYCLE,
REFLECT,
REPEAT
}

public static enum ColorSpaceType {
SRGB,
LINEAR_RGB
}

public LinearGradientPaint(float x1, float y1, float x2, float y2,
float[] offset, int[] color, int count,
float opacity) {
float opacity, AffineTransform xform) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
@@ -114,6 +130,13 @@ public LinearGradientPaint(float x1, float y1, float x2, float y2,
this.color = color;
this.count = count;
this.opacity = opacity;
this.xform = xform;

//set an array of type Color
this.colors = new Color[this.color.length];
for (int i = 0; i < this.color.length; i++) {
this.colors[i] = new Color(this.color[i], true);
}
}

public PaintContext createContext(ColorModel cm,
@@ -125,6 +148,35 @@ public PaintContext createContext(ColorModel cm,
(float) t2.getX(), (float) t2.getY());
}

public Point2D getStartPoint() {
return new Point2D.Float(this.x1, this.y1);
}

public Point2D getEndPoint() {
return new Point2D.Float(this.x2, this.y2);
}

/* MultipleGradientPaint methods... */
public AffineTransform getTransform() {
return this.xform;
}

public ColorSpaceType getColorSpace() {
return ColorSpaceType.SRGB;
}

public CycleMethod getCycleMethod() {
return CycleMethod.NO_CYCLE;
}

public Color[] getColors() {
return Arrays.copyOf(this.colors, this.colors.length);
}

public float[] getFractions() {
return Arrays.copyOf(this.offset, this.offset.length);
}

public int getTransparency() {
return TRANSLUCENT; // why not.. rather than checking each color
}
@@ -221,23 +273,43 @@ public Raster getRaster(int x, int y, int w, int h) {
}


static class RadialGradientPaint implements Paint {
public static class RadialGradientPaint implements Paint {
float cx, cy, radius;
float[] offset;
int[] color;
Color[] colors;
int count;
float opacity;
AffineTransform xform;

public static enum CycleMethod {
NO_CYCLE,
REFLECT,
REPEAT
}

public static enum ColorSpaceType {
SRGB,
LINEAR_RGB
}

public RadialGradientPaint(float cx, float cy, float radius,
float[] offset, int[] color, int count,
float opacity) {
float opacity, AffineTransform xform) {
this.cx = cx;
this.cy = cy;
this.radius = radius;
this.offset = offset;
this.color = color;
this.count = count;
this.opacity = opacity;
this.xform = xform;

//set an array of type Color
this.colors = new Color[this.color.length];
for (int i = 0; i < this.color.length; i++) {
this.colors[i] = new Color(this.color[i], true);
}
}

public PaintContext createContext(ColorModel cm,
@@ -246,6 +318,41 @@ public PaintContext createContext(ColorModel cm,
return new RadialGradientContext();
}

public Point2D getCenterPoint() {
return new Point2D.Double(this.cx, this.cy);
}

//TODO: investigate how to change a focus point for 0% x of the gradient
//for now default to center x/y
public Point2D getFocusPoint() {
return new Point2D.Double(this.cx, this.cy);
}

public float getRadius() {
return this.radius;
}

/* MultipleGradientPaint methods... */
public AffineTransform getTransform() {
return this.xform;
}

public ColorSpaceType getColorSpace() {
return ColorSpaceType.SRGB;
}

public CycleMethod getCycleMethod() {
return CycleMethod.NO_CYCLE;
}

public Color[] getColors() {
return Arrays.copyOf(this.colors, this.colors.length);
}

public float[] getFractions() {
return Arrays.copyOf(this.offset, this.offset.length);
}

public int getTransparency() {
return TRANSLUCENT;
}
@@ -305,14 +412,14 @@ protected Paint calcGradientPaint(Gradient gradient) {
LinearGradient grad = (LinearGradient) gradient;
return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2,
grad.offset, grad.color, grad.count,
opacity);
opacity, grad.transform);

} else if (gradient instanceof RadialGradient) {
// System.out.println("creating radial gradient");
RadialGradient grad = (RadialGradient) gradient;
return new RadialGradientPaint(grad.cx, grad.cy, grad.r,
grad.offset, grad.color, grad.count,
opacity);
opacity, grad.transform);
}
return null;
}
5 changes: 3 additions & 2 deletions core/src/processing/core/PShapeSVG.java
Original file line number Diff line number Diff line change
@@ -1397,7 +1397,8 @@ void setFillOpacity(String opacityText) {
}


void setColor(String colorText, boolean isFill) {
//making this public allows us to set gradient fills on a PShape
public void setColor(String colorText, boolean isFill) {
colorText = colorText.trim();
int opacityMask = fillColor & 0xFF000000;
boolean visible = true;
@@ -1620,7 +1621,7 @@ static protected float parseFloatOrPercent(String text) {


static public class Gradient extends PShapeSVG {
AffineTransform transform;
public AffineTransform transform;

public float[] offset;
public int[] color;
209 changes: 209 additions & 0 deletions java/libraries/svg/src/processing/svg/GradientExtensionHandler.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions java/libraries/svg/src/processing/svg/PGraphicsSVG.java
Original file line number Diff line number Diff line change
@@ -87,6 +87,10 @@ public void beginDraw() {
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