You may have noticed that the Playfair project started with the JFreeSVG library and has now switched to the EchoSVG library. Both libraries have their advantages and disadvantages, but JFreeSVG’s GPL 3.0 license in particular makes working with it difficult in many projects, often even impossible. With the next version 0.7.0, Playfair will switch to the Apache 2.0 license. This would be a little too little content for a blog post, so let’s take a look at one of EchoSVG’s weaknesses.
The cover image used here shows a pie chart with a gloss effect. This gloss effect is created by a radial gradient. For gradients, Java AWT provides the GradientPaint, LinearGradientPaint and RadialGradientPaint classes. Unfortunately, EchoSVG only supports only the GradientPaint class and not the LinearGradientPaint and RadialGradientPaint classes. As a result, our pie chart is colored white throughout.
Fortunately, the library offers an interface for extensions that can be used to solve this problem.
public class RadialGradientExtensionHandler extends DefaultExtensionHandler {
public RadialGradientExtensionHandler(SVGGeneratorContext generatorContext) {
}
@Override
public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext svgGeneratorContext) {
if (paint instanceof RadialGradientPaint radialGradientPaint) {
return new SVGRadialGradient(svgGeneratorContext).toSVG(radialGradientPaint);
}
return null;
}
}
The DefaultExtensionHandler provides the handlePaint method, which can be used to support custom Paint implementations. Although the RadialGradientPaint class is not a custom paint implementation, we will not let that deter us. The procedure within the method corresponds to the implementation of EchoSVG for linear gradients. We create our own AbstractSVGConverter implementation SVGRadialGradient, which generates the necessary SVG for the gradient.
public class SVGRadialGradient extends AbstractSVGConverter {
private final Map<RadialGradientPaint, SVGDescriptor> descMap = new HashMap<>();
public SVGRadialGradient(SVGGeneratorContext generatorContext) {
super(generatorContext);
}
@Override
public SVGDescriptor toSVG(GraphicContext gc) {
return toSVG((RadialGradientPaint) gc.getPaint());
}
public SVGPaintDescriptor toSVG(RadialGradientPaint gradient) {
// Later...
}
}
The ExtensionHandler calls the class using the second toSVG method and creates a new SVGPaintDescriptor.
private SVGPaintDescriptor toSVG(RadialGradientPaint gradient) {
SVGIDGenerator idGenerator = getGeneratorContext().getIDGenerator();
String id = idGenerator.generateID(ID_PREFIX_RADIAL_GRADIENT);
Document domFactory = getGeneratorContext().getDOMFactory();
Element gradientDef = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_RADIAL_GRADIENT_TAG);
gradientDef.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE);
Point2D center = gradient.getCenterPoint();
Point2D focus = gradient.getFocusPoint();
float radius = gradient.getRadius();
gradientDef.setAttribute(SVG_ID_ATTRIBUTE, id);
gradientDef.setAttribute(SVG_CX_ATTRIBUTE, doubleString(center.getX()));
gradientDef.setAttribute(SVG_CY_ATTRIBUTE, doubleString(center.getY()));
gradientDef.setAttribute(SVG_FX_ATTRIBUTE, doubleString(focus.getX()));
gradientDef.setAttribute(SVG_FY_ATTRIBUTE, doubleString(focus.getY()));
gradientDef.setAttribute(SVG_R_ATTRIBUTE, doubleString(radius));
gradientDef.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, switch (gradient.getCycleMethod()) {
case NO_CYCLE -> SVG_PAD_VALUE;
case REFLECT -> SVG_REFLECT_VALUE;
case REPEAT -> SVG_REPEAT_VALUE;
});
Color[] gradientColors = gradient.getColors();
float[] gradientFractions = gradient.getFractions();
for (int i = 0; i < gradientColors.length; i++) {
Element gradientStop = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
gradientStop.setAttribute(SVG_OFFSET_ATTRIBUTE, doubleString(gradientFractions[i] * 100) + "%");
SVGPaintDescriptor colorDesc = SVGColor.toSVG(gradientColors[i], getGeneratorContext());
gradientStop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, colorDesc.getPaintValue());
gradientStop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, colorDesc.getOpacityValue());
gradientDef.appendChild(gradientStop);
}
return new SVGPaintDescriptor(URL_PREFIX + SIGN_POUND + id + URL_SUFFIX, SVG_OPAQUE_VALUE, gradientDef);
}
This method creates a radial gradient in SVG based on the values of the GradientRadialPaint instance. Fortunately, both definitions are semantically equivalent, so only the values need to be converted to a different structure. It is a little annoying at this point that you actually have to deal with XML processing via the DOM API. This is one of the few moments when even experienced developers are happy to have the help of an AI.
At this point the support for radial gradients in EchoSVG is already implemented, and fortunately, the result looks exactly the same as with the library used previously. The next step would now be to contribute the implementation to the EchoSVG team. This implementation was a good exercise for another addition to Plaifair and EchoSVG: texture support for SVG textures.