weisJ/jsvg

Simple Swing Example

Closed this issue · 27 comments

Hello,

is there a simple example that shows how to use the lib in swing apps?
Is it possible to lookup svg elements and e.g. access them and change the color in the view?

BR Peter

is there a simple example that shows how to use the lib in swing apps?

Documentation is still work in progress, so this example doesn't yet exist. Nonetheless here is how it could look like :)

For swing apps the easiest version would be something like this:

public class SVGIcon implements Icon {
    private final SVGDocument document;
    private final int width;
    private final int height;

    public SVGIcon(SVGDocument document, int width, int height) {
        this.document = document;
        this.width = width;
        this.height = height;
    }


    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        document.render((JComponent) c, (Graphics2D) g, new ViewBox(x, y, width, height));
    }

    @Override
    public int getIconWidth() {
        return width;
    }

    @Override
    public int getIconHeight() {
        return height;
    }
}

This is a very naive implementation which will work fine if you just want to display a single file in a JLabel. However if the goal is to use svg icons throughout the application then a bit more care has to be taken with implementing an svg icon class as above (e.g. caching the icon to a BufferedImage, multi resolution support for hide etc.). See https://github.com/weisJ/darklaf/blob/master/property-loader/src/main/java/com/github/weisj/darklaf/properties/icons/DarkSVGIcon.java for an example

Is it possible to lookup svg elements and e.g. access them and change the color in the view?

I intentionally don't want to expose the svg DOM during runtime for inspection and modification mainly due to the following two reasons:

  • I don't want to guarantee any ABI stability for how the DOM is represented at runtime. Implementing features may require major restructuring and I don't want to have breaking changes in releases.
  • By not allowing any modifications to the DOM I can make some additional assumptions which allow for more optimisations.

Changing the color however is something that I kept in mind (it is a use case I needed myself). See #52 for a short exposition on how to achieve recovering. If you have any further questions feel free to ask :)

Hello, I wrote a little program to play with jsvg. For some reason it does not render nicely. The SVG is attached and looks great. Do you have an idea what could be wrong?

Capture

SVG Image:
graph

The code looks as follows:

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
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 com.github.weisj.jsvg.geometry.size.FloatSize;
import java.awt.geom.AffineTransform;

public class SwingSandbox {

    static SVGDocument svgDocument;
    static JFrame frame;
    public static void main(String[] args) throws IOException {

        SwingUtilities.invokeLater(() -> {
            try{
                createAndShowGUI();
            }catch(IOException e){
                e.printStackTrace();
            }
        });
    }


    private static void createAndShowGUI() throws IOException{
        frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(400,400));

        SVGLoader loader = new SVGLoader();
        //URL svgUrl = SwingSandbox.class.getResource("batik70.svg");
        URL svgUrl = SwingSandbox.class.getResource("graph.svg");
        svgDocument = loader.load(svgUrl);
        FloatSize size = svgDocument.size();
        BufferedImage image = new BufferedImage((int) size.width,(int) size.height, BufferedImage.TYPE_INT_ARGB);

        JScrollPane scrollPane = new JScrollPane(new ImagePanel(image));
        frame.add("Center", scrollPane);

        frame.pack();
        frame.setLocationRelativeTo(null);

        frame.setVisible(true);

    }

    static class ImagePanel extends JPanel{
        private BufferedImage image;

        public ImagePanel(BufferedImage image){
            this.image = image;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            svgDocument.render(this, (Graphics2D) g, new ViewBox(0, 0, svgDocument.size().width , svgDocument.size().height));
        }

        @Override
        public Dimension getPreferredSize(){
            return new Dimension(getWidth(), getHeight());
        }
    }
}

You have to enable antialiasing

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

With the next version jsvg does it for you unless the value for the antialiasing hint is VALUE_ANTIALIAS_OFF.

Hallo,

yes, that solved the issue. Attached is my complete example. Feel free to share it it as "getting started" for other users.

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.awt.event.*;
import com.github.weisj.jsvg.*;
import com.github.weisj.jsvg.attributes.*;
import com.github.weisj.jsvg.parser.*;
import com.github.weisj.jsvg.geometry.size.FloatSize;
import java.awt.geom.AffineTransform;

public class SwingSandbox {

    static SVGDocument svgDocument;
    static JFrame frame;
    static BufferedImage image;
    static double scaleFactor = 1.0;
    static JScrollPane scrollPane;
    static JPanel myPanel;
    static ViewBox viewBox;
    static JPanel buttonPanel;

    public static void main(String[] args) throws IOException {

        SwingUtilities.invokeLater(() -> {
            try{
                createAndShowGUI();
            }catch(IOException e){
                e.printStackTrace();
            }
        });
    }

    private static void createAndShowGUI() throws IOException{
        frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(400,400));

        SVGLoader loader = new SVGLoader();

        URL svgUrl = SwingSandbox.class.getResource("graph.svg");
        svgDocument = loader.load(svgUrl);
        FloatSize size = svgDocument.size();
        image = new BufferedImage((int) size.width,(int) size.height, BufferedImage.TYPE_INT_ARGB);

        scrollPane = new JScrollPane(new ImagePanel(image),ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        buttonPanel = new JPanel();

        JButton button1 = new JButton("+");
        JButton button2 = new JButton("-");
                
        buttonPanel.add(button1);
        buttonPanel.add(button2);        

        frame.add(scrollPane);
        frame.add(buttonPanel, BorderLayout.NORTH);

        button1.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                scaleFactor=scaleFactor*1.1;
                rescaleAndRepaint();
            }
        });

        button2.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                scaleFactor=scaleFactor*0.9;
                rescaleAndRepaint();
            }
        });

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    static void rescaleAndRepaint(){
        // Update the scrollbars
        int scaledWidth = (int) (image.getWidth() * scaleFactor);
        int scaledHeight = (int) (image.getHeight() * scaleFactor);   

        // Set the maximum values for the scrollbars
        scrollPane.getHorizontalScrollBar().setMaximum(Math.max(0, scaledWidth - scrollPane.getWidth()));
        scrollPane.getVerticalScrollBar().setMaximum(Math.max(0, scaledHeight - scrollPane.getHeight()));

        // Explicitly revalidate and repaint the JScrollPane
        scrollPane.revalidate();
        scrollPane.repaint();
    }

    static class ImagePanel extends JPanel{
        private BufferedImage image;

        public ImagePanel(BufferedImage image){
            super();
            this.image = image;
        }

        @Override
        public Dimension getPreferredSize() {
            int scaledWidth = (int) (image.getWidth() * scaleFactor);
            int scaledHeight = (int) (image.getHeight() * scaleFactor);
            return new Dimension(scaledWidth, scaledHeight);
        }
        
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            AffineTransform at = new AffineTransform();
            at.setToScale(scaleFactor, scaleFactor);
            ((Graphics2D)g).setTransform(at);

            int scaledWidth = (int) (image.getWidth() * scaleFactor);
            int scaledHeight = (int) (image.getHeight() * scaleFactor);

            // adjust viewpos acc. scaled size and position
            Point viewPos = scrollPane.getViewport().getViewPosition();
            viewBox = new ViewBox(-1*viewPos.x, -1*viewPos.y, scaledWidth, scaledHeight);
    
            svgDocument.render(this, (Graphics2D) g, viewBox);
            scrollPane.revalidate();
            scrollPane.repaint();
        }
    }
}

After having a playground I looked into your suggestion on how to dynamically change the color of any element.

I could follow your suggestion to create a CustomColorsProcessor.

What I could not figure out are two things. Lets take this element:

<g id="node1" class="node">
<title>React</title>
<ellipse fill="none" stroke="black" cx="184" cy="-206.5" rx="32.6" ry="18"/>
<text text-anchor="middle" x="184" y="-202.97" font-family="Verdana" font-size="12.00">React</text>
</g>
  1. I could "find" the attributes of the ellipse as well as the id and class.
    But I could not find the title. How it is represented in the element or node?

  2. The method processImpl seems to be called during initial parsing of the SVG file. How can I then dynamically change the color e.g. because someone changed it in the program that displays the SVG.

  1. I could "find" the attributes of the ellipse as well as the id and class.
    But I could not find the title. How it is represented in the element or node?

The title attribute is not present at runtime as it isn't a rendered element. If you need the information you have to reify it manually using a DomProcessor.

  1. The method processImpl seems to be called during initial parsing of the SVG file. How can I then dynamically change the color e.g. because someone changed it in the program that displays the SVG.

The API isn't very fleshed out at the moment (it is very much lacking any convenience features). The following should work for you:

class DynamicAWTSvgPaint implements SimplePaintSVGPaint {

    private @NotNull Color color;

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

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

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

class CustomColorsProcessor implements DomProcessor {

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

    private void processImpl(ParsedElement element) {
        SVGNode node = element.node();
        if ("myNode".equals(node.id())) { // Check if this is the node you are interested in
            AttributeNode attributeNode = element.attributeNode();
            Color color = attributeNode.getColor("fill"); // Assumes the color isn't a gradient
            
            // You'll want to store this somewhere to change the color later on
            // If you do so you are responsible to repaint the component the SVG is displayed in
            DynamicAWTSvgPaint dynamicColor = new DynamicAWTSvgPaint(color);
            
            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);
        }
    }
}

Can you show the final code of how you were able to change the color of an SVG object? I am loading an SVG using the SVGLoader and SVGDocument.

Here is an (almost) complete example with an example svg:

import com.github.weisj.jsvg.attributes.paint.SimplePaintSVGPaint;
import com.github.weisj.jsvg.nodes.SVGNode;
import com.github.weisj.jsvg.parser.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

class ColorChange {

    /*
    <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="id1"/>
        <rect x="0" y="60" width="100%" height="40%" id="id2"/>
    </svg>
     */
    public static void main(String[] args) {
        SVGLoader loader = new SVGLoader();
        URL svgUrl = ColorChange.class.getResource("path/to/svg.svg");
        List<String> elementIds = List.of("id1", "id2");
        CustomColorsProcessor processor = new CustomColorsProcessor(elementIds);
        SVGDocument document = loader.load(svgUrl, new DefaultParserProvider() {
            @Override
            public DomProcessor createPostProcessor() {
                return processor;
            }
        });

        processor.customColorForId("id1").setColor(Color.RED);

        // Do something with docuemnt
    }
}

class DynamicAWTSvgPaint implements SimplePaintSVGPaint {

    private @NotNull Color color;

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

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

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

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) {
        SVGNode node = element.node();
        String nodeId = node.id();
        if (customColors.containsKey(nodeId)) {
            DynamicAWTSvgPaint dynamicColor = customColors.get(nodeId);

            AttributeNode attributeNode = element.attributeNode();
            Color color = attributeNode.getColor("fill"); // Assumes the color isn't a gradient

            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);
        }
    }
}

Hello @weisJ I appreciate the quick comment. For some reason even after copying the code that you posted above I am still not getting the desired result. One of these rectangles should be blue and the other one should be red. Any ideas?
image

Can you provide your code? This way I’ll have a better idea of what might need more explanation :)

Hello @weisJ Sure thing! Here is my swing component

package Components;

import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.attributes.ViewBox;
import com.github.weisj.jsvg.nodes.SVGNode;
import com.github.weisj.jsvg.parser.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.github.weisj.jsvg.attributes.paint.SimplePaintSVGPaint;

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

public class CustomIcon extends JComponent {
    private SVGDocument svgDocument = null;
    private double width = 0.0;
    private double height = 0.0;

    public CustomIcon() {
        SVGLoader loader = new SVGLoader();
        /*
        <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="id1"/>
            <rect x="0" y="60" width="100%" height="40%" id="id2"/>
        </svg>
         */
        URL svgUrl = this.getClass().getResource("../Assets/Test.svg");
        List<String> elementIds = List.of("id1", "id2");
        if (svgUrl != null) {
            CustomColorsProcessor processor = new CustomColorsProcessor(elementIds);
            svgDocument = loader.load(svgUrl, new DefaultParserProvider() {
                @Override
                public DomProcessor createPostProcessor() {
                    return processor;
                }
            });
            processor.customColorForId("id1").setColor(Color.BLUE);
            processor.customColorForId("id2").setColor(Color.RED);
        }
        if (svgDocument != null) {
            this.width = svgDocument.size().getWidth();
            this.height = svgDocument.size().getHeight();
        }
        this.setSize((int) this.width, (int) this.height);
        this.setLocation(0, 0);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        svgDocument.render(this, (Graphics2D) g, new ViewBox(0, 0, (float) this.width, (float) this.height));
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(getWidth(), getHeight());
    }
}

class DynamicAWTSvgPaint implements SimplePaintSVGPaint {

    private @NotNull Color color;

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

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

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

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) {
        SVGNode node = element.node();
        String nodeId = node.id();
        if (customColors.containsKey(nodeId)) {
            DynamicAWTSvgPaint dynamicColor = customColors.get(nodeId);

            AttributeNode attributeNode = element.attributeNode();
            Color color = attributeNode.getColor("fill"); // Assumes the color isn't a gradient

            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);
        }
    }
}

Oh I see my mistake. Simply replace createPostProcessor with createPreProcessor and it should work.

@weisJ For some reason that did not seem to change the color. Is there something else I am missing based on my code above?

String nodeId = element.id(); will work (together with using the pre-processor). During the pre-processing phase the node itself doesn't contain any information. Sorry for the confusion.

@weisJ That worked like a charm. I appreciate all the help!
image

I have added the code examples for this in the README.

I am still having trouble getting my SVG to change color while wrapped in a JComponent Class. I would like the ability to create a method in my new class that extends JComponent to do something like setColor and then have the SVG re-render and the color update but for some reason I cannot get that to happen no matter what I do. Any ideas?

Did you have a look at the example i added in the readme? It explains how to change the color during runtime. Essentially you add a DynamicAWTSvgPaint.setColor method to change the color. Once you changed the color you have to make sure to repaint the component you have painted to.

@weisJ Thanks for your help yes that did the trick! I was just getting lost in the code for sure. Very easy fix indeed

Is there a way to change the background color and stroke color at the same time? Trying to figure out how to accomplish this with possibly a different processor.

In the sense that fill and stroke always have the same color?
Set the same id for both fill and stroke.

attributeNode.attributes().put("fill", uniqueIdForDynamicColor);
attributeNode.attributes().put("stroke", uniqueIdForDynamicColor);

@weisJ In the sense that I want a different color depending. I am filling an icon but when the Icon is black I do not want the stroke to also be black as the stroke would not be visible. Apologies for lack of explanation.

You just have to register a different color for fill and store it somewhere. Then just update both the DynamicAWTSvgPaint for the stroke and fill at the same time according to your requirements. There is no method to know whether a SVGPaint is used for painting or strokes or fill when paint() is called.

@weisJ This is the code I have added but my icon's stroke is the same color as the fill no matter what the color of the background is
image

You need to create a separate DynamicAWTSvgPaint for the stroke.

That did the trick! Very much appreciated. I am happy to edit the documentation and add this as an example for future people to the project.

Please feel free to submit a PR improving the documentation.