Java + Apache Batik で SVG のパスデータ (path 要素の d 属性) を描画する

## 概要

- Apache Batik の Parser モジュールで SVG のパスデータ (path 要素の d 属性) フォーマットを解析して Shape オブジェクトを生成して描画処理をする

## Apache Batik の Parser モジュール

Apache Batik の Parser モジュールは SVG を解析できる。
ただ、ドキュメントには Parser モジュールは一般的な開発には使うことを想定していないと書かれている (そのわりにはドキュメントがそれなりにちゃんと書かれている)。

[Architecture overview](https://xmlgraphics.apache.org/batik/using/architecture.html)

> Finally, the Low level modules are used internally by the core modules to accomplish their work. These modules are not typically used by developers directly.

[Parser module](https://xmlgraphics.apache.org/batik/using/parsers.html)

> SVG has a number of microsyntaxes that are used within attribute values, such as the transform attribute on SVGTransformable elements, and the path data d attribute on path elements. Since these are not trivial to parse, this functionality has been factored out into a separate package that can be used by other SVG-processing applications if needed.

## Apache Batik の Parser モジュールを導入する build.gradle

Gradle 用の build.gradle を用意。

```groovy
plugins {
  id 'java'
}

repositories {
  mavenCentral()
}

dependencies {
  // Apache Batik の Parser モジュールを導入
  // Batik SVG microsyntax parser
  // https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-parser
  implementation 'org.apache.xmlgraphics:batik-parser:1.13'
}

// サンプルコード PathDataDraw を実行するタスクを定義
task runPathDataDraw(type: JavaExec) {
  main = 'PathDataDraw'
  classpath = sourceSets.main.runtimeClasspath
}

// サンプルコード SVGPathDataDraw を実行するタスクを定義
task runSVGPathDataDraw(type: JavaExec) {
  main = 'SVGPathDataDraw'
  classpath = sourceSets.main.runtimeClasspath
}
```

## パスデータを描画するサンプルコード

パスデータを文字列で用意しておいて、Apache Batik Parser で Shape オブジェクトを生成する。

```java
import org.apache.batik.parser.AWTPathProducer;
import org.apache.batik.parser.PathParser;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class PathDataDraw {

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

    // 描画先の画像オブジェクト
    BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();

    // 背景を白色で塗りつぶす
    g.setBackground(Color.WHITE);
    g.clearRect(0, 0, 500, 500);

    // 描画用のペン
    g.setStroke(new BasicStroke(5.0f));

    // 描画するパスデータのリスト
    // パスデータは SVG の path 要素の d 属性に指定するものと同等の文字列
    // path 要素の例: <path d="M 0,0 h 500 v 500 h -500 z" />
    String[] openedPathList = {
      "M 0,0 h 500 v 500 h -500 z", // 画像サイズと同じ正方形
      "M 30 30 C 40 140, 80 140, 100 110", // ベジェ曲線
      "M 30 80 C 120 10, 195 10, 285 80 S 450 150, 480 80", // ベジェ曲線
      "M 10 80 Q 52.5 10, 95 80 T 180 80", // ベジェ曲線
      "A 30 50 -45 0 1 215.1 109.9", // 円弧
    };
    // パスデータを描画
    g.setPaint(Color.RED);
    for (String path : openedPathList) {
      // パスデータを Shape オブジェクトに変換
      Shape shape = getShape(path);
      // パスデータを線として描画
      g.draw(shape);
    }

    // 描画するパスデータのリスト
    String[] closedPathList = {
      "M 100,200 h 200 v 100 h -200 z", // 長方形
      "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z", // ハート
    };
    // パスデータを描画する
    for (String path : closedPathList) {
      // パスデータを Shape オブジェクトに変換
      Shape shape = getShape(path);
      // パスデータを面として描画 (塗りつぶし)
      g.setPaint(Color.GREEN);
      g.fill(shape);
      // パスデータを線として描画
      g.setPaint(Color.BLUE);
      g.draw(shape);
    }

    // 画像を PNG ファイルとして出力
    ImageIO.write(image, "png", new File("output.png"));
  }

  /**
   * パスデータを Shape オブジェクトに変換する。
   * @param d path 要素の d 属性に指定するものと同等の文字列
   * @return 形状を表すオブジェクト
   */
  private static Shape getShape(String d) {

    // d 属性値を解析するパーサー
    PathParser parser = new PathParser();

    // 解析結果を受け取って Shape を生成するオブジェクト
    // AWTPathProducer は PathHandler インターフェースを実装している
    AWTPathProducer producer = new AWTPathProducer();

    // パーサーに PathHandler を設定
    parser.setPathHandler(producer);

    // 解析処理を実施
    parser.parse(d);

    // Shape オブジェクトｗｐ取得
    Shape shape = producer.getShape();

    return shape;
  }
}
```

## SVG ファイルから path 要素の d 属性を抽出してパスデータを描画するサンプル

SVG ファイルから path 要素の d 属性を抽出し、そのパスデータを Apache Batik Parser で Shape オブジェクトに変換する。

```java
import org.apache.batik.parser.AWTPathProducer;
import org.apache.batik.parser.PathParser;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Random;

public class SVGPathDataDraw {

  public static void main(String[] args) throws IOException, XPathExpressionException, ParserConfigurationException, SAXException {

    // SVG ファイルを読み込む
    String svgFile = "input.svg";
    Document doc;
    try (FileInputStream is = new FileInputStream(Paths.get(svgFile).toFile())) {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = factory.newDocumentBuilder();
      doc = builder.parse(is);
    }

    // 描画先のオブジェクトを用意
    BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();

    Random r = new Random(); // 乱数発生機

    // SVG 文書から path 要素の d 属性を抜き出して描画
    XPath xpath = XPathFactory.newInstance().newXPath();
    NodeList nodelist = (NodeList) xpath.evaluate("//path/@d", doc, XPathConstants.NODESET);
    for (int i = 0; i < nodelist.getLength(); i++) {
      // d 属性の値を取得
      Attr attr = (Attr) nodelist.item(i);
      // d 属性の値を Shape オブジェクトに変換
      Shape shape = getShape(attr.getValue());
      // ランダムで描画色を決める
      g.setColor(new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256)));
      // パスデータを描画
      g.draw(shape);
    }

    // 画像を PNG ファイルとして出力
    ImageIO.write(image, "png", new File("output.png"));
  }

  /**
   * パスデータを Shape オブジェクトに変換する。
   * @param d path 要素の d 属性に指定するものと同等の文字列
   * @return 形状を表すオブジェクト
   */
  private static Shape getShape(String d) {
    PathParser parser = new PathParser();
    AWTPathProducer producer = new AWTPathProducer();
    parser.setPathHandler(producer);
    parser.parse(d);
    return producer.getShape();
  }
}
```

## 参考資料

- [Architecture overview - Apache Batik SVG Toolkit](https://xmlgraphics.apache.org/batik/using/architecture.html)
- [Parser module - Apache Batik SVG Toolkit](https://xmlgraphics.apache.org/batik/using/parsers.html)
- [Maven Repository: org\.apache\.xmlgraphics » batik\-parser](https://mvnrepository.com/artifact/org.apache.xmlgraphics/batik-parser)
- [PathParser \(Apache Batik Javadoc\)](https://xmlgraphics.apache.org/batik/javadoc/org/apache/batik/parser/PathParser.html)
- [AWTPathProducer \(Apache Batik Javadoc\)](https://xmlgraphics.apache.org/batik/javadoc/org/apache/batik/parser/AWTPathProducer.html)
- [PathHandler \(Apache Batik Javadoc\)](https://xmlgraphics.apache.org/batik/javadoc/org/apache/batik/parser/PathHandler.html)
- [d \- SVG: Scalable Vector Graphics \| MDN](https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d)