Last active
December 14, 2017 11:55
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.0.6" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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