Creating levels in Box2D Part 3 - Extracting Inkscape path data using Batik

Welcome to the third part of my tutorial series on how to use Inkscape to create objects for Box2D physics simulations. In the first part I described how to load the SVG file into Java and start extracting the information we need. In the second section how to interpolate cubic splines. In this section I'm going to describe how to parse the SVG path data using the open source library Batik.

Firstly you need to download Batik here and make the library available in your software development environment. I use Eclipse for my Java development and add external libraries as user libraries. For instructions on how to do this visit here.

Once Batik is set up we're going to start by extracting the transform data from the SVG document.

Parsing transform data

To parse the transform we're going to use the Batik TransformListParser class with the TransformListHandler interface. Batik uses an event based parsing model. This means that as parser parses the code for each command it finds it calls relevant method in the handler i.e. matrix command calls matrix() method. Batik provides an implementation of the handler interface for AWT but we want to write out own implementation. To do we need to create a new class which implements the TransformHandler interface. Then in each of the methods we can decide what we want to do when this event occurs.

  1. public class TransformHandler implements TransformListHandler {
  2. // Used for logging
  3. public final String TAG = this.getClass().getSimpleName();
  4.  
  5. // Class imported from the Java Topology Suite library
  6. public AffineTransformation at;
  7.  
  8. @Override
  9. public void startTransformList() throws ParseException {
  10. at = new AffineTransformation();
  11. }
  12.  
  13. @Override
  14. public void matrix(float a, float b, float c, float d, float e, float f) throws ParseException {
  15. at.setTransformation(a, c, e, b, d, f);
  16. }
  17.  
  18. @Override
  19. public void rotate(float theta) throws ParseException {}
  20.  
  21. @Override
  22. public void rotate(float theta, float cx, float cy) throws ParseException {}
  23.  
  24. @Override
  25. public void translate(float tx) throws ParseException {
  26. at.translate(tx, 0);
  27. }
  28.  
  29. @Override
  30. public void translate(float tx, float ty) throws ParseException {
  31. at.translate(tx, ty);
  32. }
  33.  
  34. @Override
  35. public void scale(float sx) throws ParseException {}
  36.  
  37. @Override
  38. public void scale(float sx, float sy) throws ParseException {}
  39.  
  40. @Override
  41. public void skewX(float skx) throws ParseException {}
  42.  
  43. @Override
  44. public void skewY(float sky) throws ParseException {}
  45.  
  46. @Override
  47. public void endTransformList() throws ParseException {}
  48. }

As you can see from the above code we're only interested in the translate and matrix commands. If you wanted, you could support more transform types but in this instance we don't need to because for complex paths Inkscape will re-calculate the coordinates instead of applying a rotate command for example.

When we find a command we're interested in we need to add it to the affine transform. You could concatenate the new transform onto the old transform in case an object had both a rotate and a translate command but from my testing it seems that normally if there are a number of transformations Inkscape will use the transformation matrix instead.

Using this class we can now populate the our empty parseGlobalTransform and parseTransform functions.

  1. /*
  2. * This function extracts the transform data from the SVG file
  3. * then adds it to our custom coordinate transform
  4. */
  5. public void parseGlobalTransform(Element baseElement) {
  6. // Get layer transform
  7. NodeList nl = baseElement.getElementsByTagName("g");
  8.  
  9. Element gTag = (Element) nl.item(0);
  10. // get the transform attribute
  11. String transform = gTag.getAttribute("transform");
  12.  
  13. // Concatinate the new layer affine transform to the current
  14. at.composeBefore(parseTransform(transform));
  15. }
  16. /*
  17. * Performs the actual parsing of the SVG
  18. */
  19. private AffineTransformation parseTransform (String transform) {
  20. // Create new handler and parser
  21. TransformHandler th = new TransformHandler();
  22. TransformListParser tlp = new TransformListParser();
  23.  
  24. // Add our transform list handler to the parser
  25. tlp.setTransformListHandler(th);
  26. tlp.parse(transform);
  27.  
  28. return th.at;
  29. }

This function takes the base element as it's argument, extracts the "g" node which represents the Inkscape layer, then extracts the transform attributes. Next it calls the second function parseTransform. This initialises our TransformHandler and creates a new TransformListParser. After we have parsed the transform string we return the affine transform from our transform handler. The new transform is added to our global coordinate transformation. This means that the origin in the bottom left hand corner then the canvas is translated to match the transform attribute.

Parsing the path

Next we need to parse the path data from the SVG file. The SVG file stores it's path data in a long string containing point coordinates and letters to represent the type of path. For detailed information about the SVG path specification visit here. As before we use the same design pattern - path handler and path parser. Below is my implementation of the PathHandler interface which will parse the path and return a Spline which represents the path. This code supports the following SVG command types: moveto (m, M), closepath(z, Z), lineto(l, L, h, H, v, V), Cubic Benzier(c, C) Cubic Benzier smooth (s, S). Quadratic benziers (q, Q, t, T) and arcs (a, A) aren't currently supported because they aren't used by Inkscape.

  1. public class PathHandler implements org.apache.batik.parser.PathHandler {
  2. public final String TAG = this.getClass().getSimpleName();
  3.  
  4. public ArrayList<Spline> splines = new ArrayList<Spline>();
  5. public Spline currentSpline;
  6. private SplineVertex currentPoint;
  7.  
  8.  
  9. /*
  10. * When we find a new path create a new spline to store the path information
  11. */
  12. @Override
  13. public void startPath() throws ParseException {
  14. currentSpline = new Spline();
  15. }
  16.  
  17. /*
  18. * When we find an end path command add the current spline to the array of paths
  19. */
  20. @Override
  21. public void endPath() throws ParseException {
  22. splines.add(currentSpline);
  23. }
  24.  
  25. /**
  26.   * Parses a 'm' command.
  27.   */
  28. @Override
  29. public void movetoRel(float x, float y) throws ParseException {
  30. currentPoint = new SplineVertex(x,y);
  31. addCurrentPoint();
  32. }
  33.  
  34. /**
  35.   * Parses a 'M' command.
  36.   */
  37. @Override
  38. public void movetoAbs(float x, float y) throws ParseException {
  39. currentPoint = new SplineVertex(x, y);
  40. addCurrentPoint();
  41. }
  42.  
  43. @Override
  44. public void closePath() throws ParseException {
  45. currentPoint = currentSpline.getFirst();
  46. addCurrentPoint();
  47. }
  48.  
  49. /**
  50.   * Parses a 'l' command.
  51.   */
  52. @Override
  53. public void linetoRel(float x, float y) throws ParseException {
  54. currentPoint = currentPoint.add(x,y);
  55. addCurrentPoint();
  56. }
  57.  
  58. /**
  59.   * Parses a 'L' command.
  60.   */
  61. @Override
  62. public void linetoAbs(float x, float y) throws ParseException {
  63. currentPoint = new SplineVertex(x,y);
  64. addCurrentPoint();
  65. }
  66.  
  67. /**
  68.   * Parses a 'h' command.
  69.   */
  70. @Override
  71. public void linetoHorizontalRel(float x) throws ParseException {
  72. currentPoint = currentPoint.add(x, 0);
  73. addCurrentPoint();
  74. }
  75.  
  76. /**
  77.   * Parses a 'H' command.
  78.   */
  79. @Override
  80. public void linetoHorizontalAbs(float x) throws ParseException {
  81. currentPoint = new SplineVertex(x, currentPoint.p.y);
  82. addCurrentPoint();
  83. }
  84.  
  85. /**
  86.   * Parses a 'v' command.
  87.   */
  88. @Override
  89. public void linetoVerticalRel(float y) throws ParseException {
  90. currentPoint = currentPoint.add(0, y);
  91. addCurrentPoint();
  92. }
  93.  
  94. /**
  95.   * Parses a 'V' command.
  96.   */
  97. @Override
  98. public void linetoVerticalAbs(float y) throws ParseException {
  99. currentPoint = new SplineVertex(currentPoint.p.x, y);
  100. addCurrentPoint();
  101. }
  102.  
  103. /**
  104.   * Parses a 'c' command.
  105.   */
  106. @Override
  107. public void curvetoCubicRel(float x1, float y1, float x2, float y2,
  108. float x, float y) throws ParseException {
  109. // add control point to previous current point
  110. currentPoint.cp2 = currentPoint.p.add(x1, y1);
  111. // create a new current point relative to the last
  112. currentPoint = new SplineVertex(currentPoint.p.add(x,y), currentPoint.p.add(x2,y2), currentPoint.p.add(x,y));
  113. addCurrentPoint();
  114. }
  115.  
  116. /**
  117.   * Parses a 'C' command.
  118.   */
  119. @Override
  120. public void curvetoCubicAbs(float x1, float y1, float x2, float y2,
  121. float x, float y) throws ParseException {
  122. // add control point to previous current point
  123. currentPoint.cp2.set(x1, y1);
  124. // create a new current point relative to the last
  125. currentPoint = new SplineVertex(x,y, x2,y2, x,y);
  126. addCurrentPoint();
  127. }
  128.  
  129. /**
  130.   * Parses a 's' command.
  131.   */
  132. @Override
  133. public void curvetoCubicSmoothRel(float x2, float y2, float x, float y)
  134. throws ParseException {
  135. SplineVertex sv = currentPoint;
  136. currentPoint.cp2.set(sv.p.x * 2 - sv.cp1.x, sv.p.y * 2 - sv.cp1.y);
  137. currentPoint = new SplineVertex(currentPoint.p.add(x,y), currentPoint.p.add(x2,y2), currentPoint.p.add(x,y));
  138. addCurrentPoint();
  139. }
  140.  
  141. /**
  142.   * Parses a 'S' command.
  143.   */
  144. @Override
  145. public void curvetoCubicSmoothAbs(float x2, float y2, float x, float y)
  146. throws ParseException {
  147. SplineVertex sv = currentPoint;
  148. currentPoint.cp2.set(sv.p.x * 2 - sv.cp1.x, sv.p.y * 2 - sv.cp1.y);
  149. currentPoint = new SplineVertex(x,y, x2,y2, x,y);
  150. addCurrentPoint();
  151. }
  152.  
  153. /**
  154.   * Parses a 'q' command.
  155.   */
  156. @Override
  157. public void curvetoQuadraticRel(float x1, float y1, float x, float y)
  158. throws ParseException {}
  159.  
  160. /**
  161.   * Parses a 'Q' command.
  162.   */
  163. @Override
  164. public void curvetoQuadraticAbs(float x1, float y1, float x, float y)
  165. throws ParseException {}
  166.  
  167. /**
  168.   * Parses a 't' command.
  169.   */
  170. @Override
  171. public void curvetoQuadraticSmoothRel(float x, float y)
  172. throws ParseException {}
  173.  
  174. /**
  175.   * Parses a 'T' command.
  176.   */
  177. @Override
  178. public void curvetoQuadraticSmoothAbs(float x, float y)
  179. throws ParseException {}
  180.  
  181. /**
  182.   * Parses a 'a' command.
  183.   */
  184. @Override
  185. public void arcRel(float rx, float ry, float xAxisRotation,
  186. boolean largeArcFlag, boolean sweepFlag, float x, float y)
  187. throws ParseException {}
  188.  
  189. /**
  190.   * Parses a 'A' command.
  191.   */
  192. @Override
  193. public void arcAbs(float rx, float ry, float xAxisRotation,
  194. boolean largeArcFlag, boolean sweepFlag, float x, float y)
  195. throws ParseException {}
  196.  
  197. public void addCurrentPoint () {
  198. currentSpline.addPoint(currentPoint);
  199. }
  200. }

The above code should be fairly easy to understand if you're familiar with the SVG path specification. If not you can use the code as is. Now that we can convert an Inkscape path into a spline object we can add this code to our inkscape parsing class.

  1. public void parsePaths(Element baseElement) {
  2.  
  3. /*
  4. * Extract the path data from the SVG file
  5. */
  6. NodeList nl = baseElement.getElementsByTagName("path");
  7.  
  8. // Path parser
  9. OpenGLPathHandler ph = new PathHandler();
  10. PathParser pp = new PathParser();
  11. pp.setPathHandler(ph);
  12.  
  13. /*
  14. * For each path get the element for the particular path, get the path data from the "d" tag
  15. * then parse the path using our path handler. Extract the transform from the path and apply it to the spline
  16. * finally add the path to the list of paths in our Inkscape parsing class.
  17. */
  18. for(int i=0; i<nl.getLength(); i++) {
  19. Element elm = (Element) nl.item(i);
  20.  
  21. // Get path data
  22. String path = elm.getAttribute("d");
  23. pp.parse(path);
  24.  
  25. // Get transformation data
  26. String transform = elm.getAttribute("transform");
  27.  
  28. // Transform the resulting spline
  29. ph.currentSpline.transfrom(parseTransform(transform));
  30. addPath(ph.currentSpline);
  31. }
  32. }

That's it, we have all the data we need from the SVG file in a format we can use. The complete source code can be downloaded here. To use this source just unzip the files to your IDE. To parse an SVG file you just need to call the static method ExtractSVGPaths.extract(String filePath) with the path to the file. This method will return an ArrayList of Vec2 coordinates containing the points on the path. If you need extra functionality i.e. you want to generate splines to a lower resolution you can modify this class.

In the next section I'll explain how to triangulate the path using the library Poly2Tri.

Tweet: 

Comments

Hello, I've been using your code for my own tool to extract paths from Inkscape, as well as adding a way to extract positions of objects and making output data for several layers. It outputs this data to a Lua file that can be read as a table. I'm in the process of cleaning this up and making it usable for anyone and would like to stick it in a github project or something like that . I've made many changes but several of your classes and code is untouched - would you mind this? I'll of course give proper attribution that it's based on this blog post.

Hi Stoffe,

You're welcome to use the code and it would be great if you could credit this blog with any code you use. When you make your tool available I'd be interested in taking a look.

Ben

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <c>, <cpp>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>. The supported tag styles are: <foo>, [foo].
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.