package com.xenoage.zong.io.symbols; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.utils.math.geom.Point2f.p; import java.util.List; import lombok.RequiredArgsConstructor; import com.xenoage.utils.math.geom.Point2f; import com.xenoage.zong.symbols.path.ClosePath; import com.xenoage.zong.symbols.path.CubicCurveTo; import com.xenoage.zong.symbols.path.LineTo; import com.xenoage.zong.symbols.path.MoveTo; import com.xenoage.zong.symbols.path.Path; import com.xenoage.zong.symbols.path.PathElement; import com.xenoage.zong.symbols.path.QuadraticCurveTo; /** * This class creates a path from a given SVG path * (d attribute value of a SVG path element). * * @author Andreas Wenger */ @RequiredArgsConstructor public class SvgPathReader { private final String svgPath; private int pos = 0; private List<PathElement> elements = alist(); private Point2f pCurrent = p(-10, -10), pStart = pCurrent; /** * Creates a path from the given d attribute value of a SVG path element. * The type of the path is implementation dependent. * The path and its bounding rect is returned. */ public Path read() { //parse commands char tokenChar = '?'; String token = getNextToken(); Point2f p, cp1, cp2; float x, y; while (token != null) { char nextTokenChar = token.charAt(0); if (Character.isDigit(nextTokenChar) || nextTokenChar == '-' || nextTokenChar == '+') { //number. reuse last command (if not 'M' or 'm' - then it is 'L' or 'l'. see SVG spec) pos -= token.length(); if (tokenChar == 'M') tokenChar = 'L'; else if (tokenChar == 'm') tokenChar = 'l'; } else { //next command tokenChar = nextTokenChar; } switch (tokenChar) { //MoveTo (absolute) case 'M': p = readPointAbs(); moveTo(p); break; //MoveTo (relative) case 'm': p = readPointRel(); moveTo(pCurrent.add(p)); break; //ClosePath case 'Z': case 'z': closePath(); break; //LineTo (absolute) case 'L': p = readPointAbs(); lineTo(p); break; //LineTo (relative) case 'l': p = readPointRel(); lineTo(pCurrent.add(p)); break; //Horizontal LineTo (absolute) case 'H': x = readCoordAbs(); lineTo(p(x, pCurrent.y)); break; //Horizontal LineTo (relative) case 'h': x = readCoordRel(); lineTo(p(pCurrent.x + x, pCurrent.y)); break; //Vertical LineTo (absolute) case 'V': y = readCoordAbs(); lineTo(p(pCurrent.x, y)); break; //Vertical LineTo (relative) case 'v': y = readCoordRel(); lineTo(p(pCurrent.x, pCurrent.y + y)); break; //Cubic CurveTo (absolute) case 'C': cp1 = readPointAbs(); cp2 = readPointAbs(); p = readPointAbs(); cubicCurveTo(cp1, cp2, p); break; //Cubic CurveTo (relative) case 'c': cp1 = readPointRel(); cp2 = readPointRel(); p = readPointRel(); cubicCurveTo(pCurrent.add(cp1), pCurrent.add(cp2), pCurrent.add(p)); break; //Quadratic CurveTo (absolute) case 'Q': cp1 = readPointAbs(); p = readPointAbs(); quadraticCurveTo(cp1, p); break; //Quadratic CurveTo (relative) case 'q': cp1 = readPointRel(); p = readPointRel(); quadraticCurveTo(pCurrent.add(cp1), pCurrent.add(p)); break; //not implemented commands case 'T': case 't': case 'S': case 's': case 'A': case 'a': throw new IllegalStateException("SVG command \"" + token + "\" not implemented yet."); //unknown command default: throw new IllegalStateException("Unknown SVG command: \"" + token + "\""); } token = getNextToken(); } return new Path(elements); } private void closePath() { pCurrent = pStart; elements.add(new ClosePath()); } private void lineTo(Point2f p) { pCurrent = p; elements.add(new LineTo(p)); } private void moveTo(Point2f p) { pStart = pCurrent = p; elements.add(new MoveTo(p)); } private void cubicCurveTo(Point2f cp1, Point2f cp2, Point2f p) { pCurrent = p; elements.add(new CubicCurveTo(cp1, cp2, p)); } private void quadraticCurveTo(Point2f cp, Point2f p) { pCurrent = p; elements.add(new QuadraticCurveTo(cp, p)); } /** * Gets the next token of the svg string, starting at pos. * Returns null, when there is no token any more. */ private String getNextToken() { //skip " " and "," and "\n" and "\r". while (pos < svgPath.length() && isWhitespace(svgPath.charAt(pos))) { pos++; } //when the end of the String is reached, return null if (pos >= svgPath.length()) return null; //find the end of the token char c0 = svgPath.charAt(pos); boolean c0Numeric = isNumeric(c0); int posEnd = pos; for (int i = pos + 1; i < svgPath.length(); i++) { char ci = svgPath.charAt(i); boolean ciNumeric = isNumeric(ci); //if c0 is numeric, but c1 not (or the other way round), the token is finished if (c0Numeric != ciNumeric) { break; } //if ci is whitespace, the token is finished if (isWhitespace(ci)) { break; } posEnd++; } String ret = svgPath.substring(pos, posEnd + 1); //new starting point is current end point pos = posEnd + 1; return ret; } /** * Returns true, if the given char is a digit, a dot, * a plus or a minus. */ private boolean isNumeric(char c) { return (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || c == '6' || c == '7' || c == '8' || c == '9' || c == '.' || c == '-' || c == '+'); } /** * Returns true, if the given char is a whitespace * (' ', ',', '\n', '\r'). */ private boolean isWhitespace(char c) { return (c == ' ' || c == ',' || c == '\n' || c == '\r'); } private Point2f readPointAbs() { return readPoint().sub(1000, 1000).scale(0.01f); } private Point2f readPointRel() { return readPoint().scale(0.01f); } private float readCoordAbs() { return (parseNumericToken(getNextToken()) - 1000) * 0.01f; } private float readCoordRel() { return parseNumericToken(getNextToken()) * 0.01f; } /** * Parse a numeric token. */ private float parseNumericToken(String token) throws NumberFormatException { return Float.parseFloat(token); } /** * Reads the next two tokens and interprets them as a point. * The values are moved by -1000/-1000 and scaled by 0.01. */ private Point2f readPoint() throws NumberFormatException { float x = parseNumericToken(getNextToken()); float y = parseNumericToken(getNextToken()); return new Point2f(x, y); } }