17

I have SVG file with following image:

Image

Each of the arrows is represented by a code like this:

<g
   transform="matrix(-1,0,0,-1,149.82549,457.2455)"
   id="signS"
   inkscape:label="#sign">
  <title
     id="title4249">South, 180</title>
  <path
     sodipodi:nodetypes="ccc"
     inkscape:connector-curvature="0"
     id="path4251"
     d="m 30.022973,250.04026 4.965804,-2.91109 4.988905,2.91109"
     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.6855976px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
  <rect
     y="250.11305"
     x="29.768578"
     height="2.6057031"
     width="10.105703"
     id="rect4253"
     style="fill:#008000;fill-opacity:1;stroke:#000000;stroke-width:0.53715414;stroke-opacity:1" />
</g>

I want to calculate the absolute position of the rectangle (rect node). In order to do this, I need to evaluate the expression inside the transform tag of the g tag (matrix(-1,0,0,-1,149.82549,457.2455) in the example above).

How can I do it?

I assume that the first step is to read the file as SVGDocument using Apache Batik:

import java.io.IOException;

import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.util.XMLResourceDescriptor;

import org.w3c.dom.Document;

try {
    String parser = XMLResourceDescriptor.getXMLParserClassName();
    SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
    String uri = "http://www.example.org/diagram.svg";
    Document doc = f.createDocument(uri);
} catch (IOException ex) {
    // ...
}

As far as I know, doc can be cast to SVGDocument.

How can I get from SVG document to the absolute locations of the rectangles or groups?

Note: I need some existing code, which does the transformations above (don't tell me to implement these transformations myself - they must be implemented already and I want to re-use that code).

Update 1 (08.11.2015 12:56 MSK):

First attempt to implement Robert Longson's recommendation:

public final class BatikTest {
    @Test
    public void test() throws XPathExpressionException {
        try {
            final File initialFile =
                new File("src/main/resources/trailer/scene05_signs.svg");
            InputStream sceneFileStream = Files.asByteSource(initialFile).openStream();


            String parser = XMLResourceDescriptor.getXMLParserClassName();
            SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
            String uri = "http://www.example.org/diagram.svg";
            final SVGOMDocument doc = (SVGOMDocument) f.createDocument(
                uri, sceneFileStream);

            final NodeList nodes =
                doc.getDocumentElement().getElementsByTagName("g");
            SVGOMGElement signSouth = null;


            for (int i=0; (i < nodes.getLength()) && (signSouth == null); i++) {
                final Node curNode = nodes.item(i);
                final Node id = curNode.getAttributes().getNamedItem("id");
                if ("signS".equals(id.getTextContent())) {
                    signSouth = (SVGOMGElement) curNode;
                }

                System.out.println("curNode: " + nodes);
            }
            System.out.println("signSouth: " + signSouth);

            final NodeList rectNodes = signSouth.getElementsByTagName("rect");
            System.out.println("rectNodes: " + rectNodes);

            SVGOMRectElement rectNode = (SVGOMRectElement) rectNodes.item(0);

            System.out.println("rectNode: " + rectNode);

            final SVGMatrix m2 =
                signSouth.getTransformToElement(rectNode);

            System.out.println("m2: " + m2);
        } catch (IOException ex) {
            Assert.fail(ex.getMessage());
        }
    }
}

Calls to m2.getA()-m2.getF() result in NullPointerExceptions.

Update 2 (08.11.2015 13:38 MSK):

Added following code to create SVGPoint and apply the matrix transform to it:

final SVGSVGElement docElem = (SVGSVGElement)
    doc.getDocumentElement();
final SVGPoint svgPoint = docElem.createSVGPoint();
svgPoint.setX((float) x);
svgPoint.setY((float) y);
final SVGPoint svgPoint1 =
    svgPoint.matrixTransform(signSouth.getScreenCTM()); // Line 77

System.out.println("x: " + svgPoint1.getX());
System.out.println("y: " + svgPoint1.getY());

Result:

java.lang.NullPointerException
    at org.apache.batik.dom.svg.SVGLocatableSupport$3.getAffineTransform(Unknown Source)
    at org.apache.batik.dom.svg.AbstractSVGMatrix.getA(Unknown Source)
    at org.apache.batik.dom.svg.SVGOMPoint.matrixTransform(Unknown Source)
    at org.apache.batik.dom.svg.SVGOMPoint.matrixTransform(Unknown Source)
    at [...].BatikTest.test(BatikTest.java:77)

Update 3, terms of the bounty (10.11.2015 MSK):

Conditions, which must be satisfied in order to get the bounty:

I will award the bounty to the hero or heroine, who manages to implement the methods magicallyCalculateXCoordinate and magicallyCalculateYCoordinate in the JUnit test BatikTest such that I can get in my Java code the coordinates of the shapes, which are displayed in InkScape (see the following screenshot for an example).

Image with coordinates

The presented method of calculating the position of shapes in SVG files must work either

  1. for group nodes (like the one in the picture and in the sample file) or
  2. for triangles.

The code you provide must work for all four shapes in the sample file, i. e. using it I must be able to calculate in Java code the coordinates of them, which are equal to those displayed in Inkscape.

You can add parameters to the methods magicallyCalculateXCoordinate and magicallyCalculateYCoordinate, and you also can create your own methods, which calculate the coordinates.

You may use any libraries, which can be used legally for commercial purposes.

All files related to this request are available on GitHub. I managed to compile the test using IntelliJ Idea Community Edition 14.1.5.

Mentiflectax
  • 13,367
  • 41
  • 152
  • 285
  • 1
    Any of getScreenCTM, getCTM or getTransformToElement will give you the overall transform from absolute units to local units. Apply that to the element's local co-orginates and that will give you the co-ordinates in absolute units. – Robert Longson Nov 08 '15 at 09:01
  • @RobertLongson Thanks. I tried to implement your approach, see update 1. I can find the group nodes and the rect node inside it. How do I apply the matrices to get the actual coordinates in that code? – Mentiflectax Nov 08 '15 at 10:00
  • 1
    create an SVGPoint set its values to the local co-ordinates, call matrixTransform http://www.w3.org/TR/SVG/coords.html#InterfaceSVGPoint and http://www.w3.org/TR/SVG/struct.html#__svg__SVGSVGElement__createSVGPoint – Robert Longson Nov 08 '15 at 10:05
  • @RobertLongson I'm getting a `NullPointerException`, when I apply the matrix to the `SVGPoint`. See update 2. – Mentiflectax Nov 08 '15 at 10:40
  • Not sure, presumably getScreenCTM is null which shouldn't happen. Maybe try getCTM or getTransformToElement (from the root element) – Robert Longson Nov 08 '15 at 11:11
  • @RobertLongson See update 3. I think you can easily win the bounty :) – Mentiflectax Nov 10 '15 at 10:45

1 Answers1

2

I know, I'm late to the party, but I stumbled upon this question and enjoyed the riddle ;-)

The origin in a svg is the top left corner, while inkscape uses the bottom left as origin.

origin and reference point in svg (left) and inkscape (right)

So we need to apply stroking and transform, then find the bottom left point rounded o three decimal figures. I used GVTBuilder for getting the bounding box with applied styling, then transformed the boundingBox, requested bounding box of the transformed group, then used xMin and yMax as the reference point. Using the viewbox to determine the height I transformed the y coordinate and finally rounded the coordinates. (see pull request on github)

package test.java.svgspike;

import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.anim.dom.SVGOMGElement;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.XMLResourceDescriptor;

import javax.xml.xpath.XPathExpressionException;

import java.awt.geom.Point2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;

import com.google.common.io.Files;

import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Created by pisarenko on 10.11.2015.
 */
public final class BatikTest {

    @Test
    public void test() throws XPathExpressionException {
        try {
            final File initialFile =
                    new File("src/test/resources/scene05_signs.svg");
            InputStream sceneFileStream = Files.asByteSource(initialFile).openStream();

            String parser = XMLResourceDescriptor.getXMLParserClassName();
            SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
            String uri = "http://www.example.org/diagram.svg";
            final SVGOMDocument doc = (SVGOMDocument) f.createDocument(
                    uri, sceneFileStream);

            String viewBox = doc.getDocumentElement().getAttribute("viewBox");

            Point2D referencePoint = getReferencePoint(doc, getGroupElement(doc, "signS"));

            double signSouthX = magicallyCalculateXCoordinate(referencePoint);
            double signSouthY = magicallyCalculateYCoordinate(referencePoint, viewBox);

            Assert.assertEquals(109.675, signSouthX, 0.0000001);
            Assert.assertEquals(533.581, signSouthY, 0.0000001);

            referencePoint = getReferencePoint(doc, getGroupElement(doc, "signN"));
            Assert.assertEquals(109.906, magicallyCalculateXCoordinate(referencePoint), 0.0000001);
            Assert.assertEquals(578.293, magicallyCalculateYCoordinate(referencePoint, viewBox), 0.0000001);

            referencePoint = getReferencePoint(doc, getGroupElement(doc, "signE"));
            Assert.assertEquals(129.672, magicallyCalculateXCoordinate(referencePoint), 0.0000001);
            Assert.assertEquals(554.077, magicallyCalculateYCoordinate(referencePoint, viewBox), 0.0000001);

            referencePoint = getReferencePoint(doc, getGroupElement(doc, "signW"));
            Assert.assertEquals(93.398, magicallyCalculateXCoordinate(referencePoint), 0.0000001);
            Assert.assertEquals(553.833, magicallyCalculateYCoordinate(referencePoint, viewBox), 0.0000001);


        } catch (IOException ex) {
            Assert.fail(ex.getMessage());
        }
    }

    private SVGOMGElement getGroupElement(SVGOMDocument doc, String id){
        final NodeList nodes = doc.getDocumentElement().getElementsByTagName("g");
        SVGOMGElement signGroup = null;
        for (int i=0; (i < nodes.getLength()) && (signGroup == null); i++) {
            final Node curNode = nodes.item(i);
            final Node idNode = curNode.getAttributes().getNamedItem("id");
            if (id.equals(idNode.getTextContent())) signGroup = (SVGOMGElement) curNode;
        }
        return signGroup;
    }

    /**
     * @param doc
     * @param signGroup
     * @return the reference point, inkscape uses for group (bottom left corner of group)
     */
    private Point2D getReferencePoint(SVGOMDocument doc, SVGOMGElement signGroup){

        Point2D referencePoint = new Point2D.Double(0, 0);

        try {

            BridgeContext ctx = new BridgeContext(new UserAgentAdapter());
            new GVTBuilder().build(ctx, doc);
            GraphicsNode gvtElement = new GVTBuilder().build(ctx, signGroup);

            Rectangle2D rc = gvtElement.getSensitiveBounds();
            rc = ((Path2D) gvtElement.getTransform().createTransformedShape(rc)).getBounds2D();

          //find xMin and yMax in poi
            referencePoint = new Point2D.Double(rc.getMinX(), rc.getMaxY());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return referencePoint;
    }

    /**
     * inkscape states y coordinate with origin in left bottom corner, while svg uses top left corner as origin
     * @param referencePoint bottom left corner of group
     * @param viewBox in "originX originY width height" notation
     * @return corrected y coordinate, rounded to three decimal figures (half up)
     */
    private double magicallyCalculateYCoordinate(Point2D referencePoint, String viewBox) {
        String[] viewBoxValues = viewBox.split(" ");
        BigDecimal roundedY = new BigDecimal(Double.parseDouble(viewBoxValues[3])-referencePoint.getY());
        roundedY = roundedY.setScale(3, BigDecimal.ROUND_HALF_UP);
        return roundedY.doubleValue();
    }

    /**
     * @param referencePoint bottom left corner of group
     * @return x coordinate, rounded to three decimal figures (half up)
     */
    private double magicallyCalculateXCoordinate(Point2D referencePoint) {
        BigDecimal roundedX = new BigDecimal(referencePoint.getX()).setScale(3, BigDecimal.ROUND_HALF_UP);
        return roundedX.doubleValue();
    }

}

It should work for all groups and all transformations.

Frederic Klein
  • 2,796
  • 3
  • 21
  • 36