Skip to content

Instantly share code, notes, and snippets.

@nimatrueway
Last active December 14, 2017 11:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nimatrueway/cc1668c16096e2f2184275d8f780e3a5 to your computer and use it in GitHub Desktop.
Save nimatrueway/cc1668c16096e2f2184275d8f780e3a5 to your computer and use it in GitHub Desktop.
XML and SVG-Path parser sample for scala-guide assignment#2 | http://engineering.pintapin.com/1396/06/30/scala-quick-guide/
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.0.6"
package engine;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* PathParser adopted from react-native-svg renderer written for android
* https://raw.githubusercontent.com/react-native-community/react-native-svg/a958668e7f2753e7587eaf8a39ea46133809dc1a/android/src/main/java/com/horcrux/svg/PropHelper.java
*/
public class PathParser {
static private final Pattern PATH_REG_EXP = Pattern.compile("[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+", Pattern.CASE_INSENSITIVE);
static private final Pattern DECIMAL_REG_EXP = Pattern.compile("(\\.\\d+)(?=-?\\.)");
private Matcher mMatcher;
private Path2D mPath;
private final String mString;
private double mPenX = 0f;
private double mPenY = 0f;
private double mPenDownX;
private double mPenDownY;
private double mPivotX = 0f;
private double mPivotY = 0f;
private double mScale = 1f;
private boolean mValid = true;
private boolean mPendDownSet = false;
private String mLastCommand;
private String mLastValue;
private LinkedList<Point2D> mBezierCurves;
private Point2D mLastStartPoint;
public PathParser(String d, double scale) {
mScale = scale;
mString = d;
}
private void executeCommand(String command) {
switch (command) {
// moveTo command
case "m":
move(getNextDouble(), getNextDouble());
break;
case "M":
moveTo(getNextDouble(), getNextDouble());
break;
// lineTo command
case "l":
line(getNextDouble(), getNextDouble());
break;
case "L":
lineTo(getNextDouble(), getNextDouble());
break;
// horizontalTo command
case "h":
line(getNextDouble(), 0);
break;
case "H":
lineTo(getNextDouble(), mPenY);
break;
// verticalTo command
case "v":
line(0, getNextDouble());
break;
case "V":
lineTo(mPenX, getNextDouble());
break;
// curveTo command
case "c":
curve(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
case "C":
curveTo(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
// smoothCurveTo command
case "s":
smoothCurve(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
case "S":
smoothCurveTo(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
// quadraticBezierCurveTo command
case "q":
quadraticBezierCurve(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
case "Q":
quadraticBezierCurveTo(getNextDouble(), getNextDouble(), getNextDouble(), getNextDouble());
break;
// smoothQuadraticBezierCurveTo command
case "t":
smoothQuadraticBezierCurve(getNextDouble(), getNextDouble());
break;
case "T":
smoothQuadraticBezierCurveTo(getNextDouble(), getNextDouble());
break;
// arcTo command
case "a":
arc(getNextDouble(), getNextDouble(), getNextDouble(), getNextBoolean(), getNextBoolean(), getNextDouble(), getNextDouble());
break;
case "A":
arcTo(getNextDouble(), getNextDouble(), getNextDouble(), getNextBoolean(), getNextBoolean(), getNextDouble(), getNextDouble());
break;
// close command
case "Z":
case "z":
close();
break;
default:
mLastValue = command;
executeCommand(mLastCommand);
return;
}
mLastCommand = command;
if (command.equals("m")) {
mLastCommand = "l";
} else if (command.equals("M")) {
mLastCommand = "L";
}
}
public Path2D getPath() {
mPath = new Path2D.Double();
mBezierCurves = new LinkedList();
mMatcher = PATH_REG_EXP.matcher(DECIMAL_REG_EXP.matcher(mString).replaceAll("$1,"));
while (mMatcher.find() && mValid) {
executeCommand(mMatcher.group());
}
return mPath;
}
private Point2D getPointMap(double x, double y) {
return new Point2D.Double(x * mScale, y * mScale);
}
private Point2D clonePointMap(Point2D map) {
return new Point2D.Double(map.getX(), map.getY());
}
private boolean getNextBoolean() {
if (mMatcher.find()) {
return mMatcher.group().equals("1");
} else {
mValid = false;
mPath = new Path2D.Double();
return false;
}
}
private double getNextDouble() {
if (mLastValue != null) {
String lastValue = mLastValue;
mLastValue = null;
return Double.parseDouble(lastValue);
} else if (mMatcher.find()) {
return Double.parseDouble(mMatcher.group());
} else {
mValid = false;
mPath = new Path2D.Double();
return 0;
}
}
private void move(double x, double y) {
moveTo(x + mPenX, y + mPenY);
}
private void moveTo(double x, double y) {
mPivotX = mPenX = x;
mPivotY = mPenY = y;
mPath.moveTo(x * mScale, y * mScale);
mLastStartPoint = getPointMap(x ,y);
mBezierCurves.add(getPointMap(x, y));
}
private void line(double x, double y) {
lineTo(x + mPenX, y + mPenY);
}
private void lineTo(double x, double y) {
setPenDown();
mPivotX = mPenX = x;
mPivotY = mPenY = y;
mPath.lineTo(x * mScale, y * mScale);
LinkedList<Point2D> points = new LinkedList<>();
points.add(getPointMap(x, y));
points.add(getPointMap(x, y));
points.add(getPointMap(x, y));
mBezierCurves.addAll(points);
}
private void curve(double c1x, double c1y, double c2x, double c2y, double ex, double ey) {
curveTo(c1x + mPenX, c1y + mPenY, c2x + mPenX, c2y + mPenY, ex + mPenX, ey + mPenY);
}
private void curveTo(double c1x, double c1y, double c2x, double c2y, double ex, double ey) {
mPivotX = c2x;
mPivotY = c2y;
cubicTo(c1x, c1y, c2x, c2y, ex, ey);
}
private void cubicTo(double c1x, double c1y, double c2x, double c2y, double ex, double ey) {
setPenDown();
mPenX = ex;
mPenY = ey;
mPath.curveTo(c1x * mScale, c1y * mScale, c2x * mScale, c2y * mScale, ex * mScale, ey * mScale);
LinkedList<Point2D> points = new LinkedList<>();
points.add(getPointMap(c1x, c1y));
points.add(getPointMap(c2x, c2y));
points.add(getPointMap(ex, ey));
mBezierCurves.addAll(points);
}
private void smoothCurve(double c1x, double c1y, double ex, double ey) {
smoothCurveTo(c1x + mPenX, c1y + mPenY, ex + mPenX, ey + mPenY);
}
private void smoothCurveTo(double c1x, double c1y, double ex, double ey) {
double c2x = c1x;
double c2y = c1y;
c1x = (mPenX * 2) - mPivotX;
c1y = (mPenY * 2) - mPivotY;
mPivotX = c2x;
mPivotY = c2y;
cubicTo(c1x, c1y, c2x, c2y, ex, ey);
}
private void quadraticBezierCurve(double c1x, double c1y, double c2x, double c2y) {
quadraticBezierCurveTo(c1x + mPenX, c1y + mPenY, c2x + mPenX, c2y + mPenY);
}
private void quadraticBezierCurveTo(double c1x, double c1y, double c2x, double c2y) {
mPivotX = c1x;
mPivotY = c1y;
double ex = c2x;
double ey = c2y;
c2x = (ex + c1x * 2) / 3;
c2y = (ey + c1y * 2) / 3;
c1x = (mPenX + c1x * 2) / 3;
c1y = (mPenY + c1y * 2) / 3;
cubicTo(c1x, c1y, c2x, c2y, ex, ey);
}
private void smoothQuadraticBezierCurve(double c1x, double c1y) {
smoothQuadraticBezierCurveTo(c1x + mPenX, c1y + mPenY);
}
private void smoothQuadraticBezierCurveTo(double c1x, double c1y) {
double c2x = c1x;
double c2y = c1y;
c1x = (mPenX * 2) - mPivotX;
c1y = (mPenY * 2) - mPivotY;
quadraticBezierCurveTo(c1x, c1y, c2x, c2y);
}
private void arc(double rx, double ry, double rotation, boolean outer, boolean clockwise, double x, double y) {
arcTo(rx, ry, rotation, outer, clockwise, x + mPenX, y + mPenY);
}
private void arcTo(double rx, double ry, double rotation, boolean outer, boolean clockwise, double x, double y) {
double tX = mPenX;
double tY = mPenY;
ry = Math.abs(ry == 0 ? (rx == 0 ? (y - tY) : rx) : ry);
rx = Math.abs(rx == 0 ? (x - tX) : rx);
if (rx == 0 || ry == 0 || (x == tX && y == tY)) {
lineTo(x, y);
return;
}
double rad = Math.toRadians(rotation);
double cos = Math.cos(rad);
double sin = Math.sin(rad);
x -= tX;
y -= tY;
// Ellipse Center
double cx = cos * x / 2 + sin * y / 2;
double cy = -sin * x / 2 + cos * y / 2;
double rxry = rx * rx * ry * ry;
double rycx = ry * ry * cx * cx;
double rxcy = rx * rx * cy * cy;
double a = rxry - rxcy - rycx;
if (a < 0){
a = Math.sqrt(1 - a / rxry);
rx *= a;
ry *= a;
cx = x / 2;
cy = y / 2;
} else {
a = Math.sqrt(a / (rxcy + rycx));
if (outer == clockwise) {
a = -a;
}
double cxd = -a * cy * rx / ry;
double cyd = a * cx * ry / rx;
cx = cos * cxd - sin * cyd + x / 2;
cy = sin * cxd + cos * cyd + y / 2;
}
// Rotation + Scale Transform
double xx = cos / rx;
double yx = sin / rx;
double xy = -sin / ry;
double yy = cos / ry;
// Start and End Angle
double sa = Math.atan2(xy * -cx + yy * -cy, xx * -cx + yx * -cy);
double ea = Math.atan2(xy * (x - cx) + yy * (y - cy), xx * (x - cx) + yx * (y - cy));
cx += tX;
cy += tY;
x += tX;
y += tY;
setPenDown();
mPenX = mPivotX = x;
mPenY = mPivotY = y;
arcToBezier(cx, cy, rx, ry, sa, ea, clockwise, rad);
}
private void close() {
if (mPendDownSet) {
mPenX = mPenDownX;
mPenY = mPenDownY;
mPendDownSet = false;
mPath.closePath();
LinkedList<Point2D> points = new LinkedList<>();
points.add(clonePointMap(mLastStartPoint));
points.add(clonePointMap(mLastStartPoint));
points.add(clonePointMap(mLastStartPoint));
mBezierCurves.addAll(points);
}
}
private void arcToBezier(double cx, double cy, double rx, double ry, double sa, double ea, boolean clockwise, double rad) {
// Inverse Rotation + Scale Transform
double cos = Math.cos(rad);
double sin = Math.sin(rad);
double xx = cos * rx;
double yx = -sin * ry;
double xy = sin * rx;
double yy = cos * ry;
// Bezier Curve Approximation
double arc = ea - sa;
if (arc < 0 && clockwise) {
arc += Math.PI * 2;
} else if (arc > 0 && !clockwise) {
arc -= Math.PI * 2;
}
int n = (int) Math.ceil(Math.abs(arc / (Math.PI / 2)));
double step = arc / n;
double k = (4 / 3) * Math.tan(step / 4);
double x = Math.cos(sa);
double y = Math.sin(sa);
for (int i = 0; i < n; i++){
double cp1x = x - k * y;
double cp1y = y + k * x;
sa += step;
x = Math.cos(sa);
y = Math.sin(sa);
double cp2x = x + k * y;
double cp2y = y - k * x;
mPath.curveTo(
(cx + xx * cp1x + yx * cp1y) * mScale,
(cy + xy * cp1x + yy * cp1y) * mScale,
(cx + xx * cp2x + yx * cp2y) * mScale,
(cy + xy * cp2x + yy * cp2y) * mScale,
(cx + xx * x + yx * y) * mScale,
(cy + xy * x + yy * y) * mScale
);
}
}
private void setPenDown() {
if (!mPendDownSet) {
mPenDownX = mPenX;
mPenDownY = mPenY;
mPendDownSet = true;
}
}
}
import java.awt._
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import scala.xml._
import engine.PathParser
object RendererSample {
def main(args: Array[String]): Unit = {
val svg =
"""
|<svg viewBox="0 0 75 75">
| <path d="M72.2996,74.4002 L2.0996,74.4002 C0.9006,74.4002 -0.0004,73.4002 -0.0004,72.2992 L-0.0004,2.1002 C-0.0004,0.9002 0.9996,0.0002 2.0996,0.0002 L72.4006,0.0002 C73.6006,0.0002 74.4996,1.0002 74.4996,2.1002 L74.4996,72.2002 C74.4996,73.4002 73.4996,74.4002 72.2996,74.4002" fill="#F1666A" />
| <path d="M37.2,68.9002 C37.2,68.9002 37.1,68.9002 37,68.7992 L18.8,37.4002 L18.5,36.8002 C16.9,33.8002 16,30.4002 16,26.7002 C16,15.0002 25.5,5.5002 37.3,5.5002 C49.1,5.5002 58.6,15.0002 58.6,26.7002 C58.6,30.6002 57.6,34.2002 55.8,37.3002 L37.2,68.9002 Z" fill="#A6616F" />
| <path d="M18.4002,36.8001 C16.8002,33.8001 15.9002,30.4001 15.9002,26.7001 C15.9002,15.0001 25.4002,5.5001 37.2002,5.5001 C49.0002,5.5001 58.5002,15.0001 58.5002,26.7001 C58.5002,38.4001 49.0002,47.9001 37.2002,47.9001 C35.4002,47.9001 33.6002,47.7001 31.9002,47.2001 L31.7002,47.2001 L37.2002,68.9001 C37.2002,68.9001 37.1002,68.9001 37.0002,68.8001 L18.8002,37.4001 L18.4002,36.8001 Z M37.2002,13.5001 C29.9002,13.5001 23.9002,19.4001 23.9002,26.8001 C23.9002,34.2001 29.9002,40.1001 37.2002,40.1001 C44.5002,40.1001 50.5002,34.2001 50.5002,26.8001 C50.5002,19.4001 44.5002,13.5001 37.2002,13.5001 L37.2002,13.5001 Z" fill="#FFFFFF" />
|</svg>
""".stripMargin
val xml = XML.loadString(svg)
val dimensions = xml.attribute("viewBox").map(_.head.text.split(" ").map(_.toInt)).map(d => new Dimension(d(2), d(3))).get
val image = new BufferedImage(dimensions.width, dimensions.height, BufferedImage.TYPE_INT_RGB)
val graphics = image.getGraphics.asInstanceOf[Graphics2D]
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
xml.child.filter(_.label == "path").foreach { pathNode =>
val pathParser = new PathParser(pathNode.attribute("d").get.head.text, 1.0f)
graphics.setPaint(Color.decode(pathNode.attribute("fill").get.head.text))
graphics.fill(pathParser.getPath)
}
ImageIO.write(image, "png", new File(System.getProperty("user.home")).toPath.resolve("pintapin.png").toFile)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment