• Stars
    star
    126
  • Rank 283,725 (Top 6 %)
  • Language
    Java
  • License
    MIT License
  • Created about 3 years ago
  • Updated about 1 month ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Java SVG renderer

Quality Gate Status Code Style CI Maven Central

JSVG - A Java SVG implementation

The SVG logo rendered by JSVG
The SVG logo rendered using JSVG

JSVG is an SVG user agent using AWT graphics. Its aim is to provide a small and fast implementation. This library is under active development and doesn't yet support all features of the SVG specification, some of which it decidedly won't support at all. This implementation only tries to be a static user agent meaning it won't support any scripting languages or interaction. Animations aren't currently implemented but are planned to be supported.

This library aims to be as lightweight as possible. Generally JSVG uses ~50% less memory than svgSalamander and ~98% less than Batik.

JSVG is used by the Jetbrains IDEA IDE suite for rendering their interface icons.

How to use

The library is available on maven central:

dependencies {
    implementation("com.github.weisj:jsvg:1.4.0")
}

Also, nightly snapshot builds will be released to maven:

repositories {
    maven {
        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
    }
}

// Optional:
configurations.all {
    resolutionStrategy.cacheChangingModulesFor(0, "seconds")
}

dependencies {
    implementation("com.github.weisj:jsvg:latest.integration")
}

Loading

To load an svg icon you can use the SVGLoader class. It will produce an SVGDocument

SVGLoader loader = new SVGLoader();
URL svgUrl = MyClass.class.getResource("mySvgFile.svg");
SVGDocument svgDocument = loader.load(svgUrl);

Note that SVGLoader is not guaranteed to be thread safe hence shouldn't be used across multiple threads.

Rendering

An SVGDocument can be rendered to any Graphics2D object you like e.g. a BufferedImage

FloatSize size = svgDocument.size();
BufferedImage image = new BufferedImage((int) size.width,(int) size.height);
Graphics2D g = image.createGraphics();
svgDocument.render(null,g);
g.dispose();

or a swing component

class MyComponent extends JComponent {
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        svgDocument.render(this, (Graphics2D) g, new ViewBox(0, 0, getWidth(), getHeight()));
    }
}

For more in-depth examples see #Usage examples below.

Rendering Quality

The rendering quality can be adjusted by setting the RenderingHints of the Graphics2D object. The following properties are recommended:

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

If either of these values are not set or have their respective default values (VALUE_ANTIALIAS_DEFAULT and VALUE_STROKE_DEFAULT) JSVG will automatically set them to the recommended values above.

JSVG also supports custom SVG specific rendering hints. These can be set using the SVGRenderingHints class. For example:

// Will use the value of RenderingHints.KEY_ANTIALIASING by default
g.setRenderingHint(SVGRenderingHints.KEY_IMAGE_ANTIALIASING, SVGRenderingHints.VALUE_IMAGE_ANTIALIASING_ON);

Additionally clipping with a <clipPath> element does not use soft-clipping (i.e. anti-aliasing along the edges of the clip shape). This can be enabled by setting

g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING, SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);

In the future this will get stabilized and be enabled by default.

Supported features

For supported elements most of the attributes which apply to them are implemented.

  • βœ…: The element is supported. Note that this doesn't mean that every attribute is supported.
  • (:white_check_mark:): The element is supported, but won't have any effect (e.g. it's currently not possible to query the content of a <desc> element)
  • β˜‘οΈ: The element is partially implemented and might not support most basic features of the element.
  • ❌: The element is currently not supported
  • ⚠️: The element is deprecated in the spec and has a low priority of getting implemented.
  • πŸ§ͺ: The element is an experimental part of the svg 2.* spec. It may not fully behave as expected.

Shape and container elements

Element Status
a βœ…
circle βœ…
clipPath βœ…
defs βœ…
ellipse βœ…
foreignObject ❌
g βœ…
image βœ…
line βœ…
marker βœ…
mask βœ…
path βœ…
polygon βœ…
polyline βœ…
rect βœ…
svg βœ…
symbol βœ…
use βœ…
view (:white_check_mark:)

Paint server elements

Element Status
linearGradient βœ…
πŸ§ͺmeshgradient βœ…
πŸ§ͺmeshrow βœ…
πŸ§ͺmeshpatch βœ…
pattern βœ…
radialGradient βœ…
solidColor βœ…
stop βœ…

Text elements

Element Status
text βœ…
textPath βœ…
⚠️tref ❌
tspan βœ…

Animation elements

Element Status
animate ❌
⚠️animateColor ❌
animateMotion ❌
animateTransform ❌
mpath ❌
set ❌
switch ❌

Filter elements

Element Status
feBlend βœ…
feColorMatrix βœ…
feComponentTransfer ❌
feComposite βœ…
feConvolveMatrix ❌
feDiffuseLighting βœ…
feDisplacementMap βœ…
feDistantLight ❌
feFlood βœ…
feFuncA ❌
feFuncB ❌
feFuncG ❌
feFuncR ❌
feGaussianBlur βœ…
feImage ❌
feMerge βœ…
feMergeNode βœ…
feMorphology ❌
feOffset βœ…
fePointLight ❌
feSpecularLighting ❌
feSpotLight ❌
feTile ❌
feTurbulence βœ…
filter β˜‘οΈ

Font elements

Element Status
⚠️altGlyph ❌
⚠️altGlyphDef ❌
⚠️altGlyphItem ❌
⚠️font ❌
⚠️font-face ❌
⚠️font-face-format ❌
⚠️font-face-name ❌
⚠️font-face-src ❌
⚠️font-face-uri ❌
⚠️glyph ❌
⚠️glyphRef ❌
⚠️hkern ❌
⚠️missing-glyph ❌
⚠️vkern ❌

Other elements

Element Status
desc (:white_check_mark:)
title (:white_check_mark:)
metadata (:white_check_mark:)
color-profile ❌
⚠️cursor ❌
script ❌
style β˜‘οΈ

Usage examples

To render an SVG to a swing component you can start from the following example:

import javax.swing.*;
import java.awt.*;
import java.net.URL;

import com.github.weisj.jsvg.*;
import com.github.weisj.jsvg.attributes.*;
import com.github.weisj.jsvg.parser.*;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;

public class RenderExample {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            SVGLoader loader = new SVGLoader();

            URL svgUrl = RenderExample.class.getResource("path/to/image.svg");
            SVGDocument document = loader.load(Objects.requireNonNull(svgUrl, "SVG file not found"));

            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setPreferredSize(new Dimension(400, 400));
            frame.setContentPane(new SVGPanel(Objects.requireNonNull(document)));
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    static class SVGPanel extends JPanel {
        private @NotNull final SVGDocument document;

        SVGPanel(@NotNull SVGDocument document) {
            this.document = document;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            ((Graphics2D) g).setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            ((Graphics2D) g).setRenderingHint(
                RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_PURE);

            document.render(this, (Graphics2D) g, new ViewBox(0, 0, getWidth(), getHeight()));
        }
    }
}

You can even change the color of svg elements by using a suitable DomProcessor together with a custom implementation of SVGPaint. Lets take the following SVG as an example:

<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
    <rect x="0" y="0" width="100%" height="40%" id="myRect"></rect>
    <rect x="0" y="60" width="100%" height="40%"></rect>
</svg>

We want to change the color if the first rectangle at runtime. We start by loading the SVG using a custom ParserProvider which returns a DomProcessorfor the pre-processing step. The DomProcessor will allow us to change attributes of the SVG elements before they are fully parsed.

CustomColorsProcessor processor = new CustomColorsProcessor(List.of("myRect"));
document = loader.load(svgUrl, new DefaultParserProvider() {
    @Override
    public DomProcessor createPreProcessor() {
        return processor;
    }
});

The heavy lifting is done by the CustomColorsProcessor class which looks like this:

class CustomColorsProcessor implements DomProcessor {

    private final Map<String, DynamicAWTSvgPaint> customColors = new HashMap<>();

    public CustomColorsProcessor(@NotNull List<String> elementIds) {
        for (String elementId : elementIds) {
            customColors.put(elementId, new DynamicAWTSvgPaint(Color.BLACK));
        }
    }

    @Nullable DynamicAWTSvgPaint customColorForId(@NotNull String id) {
        return customColors.get(id);
    }

    @Override
    public void process(@NotNull ParsedElement root) {
        processImpl(root);
        root.children().forEach(this::process);
    }

    private void processImpl(ParsedElement element) {
        // Obtain the id of the element
        // Note: There that Element also has a node() method to obtain the SVGNode. However during the pre-processing
        // phase the SVGNode is not yet fully parsed and doesn't contain any non-defaulted information.
        String nodeId = element.id();

        // Check if this element is one of the elements we want to change the color of
        if (customColors.containsKey(nodeId)) {
            // The attribute node contains all the attributes of the element specified in the markup
            // Even those which aren't valid for the element
            AttributeNode attributeNode = element.attributeNode();
            DynamicAWTSvgPaint dynamicColor = customColors.get(nodeId);

            // This assumed that the fill attribute is a color and not a gradient or pattern.
            Color color = attributeNode.getColor("fill");

            dynamicColor.setColor(color);

            // This can be anything as long as it's unique
            String uniqueIdForDynamicColor = UUID.randomUUID().toString();

            // Register the dynamic color as a custom element
            element.registerNamedElement(uniqueIdForDynamicColor, dynamicColor);

            // Refer to the custom element as the fill attribute
            attributeNode.attributes().put("fill", uniqueIdForDynamicColor);

            // Note: This class can easily be adapted to also support changing the stroke color.
            // With a bit more work it could also support changing the color of gradients and patterns.
        }
    }
}

class DynamicAWTSvgPaint implements SimplePaintSVGPaint {

    private @NotNull Color color;

    DynamicAWTSvgPaint(@NotNull Color color) {
        this.color = color;
    }

    public void setColor(@NotNull Color color) {
        this.color = color;
    }

    public @NotNull Color color() {
        return color;
    }

    @Override
    public @NotNull Paint paint() {
        return color;
    }
}

Now we simply have to obtain the DynamicAWTSvgPaint instance for the element we want to change the color of and hook it up in our UI:

DynamicAWTSvgPaint dynamicColor = processor.customColorForId("myRect");

SVGPanel panel = new SVGPanel(document);
JButton button = new JButton("Change color");
button.addActionListener(e -> {
    Color newColor = JColorChooser.showDialog(panel, "Choose a color", dynamicColor.color());
    if (newColor != null) {
        dynamicColor.setColor(newColor);
        // Make sure to repaint the panel to see the changes
        panel.repaint();
    }
});
JPanel content = new JPanel(new BorderLayout());
content.add(panel, BorderLayout.CENTER);
content.add(button, BorderLayout.SOUTH);
frame.setContentPane(content);