4

I am creating an SVG file using Batik, which I would like to later open in a browser and manipulate using JavaScript. I am unable to figure out how to add ID attributes to the g elements created.

svgGenerator = new SVGGraphics2D(document);
svgGenerator.setPaint(Color.black);

for (int i = 0; i < 10; i++) {
...
svgGenerator.fill(new Rectangle(p.x, p.y, 2, 2));
}

svgGenerator.setPaint(new Color(0, 0, 0, 0.2f));
for (int i = 0; i < 20; i++) {
CubicCurve2D curve = new CubicCurve2D.Float();
...
curve.setCurve(a.x, a.y, c.x, c.y, c.x, c.y, b.x, b.y);
svgGenerator.draw(curve);
}

...

OutputStream outputStream = new FileOutputStream(svgFileName);
Writer out = new OutputStreamWriter(outputStream, "UTF-8");
svgGenerator.stream(out, useCSS);
out.flush();
out.close();

gives me

<defs id="genericDefs"/>
<rect width="100%" height="100%" fill="white"/>
<g>
<g>
<rect x="699" width="2" height="2" y="350" style="stroke:none;"/>
<rect 
...
</g>
<g style="fill:rgb(0,0,0); fill-opacity:0.2; stroke-opacity:0.2; stroke:rgb(0,0,0);">
<path style="fill:none;" d="M699 350 C567 178 567 178 436 11"/>
<path 
...
</g>
</g>

I would like to give separate ID tags to the three "g" elements created by batik. How should I modify my code in order to achieve this?

edit: I got it to work by traversing the DOM after all elements are created, using the hint given by @robert. But this feels like an "ugly" solution, is there something more elegant?

Element root = svgGenerator.getRoot();
Element topG = (Element) root.getChildNodes().item(2);
topG.setAttribute("id", "allSVG");
Element rectangleG = (Element) topG.getChildNodes().item(0);
rectangleG.setAttribute("id", "rectangles");
Element pathG = (Element) topG.getChildNodes().item(1);
pathG.setAttribute("id", "paths");
...
svgGenerator.stream(root, out, useCSS, false);
Diego
  • 85
  • 7
  • 1
    call svgGenerator.getTopLevelGroup().setAttribute("id", "whatever") at various points. – Robert Longson May 28 '18 at 05:41
  • Thanks for your response! I tried your suggestion, and find that if I insert the line you wrote at the beginning (immediately after defining svgGenerator), the output does not change. If I insert it at the end (before OutputStream) then the two subgroups (along with the rect and path elements) vanish. If I insert it immediately after the first loop, the path elements vanish along with its group. In any case, no "id" is created. Am I missing something? – Diego May 28 '18 at 08:56
  • `is there something more elegant` not likely. Your only real option here is to add the attribute yourself. And that's what you did. You can _possibly_ clean the code up by using a selector. – Richard Barker Apr 20 '21 at 05:14
  • One possible option can be transforming the generated SVG file with XSLT: certainly you will need to indicate the nodes you want to process, in your case probably by the exact position, but it will give you a uniform way of processing the svg. This approach had been already described here in SO in several questions: please, see [this](https://stackoverflow.com/questions/56733293/how-to-modify-a-svg-attribute-using-xslt) or [this other](https://stackoverflow.com/questions/13035011/transforming-svg-with-xslt-sierpinski-carpet), for instance – jccampanero Apr 20 '21 at 22:33
  • @jccampanero that's not an answer. The question is about SVG written using SVGGraphics2D and how to label different elements in the graph. In this case the elements happen to be delineated by the change of colour. – Antti Haapala -- Слава Україні Apr 21 '21 at 18:00
  • Yes @AnttiHaapala, sorry. I realize that the question is related with Batik and `SVGGraphics2D`, but Diego seems to look for a less coupled solution, this is why I tried to provide a probably more versatile alternative, but I agree with you. Please, let me know if you want me to remove the previous comment, it will be ok Antti. – jccampanero Apr 21 '21 at 18:18
  • @jccampanero even with Diego, I'd guess he'd just seen that there is a `g` element coinciding changing with paint and that adding the class to the g element would help in his case. In my case it is thousands of elements and I would like to label certain lines that I draw with a certain class. – Antti Haapala -- Слава Україні Apr 21 '21 at 20:31
  • Yes @AnttiHaapala, I understand. From a Batik perspective, perhaps you can try tweaking either [`DOMTreeManager`](https://xmlgraphics.apache.org/batik/javadoc/org/apache/batik/svggen/DOMTreeManager.html) or [`DOMGroupManager`](https://xmlgraphics.apache.org/batik/javadoc/org/apache/batik/svggen/DOMGroupManager.html), although probably the best option will be to create an intermediate object model that sits between your business model and Batik and, in general, `Graphics2D`. This model will have the responsibility to define the way in which every business object should be represented, (cont) – jccampanero Apr 22 '21 at 09:12
  • with the necessary custom attributes, either IDs or classes, required (i.e., you will still use the low level API based on `setAttribute` but in a more structured way. This approach is followed in several libraries. Consider, for example, the use case of the [`JChemPaint`](https://github.com/JChemPaint/jchempaint/tree/32864f345046ad25ceb3780b3dced0381790a355) library which under the hood uses [cdk](https://github.com/cdk/cdk), or the `cdk` library itself. It provides a rich object model that, depending on the concrete entity, is represented in some or other way. (cont) – jccampanero Apr 22 '21 at 09:17
  • In my case I've passed around Graphics2D, and what I would like is that I could just say "from now on all draw primitives should have class X or be contained in group with class X". – Antti Haapala -- Слава Україні Apr 22 '21 at 09:18
  • See, for example, [here](https://github.com/cdk/cdk/blob/master/display/renderawt/src/main/java/org/openscience/cdk/renderer/visitor/AWTDrawVisitor.java) or [this other example](https://github.com/JChemPaint/jchempaint/blob/master/src/util/TemplateImagesMaker.java). I am sorry if I did not explain myself very well, but I hope you get the idea. – jccampanero Apr 22 '21 at 09:18
  • @AnttiHaapala It will be probably of no value, but please, consider review [this example](https://github.com/iconfinder/batik/blob/22da1acccf0093c43509456a0936fa1d7514da30/test-sources/org/apache/batik/svggen/GeneratorContext.java#L54). The idea will be to provide a custom [`StyleHandler`](https://github.com/apache/xmlgraphics-batik/blob/f79ce602fa6244d2a7ff45b57b6a0347e883bc37/batik-svggen/src/main/java/org/apache/batik/svggen/StyleHandler.java#L32) that generates on the fly the stylesheet that will be applied to the SVG document. It will give you at least the ability to override it if (cont) – jccampanero Apr 26 '21 at 10:42
  • necessary; if you have the opportunity to combine it with some kind of custom element to ID mapping, it can be relevant to your problem. I hope it helps. – jccampanero Apr 26 '21 at 10:44
  • @jccampanero there is still 8 hours time to come up with something that I could give a bounty to :D – Antti Haapala -- Слава Україні Apr 26 '21 at 11:34
  • @AnttiHaapala :D I came across that information when I reviewed your question, and I think it can be of help. Having said that, honestly, I am running out of ideas: I told you about tweaking `DOMTreeManager` and/or `DOMGroupManager`, and Babl already provide you a good answer based on them, and it has been a long time without using Batik... I hope that the information provided in some way or another helps you to solve the problem. It is an interesting problem indeed. – jccampanero Apr 26 '21 at 11:55

1 Answers1

3

The "g" element stands for the Groups for Batik and they are managed by the DOMTreeManager.appendGroup() So each time Batik creates a new group (except the top level group) it calls the DOMTreeManager.appendGroup(). What you can do is extends the SVGGraphics2D by injecting your own custom version of the DOMTreeManager which sets the id on each group.

public class IndexedSVGGraphics2D extends SVGGraphics2D {

    private Integer index = 0;

    public IndexedSVGGraphics2D(Document domFactory) {
        super(domFactory);
    }

    @Override
    protected void setGeneratorContext(SVGGeneratorContext generatorCtx) {
        // do the default work
        super.setGeneratorContext(generatorCtx);
        // do the magic
        this.domTreeManager = new DOMTreeManager(gc,
                generatorCtx,
                DEFAULT_MAX_GC_OVERRIDES){
            @Override
            public void appendGroup(Element group, DOMGroupManager groupManager) {
                // here setting any attribute which you need (in your case the id)
                group.setAttribute("id", String.valueOf(index++));
                super.appendGroup(group, groupManager);

            }
        };
        this.domGroupManager = new DOMGroupManager(gc, domTreeManager);
        this.domTreeManager.addGroupManager(domGroupManager);
    }
}

Next just use the IndexedSVGGraphics2D in your code instead of SVGGraphics2D.

Note: If you need to access the top-level Group just use the domTreeManager.getTopLevelGroup().

Babl
  • 7,446
  • 26
  • 37
  • Is there a some way to force a new group? It seems they're based on the paint and such by default, was thinking if it would be possible to use SVGGraphics2D rendering hints to provide a hint of value of `id` or `class` attribute, that would force a new group – Antti Haapala -- Слава Україні Apr 22 '21 at 14:47
  • @AnttiHaapala you should be able to overwrite default functionality with group creation by overwriting the DOMGroupManager.addElement() which is called each time there should be new element added into SVG. – Babl Apr 23 '21 at 15:15