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.
The library is available on maven central:
dependencies {
implementation("com.github.weisj:jsvg:1.6.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")
}
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.
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.
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);
By default 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 custom rendering hints are:
Key | Values | Default | Description |
---|---|---|---|
KEY_IMAGE_ANTIALIASING |
VALUE_IMAGE_ANTIALIAS_ON VALUE_IMAGE_ANTIALIAS_OFF |
Value of RenderingHints.KEY_ANTIALIASING |
Enables anti-aliasing for images |
KEY_SOFT_CLIPPING |
VALUE_SOFT_CLIPPING_ON VALUE_SOFT_CLIPPING_OFF |
VALUE_SOFT_CLIPPING_OFF |
Enables soft (anti-aliased) clipping for clipPath |
KEY_MASK_CLIP_RENDERING |
VALUE_MASK_CLIP_RENDERING_FAST VALUE_MASK_CLIP_RENDERING_ACCURACY VALUE_MASK_CLIP_RENDERING_DEFAULT |
VALUE_MASK_CLIP_RENDERING_DEFAULT = VALUE_MASK_CLIP_RENDERING_FAST |
Changes how masks and clip paths are rendered. Accurate rendering enforces the sub-image to which the mask/clip is applied to be rendered on its own isolated offscreen image |
KEY_CACHE_OFFSCREEN_IMAGE |
VALUE_USE_CACHE VALUE_NO_CACHE |
VALUE_USE_CACHE |
Whether to cache offscreen images. This can be useful for performance reasons, but can also lead to increased memory usage. |
All are exposed through the SVGRenderingHints
class.
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.
- ✅*: 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.
Element | Status |
---|---|
a | ✅ |
circle | ✅ |
clipPath | ✅ |
defs | ✅ |
ellipse | ✅ |
foreignObject | ❌ |
g | ✅ |
image | ✅ |
line | ✅ |
marker | ✅ |
mask | ✅ |
path | ✅ |
polygon | ✅ |
polyline | ✅ |
rect | ✅ |
svg | ✅ |
symbol | ✅ |
use | ✅ |
view | ✅* |
Element | Status |
---|---|
linearGradient | ✅ |
🧪meshgradient | ✅ |
🧪meshrow | ✅ |
🧪meshpatch | ✅ |
pattern | ✅ |
radialGradient | ✅ |
solidColor | ✅ |
stop | ✅ |
Element | Status |
---|---|
text | ✅ |
textPath | ✅ |
❌ | |
tspan | ✅ |
Element | Status |
---|---|
animate | ❌ |
❌ | |
animateMotion | ❌ |
animateTransform | ❌ |
mpath | ❌ |
set | ❌ |
switch | ❌ |
Element | Status |
---|---|
feBlend | ✅ |
feColorMatrix | ✅ |
feComponentTransfer | ✅ |
feComposite | ✅ |
feConvolveMatrix | ❌ |
feDiffuseLighting | ✅ |
feDisplacementMap | ✅ |
feDistantLight | ❌ |
feDropShadow | ✅ |
feFlood | ✅ |
feFuncA | ✅ |
feFuncB | ✅ |
feFuncG | ✅ |
feFuncR | ✅ |
feGaussianBlur | ✅ |
feImage | ❌ |
feMerge | ✅ |
feMergeNode | ✅ |
feMorphology | ❌ |
feOffset | ✅ |
fePointLight | ❌ |
feSpecularLighting | ❌ |
feSpotLight | ❌ |
feTile | ❌ |
feTurbulence | ✅ |
filter | ☑️ |
Element | Status |
---|---|
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ | |
❌ |
Element | Status |
---|---|
desc | (:white_check_mark:) |
title | (:white_check_mark:) |
metadata | (:white_check_mark:) |
color-profile | ❌ |
❌ | |
script | ❌ |
style | ☑️ |
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 DomProcessor
for 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);