Как сделать групповые преобразования в SVG файле программно?

У меня есть файл SVG со следующим изображением:

Изображение

Каждая из стрелок представлена ​​следующим кодом:

<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>

Я хочу вычислить абсолютное положение прямоугольника (rect node). Для этого мне нужно оценить выражение внутри тега transform тега g (matrix(-1,0,0,-1,149.82549,457.2455) в приведенном выше примере).

Как я могу это сделать?

Я полагаю, что первый шаг должен прочитать файл как SVGDocument, используя 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) {
    // ...
}

Насколько я знаю, doc можно отнести к SVGDocument.

Как я могу получить из документа SVG абсолютное расположение прямоугольников или групп?

Примечание. Мне нужен некоторый существующий код, который делает вышеперечисленные преобразования (не говорите мне, что я должен реализовать эти преобразования самостоятельно - они должны быть уже реализованы, и я хочу повторно использовать этот код).

Обновление 1 (08.11.2015 12:56 MSK):

Первая попытка реализовать рекомендацию Роберта Лонгсона:

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

Вызов m2.getA() - m2.getF() приводит к NullPointerException s.

Обновление 2 (08.11.2015 13:38 MSK):

Добавлен следующий код для создания SVGPoint и применить к нему матричное преобразование:

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

Результат:

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)

Обновление 3, условия щедрости (10.11.2015 MSK):

Условия, которые должны выполняться для получения награды:

Я вручит награду герою или героине, которому удается реализовать методы magicallyCalculateXCoordinate и magicallyCalculateYCoordinate в JUnit тест BatikTest, так что я могу получить в своем Java-коде координаты фигур, которые отображаются в InkScape (см. следующий скриншот для примера).

Изображение с координатами

Представленный метод вычисления положения фигур в SVG файлах должен работать либо

  • для групповых узлов (например, тот, что находится на картинке, и в пример файла) или
  • для треугольников.

Код, который вы предоставляете, должен работать для всех четырех фигур в файле примера, т.е. е. используя его, я должен уметь вычислять в Java код их координаты, которые равны тем, которые отображаются в Inkscape.

Вы можете добавить параметры к методам magicallyCalculateXCoordinate и magicallyCalculateYCoordinate, и вы также можете создать свои собственные методы, которые вычисляют координаты.

Вы можете использовать любые библиотеки, которые могут использоваться на законных основаниях в коммерческих целях.

Все файлы, связанные с этим запросом, доступны на GitHub. Мне удалось скомпилировать тест, используя IntelliJ Idea Community Edition 14.1.5.

Ответы

Ответ 1

Я знаю, я опаздываю на вечеринку, но я наткнулся на этот вопрос и наслаждался загадкой; -)

Происхождение в svg - верхний левый угол, в то время как inkscape использует нижний левый как начало.

источник и контрольная точка в svg (слева) и inkscape (справа)

Итак, нам нужно применить поглаживание и преобразование, затем найти нижнюю левую точку, округленную о трех десятичных цифрах. Я использовал GVTBuilder для получения ограничивающей рамки с применением стиля, затем преобразовал boundingBox, запросил ограничительную рамку преобразованной группы, а затем использовал xMin и yMax в качестве контрольной точки. Используя окно просмотра для определения высоты, я преобразовал координату y и, наконец, округлил координаты. (см. запрос pull на 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();
    }

}

Он должен работать для всех групп и всех преобразований.