diff --git a/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java b/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java index 208cfb860..6c20824f0 100644 --- a/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java +++ b/src/main/java/de/neemann/digital/draw/graphics/PolygonParser.java @@ -9,19 +9,13 @@ package de.neemann.digital.draw.graphics; * Creates a polygon from a path */ public class PolygonParser { - - enum Token {EOF, COMMAND, NUMBER} - - private final String path; - private int lastTokenPos; - private int pos; - private char command; - private float value; + private final SVGTokenizer t; private float x; private float y; private VectorFloat polyStart; private VectorInterface lastQuadraticControlPoint; private VectorInterface lastCubicControlPoint; + private String command = ""; /** * Creates a new instance @@ -29,84 +23,26 @@ public class PolygonParser { * @param path the path to parse */ public PolygonParser(String path) { - this.path = path; - pos = 0; + t = new SVGTokenizer(path); } - Token next() { - lastTokenPos = pos; - while (pos < path.length() && (path.charAt(pos) == ' ' || path.charAt(pos) == ',')) - pos++; - if (pos == path.length()) - return Token.EOF; - - char c = path.charAt(pos); - if (Character.isAlphabetic(c)) { - pos++; - command = c; - return Token.COMMAND; - } else { - value = parseNumber(); - return Token.NUMBER; - } + private float nextValue() throws SVGTokenizer.TokenizerException { + return t.readFloat(); } - private char peekChar() { - return path.charAt(pos); - } - - private float parseNumber() { - int p0 = pos; - if (peekChar() == '+' || peekChar() == '-') - pos++; - - while (pos < path.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) - pos++; - - if (pos < path.length() && (peekChar() == 'e' || peekChar() == 'E')) { - pos++; - if (peekChar() == '+' || peekChar() == '-') - pos++; - - while (pos < path.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) - pos++; - } - - return Float.parseFloat(path.substring(p0, pos)); - } - - - private void unreadToken() { - pos = lastTokenPos; - } - - char getCommand() { - return command; - } - - double getValue() { - return value; - } - - private float nextValue() throws ParserException { - if (next() != Token.NUMBER) - throw new ParserException("expected a number at pos " + pos + " in '" + path + "'"); - return value; - } - - private VectorFloat nextVector() throws ParserException { + private VectorFloat nextVector() throws SVGTokenizer.TokenizerException { x = nextValue(); y = nextValue(); return new VectorFloat(x, y); } - private VectorFloat nextVectorInc() throws ParserException { + private VectorFloat nextVectorInc() throws SVGTokenizer.TokenizerException { x += nextValue(); y += nextValue(); return new VectorFloat(x, y); } - private VectorFloat nextVectorRel() throws ParserException { + private VectorFloat nextVectorRel() throws SVGTokenizer.TokenizerException { return new VectorFloat(x + nextValue(), y + nextValue()); } @@ -117,110 +53,113 @@ public class PolygonParser { * @throws ParserException ParserException */ public Polygon create() throws ParserException { - Polygon p = new Polygon(false); - Token tok; - boolean closedPending = false; - while ((tok = next()) != Token.EOF) { - if (tok == Token.NUMBER) { - unreadToken(); - if (command == 'm') - command = 'l'; - else if (command == 'M') - command = 'L'; - } - switch (command) { - case 'M': - if (closedPending) { - closedPending = false; - p.addClosePath(); - } - p.addMoveTo(setPolyStart(nextVector())); - clearControl(); - break; - case 'm': - if (closedPending) { - closedPending = false; - p.addClosePath(); - } - p.addMoveTo(setPolyStart(nextVectorInc())); - clearControl(); - break; - case 'V': - y = nextValue(); - p.add(getCurrent()); - clearControl(); - break; - case 'v': - y += nextValue(); - p.add(getCurrent()); - clearControl(); - break; - case 'H': - x = nextValue(); - p.add(getCurrent()); - clearControl(); - break; - case 'h': - x += nextValue(); - p.add(getCurrent()); - clearControl(); - break; - case 'l': - p.add(nextVectorInc()); - clearControl(); - break; - case 'L': - p.add(nextVector()); - clearControl(); - break; - case 'c': - p.add(nextVectorRel(), setLastC3(nextVectorRel()), nextVectorInc()); - break; - case 'C': - p.add(nextVector(), setLastC3(nextVector()), nextVector()); - break; - case 'q': - p.add(setLastC2(nextVectorRel()), nextVectorInc()); - break; - case 'Q': - p.add(setLastC2(nextVector()), nextVector()); - break; - case 's': - addCubicWithReflect(p, getCurrent(), nextVectorRel(), nextVectorInc()); - break; - case 'S': - addCubicWithReflect(p, getCurrent(), nextVector(), nextVector()); - break; - case 't': - addQuadraticWithReflect(p, getCurrent(), nextVectorInc()); - break; - case 'T': - addQuadraticWithReflect(p, getCurrent(), nextVector()); - break; - case 'a': - addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc()); - clearControl(); - break; - case 'A': - addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector()); - clearControl(); - break; - case 'Z': - case 'z': - closedPending = true; - if (polyStart != null) { - x = polyStart.getXFloat(); - y = polyStart.getYFloat(); - } - clearControl(); - break; - default: - throw new ParserException("unsupported path command " + command); + try { + Polygon p = new Polygon(false); + boolean closedPending = false; + while (!t.isEOF()) { + if (t.nextIsNumber()) { + if (command.equals("m")) + command = "l"; + else if (command.equals("M")) + command = "L"; + } else + command = t.readCommand(); + switch (command) { + case "M": + if (closedPending) { + closedPending = false; + p.addClosePath(); + } + p.addMoveTo(setPolyStart(nextVector())); + clearControl(); + break; + case "m": + if (closedPending) { + closedPending = false; + p.addClosePath(); + } + p.addMoveTo(setPolyStart(nextVectorInc())); + clearControl(); + break; + case "V": + y = nextValue(); + p.add(getCurrent()); + clearControl(); + break; + case "v": + y += nextValue(); + p.add(getCurrent()); + clearControl(); + break; + case "H": + x = nextValue(); + p.add(getCurrent()); + clearControl(); + break; + case "h": + x += nextValue(); + p.add(getCurrent()); + clearControl(); + break; + case "l": + p.add(nextVectorInc()); + clearControl(); + break; + case "L": + p.add(nextVector()); + clearControl(); + break; + case "c": + p.add(nextVectorRel(), setLastC3(nextVectorRel()), nextVectorInc()); + break; + case "C": + p.add(nextVector(), setLastC3(nextVector()), nextVector()); + break; + case "q": + p.add(setLastC2(nextVectorRel()), nextVectorInc()); + break; + case "Q": + p.add(setLastC2(nextVector()), nextVector()); + break; + case "s": + addCubicWithReflect(p, getCurrent(), nextVectorRel(), nextVectorInc()); + break; + case "S": + addCubicWithReflect(p, getCurrent(), nextVector(), nextVector()); + break; + case "t": + addQuadraticWithReflect(p, getCurrent(), nextVectorInc()); + break; + case "T": + addQuadraticWithReflect(p, getCurrent(), nextVector()); + break; + case "a": + addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc()); + clearControl(); + break; + case "A": + addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector()); + clearControl(); + break; + case "Z": + case "z": + closedPending = true; + if (polyStart != null) { + x = polyStart.getXFloat(); + y = polyStart.getYFloat(); + } + clearControl(); + break; + default: + throw new ParserException("unsupported path command " + command); + } } + if (closedPending) + p.setClosed(true); + return p; + } catch (SVGTokenizer.TokenizerException e) { + throw new ParserException("error parsing a path", e); } - if (closedPending) - p.setClosed(true); - return p; } private VectorFloat setPolyStart(VectorFloat v) { @@ -408,6 +347,10 @@ public class PolygonParser { private ParserException(String message) { super(message); } + + private ParserException(String message, Exception cause) { + super(message, cause); + } } /** @@ -417,7 +360,11 @@ public class PolygonParser { * @throws ParserException ParserException */ public Polygon parsePolygon() throws ParserException { - return parsePolygonPolyline(true); + try { + return parsePolygonPolyline(true); + } catch (SVGTokenizer.TokenizerException e) { + throw new ParserException("error parsing a polygon", e); + } } /** @@ -427,13 +374,17 @@ public class PolygonParser { * @throws ParserException ParserException */ public Polygon parsePolyline() throws ParserException { - return parsePolygonPolyline(false); + try { + return parsePolygonPolyline(false); + } catch (SVGTokenizer.TokenizerException e) { + throw new ParserException("error parsing a polyline", e); + } } - private Polygon parsePolygonPolyline(boolean closed) throws ParserException { + private Polygon parsePolygonPolyline(boolean closed) throws SVGTokenizer.TokenizerException { Polygon p = new Polygon(closed); - while (next() != Token.EOF) - p.add(new VectorFloat(value, nextValue())); + while (!t.isEOF()) + p.add(new VectorFloat(nextValue(), nextValue())); return p; } diff --git a/src/main/java/de/neemann/digital/draw/graphics/SVGTokenizer.java b/src/main/java/de/neemann/digital/draw/graphics/SVGTokenizer.java new file mode 100644 index 000000000..da0b62fd1 --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/graphics/SVGTokenizer.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2018 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.draw.graphics; + +/** + * A tokenizer to parse svg strings + */ +public class SVGTokenizer { + /** + * token types + */ + public enum Token { + /** + * EOF + */ + EOF, + /** + * a command + */ + COMMAND, + /** + * a number + */ + NUMBER, + /** + * a character + */ + CHAR + } + + private final String code; + private int lastTokenPos; + private int pos; + private StringBuilder command; + private float value; + private char character; + + /** + * Creates a new instance + * + * @param code the code to parse + */ + public SVGTokenizer(String code) { + this.code = code; + command = new StringBuilder(); + pos = 0; + } + + private Token next() throws TokenizerException { + lastTokenPos = pos; + while (pos < code.length() && (Character.isWhitespace(code.charAt(pos)) || code.charAt(pos) == ',')) + pos++; + if (pos == code.length()) + return Token.EOF; + + character = code.charAt(pos); + if (Character.isAlphabetic(character)) { + command.setLength(0); + pos++; + command.append(character); + while (pos < code.length() && Character.isAlphabetic(code.charAt(pos))) { + command.append(code.charAt(pos)); + pos++; + } + return Token.COMMAND; + } + if (Character.isDigit(character) || character == '-' || character == '+') { + value = parseNumber(); + return Token.NUMBER; + } else { + pos++; + return Token.CHAR; + } + } + + private char peekChar() { + return code.charAt(pos); + } + + private float parseNumber() throws TokenizerException { + int p0 = pos; + if (peekChar() == '+' || peekChar() == '-') + pos++; + + while (pos < code.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) + pos++; + + if (pos < code.length() && (peekChar() == 'e' || peekChar() == 'E')) { + pos++; + if (peekChar() == '+' || peekChar() == '-') + pos++; + + while (pos < code.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) + pos++; + } + + try { + return Float.parseFloat(code.substring(p0, pos)); + } catch (NumberFormatException e) { + throw new TokenizerException("not a number " + code.substring(p0, pos)); + } + } + + + private void unreadToken() { + pos = lastTokenPos; + } + + /** + * Expect the given character c + * + * @param c the expected character + * @throws TokenizerException TokenizerException + */ + public void expect(char c) throws TokenizerException { + if (next() != Token.CHAR) + throw new TokenizerException("expected character " + c); + if (character != c) + throw new RuntimeException("expected " + c + " found " + character); + } + + /** + * Reads a float + * + * @return the float + * @throws TokenizerException TokenizerException + */ + public float readFloat() throws TokenizerException { + if (next() != Token.NUMBER) + throw new TokenizerException("expected a number"); + return value; + } + + /** + * Reads a command + * + * @return the command + * @throws TokenizerException TokenizerException + */ + public String readCommand() throws TokenizerException { + if (next() != Token.COMMAND) + throw new TokenizerException("expected a command"); + return command.toString(); + } + + /** + * @return true if the next token is a number + * @throws TokenizerException TokenizerException + */ + public boolean nextIsNumber() throws TokenizerException { + if (next() == Token.NUMBER) { + unreadToken(); + return true; + } else { + unreadToken(); + return false; + } + } + + /** + * Checks if the next char is the given char. + * + * @param c the char + * @return true if next char is the given char. + * @throws TokenizerException TokenizerException + */ + public boolean nextIsChar(char c) throws TokenizerException { + if (next() == Token.CHAR) { + if (character == c) { + return true; + } else { + unreadToken(); + return false; + } + } else { + unreadToken(); + return false; + } + } + + + /** + * @return true if the EOF is reached + * @throws TokenizerException TokenizerException + */ + public boolean isEOF() throws TokenizerException { + if (next() == Token.EOF) { + return true; + } else { + unreadToken(); + return false; + } + } + + @Override + public String toString() { + return code + " (" + pos + ")"; + } + + /** + * @return the remainig string + */ + public String remaining() { + final String s = code.substring(pos).trim(); + pos = code.length(); + return s; + } + + /** + * Reads all up to the given character + * + * @param c the character + * @return the data string + */ + public String readTo(char c) { + int p = pos; + int brace = 0; + while (pos < code.length() && (code.charAt(pos) != c || brace != 0)) { + switch (code.charAt(pos)) { + case '(': + brace++; + break; + case ')': + brace--; + break; + } + pos++; + } + + final String r = code.substring(p, pos).trim(); + + while (pos < code.length() && code.charAt(pos) == c) + pos++; + + return r; + } + + /** + * Exception thrown by the tokenizer + */ + public final class TokenizerException extends Exception { + private TokenizerException(String message) { + super(message); + } + } + +} diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java index 3775c13be..468fffcd4 100644 --- a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/Context.java @@ -5,10 +5,7 @@ */ package de.neemann.digital.draw.shapes.custom.svg; -import de.neemann.digital.draw.graphics.Orientation; -import de.neemann.digital.draw.graphics.Transform; -import de.neemann.digital.draw.graphics.VectorFloat; -import de.neemann.digital.draw.graphics.VectorInterface; +import de.neemann.digital.draw.graphics.*; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -21,7 +18,6 @@ class Context { private static final HashMap PARSER = new HashMap<>(); - static { PARSER.put("transform", (c1, value1) -> c1.tr = Transform.mul(new TransformParser(value1).parse(), c1.tr)); PARSER.put("fill", (c, value) -> c.fill = getColorFromString(value)); @@ -82,16 +78,18 @@ class Context { } static Context readStyle(Context context, String style) throws SvgException { - StringTokenizer st = new StringTokenizer(style, ";"); - while (st.hasMoreTokens()) { - String[] t = st.nextToken().split(":"); - if (t.length == 2) { - AttrParser p = PARSER.get(t[0].trim()); + SVGTokenizer t = new SVGTokenizer(style); + try { + while (!t.isEOF()) { + String command = t.readTo(':'); + AttrParser p = PARSER.get(command); if (p != null) - p.parse(context, t[1].trim()); + p.parse(context, t.readTo(';')); } + return context; + } catch (SVGTokenizer.TokenizerException e) { + throw new SvgException("invalid svg style: " + style, e); } - return context; } Transform getTransform() { @@ -155,16 +153,15 @@ class Context { } void addClasses(String classes) { - classes = classes.trim(); - while (classes.startsWith(".")) { - int p1 = classes.indexOf("{"); - int p2 = classes.indexOf("}"); - if (p1 < 0 || p2 < 0) - return; - String key = classes.substring(1, p1); - String val = classes.substring(p1 + 1, p2); - classesMap.put(key, val); - classes = classes.substring(p2 + 1).trim(); + SVGTokenizer t = new SVGTokenizer(classes); + try { + while (t.nextIsChar('.')) { + String key=t.readTo('{'); + String val = t.readTo('}'); + classesMap.put(key, val); + } + } catch (SVGTokenizer.TokenizerException e) { + // ignore errors } } @@ -187,31 +184,39 @@ class Context { } private static Color getColorFromString(String v) { - if (v.equalsIgnoreCase("none")) - return null; - try { - if (v.startsWith("#")) { - if (v.length() == 4) - return new Color(sRGB(v.charAt(1)), sRGB(v.charAt(2)), sRGB(v.charAt(3))); + SVGTokenizer t = new SVGTokenizer(v); + if (t.nextIsChar('#')) { + String c = t.remaining(); + if (c.length() == 3) + return new Color(sRGB(c.charAt(0)), sRGB(c.charAt(1)), sRGB(c.charAt(2))); else return Color.decode(v); - } else if (v.startsWith("rgb(")) { - StringTokenizer st = new StringTokenizer(v.substring(4), " ,)"); - return new Color(rgb(st.nextToken()), rgb(st.nextToken()), rgb(st.nextToken())); + } else { + final String command = t.readCommand(); + switch (command) { + case "none": + return null; + case "rgb": + t.expect('('); + Color col = new Color(rgb(t), rgb(t), rgb(t)); + t.expect(')'); + return col; + default: + return (Color) Color.class.getField(command.toLowerCase()).get(null); + } } - - return (Color) Color.class.getField(v.toLowerCase()).get(null); - } catch (RuntimeException | IllegalAccessException | NoSuchFieldException e) { + } catch (Exception e) { return Color.BLACK; } } - private static int rgb(String s) { - if (s.endsWith("%")) - return (int) (Float.parseFloat(s.substring(0, s.length() - 1)) / 100 * 255); + private static int rgb(SVGTokenizer t) throws SVGTokenizer.TokenizerException { + float v = t.readFloat(); + if (t.nextIsChar('%')) + return (int) (v * 2.55f); else - return Integer.parseInt(s); + return (int) v; } private static int sRGB(char c) { @@ -220,10 +225,11 @@ class Context { } private static float getFloatFromString(String inp) { - inp = inp.replaceAll("[^0-9.]", ""); - if (inp.isEmpty()) + try { + return Float.parseFloat(inp); + } catch (NumberFormatException e) { return 1; - return Float.parseFloat(inp); + } } } diff --git a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/TransformParser.java b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/TransformParser.java index 1df0c407d..d0f80c77f 100644 --- a/src/main/java/de/neemann/digital/draw/shapes/custom/svg/TransformParser.java +++ b/src/main/java/de/neemann/digital/draw/shapes/custom/svg/TransformParser.java @@ -5,24 +5,14 @@ */ package de.neemann.digital.draw.shapes.custom.svg; -import de.neemann.digital.draw.graphics.Transform; -import de.neemann.digital.draw.graphics.TransformMatrix; -import de.neemann.digital.draw.graphics.TransformTranslate; -import de.neemann.digital.draw.graphics.VectorFloat; +import de.neemann.digital.draw.graphics.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; class TransformParser { private static final Logger LOGGER = LoggerFactory.getLogger(TransformParser.class); - enum Token {EOF, COMMAND, NUMBER, CHAR} - - private final String transform; - private int lastTokenPos; - private int pos; - private StringBuilder command; - private float value; - private char character; + private SVGTokenizer tok; /** * Creates a new instance @@ -30,94 +20,7 @@ class TransformParser { * @param transform the path to parse */ TransformParser(String transform) { - this.transform = transform; - command = new StringBuilder(); - pos = 0; - } - - private Token next() { - lastTokenPos = pos; - while (pos < transform.length() && (transform.charAt(pos) == ' ' || transform.charAt(pos) == ',')) - pos++; - if (pos == transform.length()) - return Token.EOF; - - character = transform.charAt(pos); - if (Character.isAlphabetic(character)) { - command.setLength(0); - pos++; - command.append(character); - while (pos < transform.length() && Character.isAlphabetic(transform.charAt(pos))) { - command.append(transform.charAt(pos)); - pos++; - } - return Token.COMMAND; - } - if (Character.isDigit(character) || character == '-' || character == '+') { - value = parseNumber(); - return Token.NUMBER; - } else { - pos++; - return Token.CHAR; - } - } - - private char peekChar() { - return transform.charAt(pos); - } - - private float parseNumber() { - int p0 = pos; - if (peekChar() == '+' || peekChar() == '-') - pos++; - - while (pos < transform.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) - pos++; - - if (pos < transform.length() && (peekChar() == 'e' || peekChar() == 'E')) { - pos++; - if (peekChar() == '+' || peekChar() == '-') - pos++; - - while (pos < transform.length() && (Character.isDigit(peekChar()) || peekChar() == '.')) - pos++; - } - - return Float.parseFloat(transform.substring(p0, pos)); - } - - - private void unreadToken() { - pos = lastTokenPos; - } - - private String getCommand() { - return command.toString(); - } - - private float getValue() { - return value; - } - - private void expect(char c) { - if (next() != Token.CHAR) - throw new RuntimeException("expected character " + c); - if (character != c) - throw new RuntimeException("expected " + c + " found " + character); - } - - private float readFloat() { - if (next() != Token.NUMBER) - throw new RuntimeException("expected a number"); - return value; - } - - private float readOptionalFloat(float def) { - if (next() != Token.NUMBER) { - unreadToken(); - return def; - } else - return value; + tok = new SVGTokenizer(transform); } /** @@ -129,36 +32,36 @@ class TransformParser { Transform combined = Transform.IDENTITY; try { Transform t; - while (true) { - final Token tok = next(); - if (tok == Token.EOF) - break; - if (tok != Token.COMMAND) - throw new RuntimeException("invalid transform", null); - switch (getCommand()) { + while (!tok.isEOF()) { + final String command = tok.readCommand(); + switch (command) { case "translate": - expect('('); - final float x = readFloat(); - float y = readOptionalFloat(0); - expect(')'); + tok.expect('('); + final float x = tok.readFloat(); + float y = 0; + if (tok.nextIsNumber()) + y = tok.readFloat(); + tok.expect(')'); t = new TransformTranslate(new VectorFloat(x, y)); break; case "scale": - expect('('); - final float xs = readFloat(); - float ys = readOptionalFloat(xs); - expect(')'); + tok.expect('('); + final float xs = tok.readFloat(); + float ys = xs; + if (tok.nextIsNumber()) + ys = tok.readFloat(); + tok.expect(')'); t = new TransformMatrix(xs, 0, 0, ys, 0, 0); break; case "matrix": - expect('('); - final float ma = readFloat(); - final float mb = readFloat(); - final float mc = readFloat(); - final float md = readFloat(); - final float mx = readFloat(); - final float my = readFloat(); - expect(')'); + tok.expect('('); + final float ma = tok.readFloat(); + final float mb = tok.readFloat(); + final float mc = tok.readFloat(); + final float md = tok.readFloat(); + final float mx = tok.readFloat(); + final float my = tok.readFloat(); + tok.expect(')'); t = new TransformMatrix( ma, mc, @@ -168,27 +71,25 @@ class TransformParser { my); break; case "rotate": - expect('('); - float w = readFloat(); - if (next() == Token.NUMBER) { + tok.expect('('); + float w = tok.readFloat(); + if (tok.nextIsNumber()) { t = TransformMatrix.rotate(w); - float xc = getValue(); - float yc = readFloat(); + float xc = tok.readFloat(); + float yc = tok.readFloat(); t = Transform.mul(new TransformTranslate(-xc, -yc), t); t = Transform.mul(t, new TransformTranslate(xc, yc)); - } else { - unreadToken(); + } else t = TransformMatrix.rotate(w); - } - expect(')'); + tok.expect(')'); break; default: - throw new RuntimeException("unknown transform: " + value, null); + throw new RuntimeException("unknown transform: " + command, null); } combined = Transform.mul(t, combined); } - } catch (RuntimeException e) { - LOGGER.warn(transform + ": " + e.getMessage()); + } catch (Exception e) { + LOGGER.warn(tok + ": " + e.getMessage()); } return combined; } diff --git a/src/test/java/de/neemann/digital/draw/graphics/PolygonParserTest.java b/src/test/java/de/neemann/digital/draw/graphics/PolygonParserTest.java deleted file mode 100644 index 0b25cb637..000000000 --- a/src/test/java/de/neemann/digital/draw/graphics/PolygonParserTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2016 Helmut Neemann - * Use of this source code is governed by the GPL v3 license - * that can be found in the LICENSE file. - */ -package de.neemann.digital.draw.graphics; - -import junit.framework.TestCase; - -public class PolygonParserTest extends TestCase { - - public void testSimple() { - PolygonParser pp = new PolygonParser("m 1,2"); - - assertEquals(PolygonParser.Token.COMMAND, pp.next()); - assertEquals('m', pp.getCommand()); - - assertEquals(PolygonParser.Token.NUMBER, pp.next()); - assertEquals(1.0, pp.getValue(), 1e-6); - - assertEquals(PolygonParser.Token.NUMBER, pp.next()); - assertEquals(2.0, pp.getValue(), 1e-6); - - assertEquals(PolygonParser.Token.EOF, pp.next()); - assertEquals(PolygonParser.Token.EOF, pp.next()); - } - - public void testSimpleExp() { - PolygonParser pp = new PolygonParser("1e1"); - assertEquals(PolygonParser.Token.NUMBER, pp.next()); - assertEquals(10, pp.getValue(), 1e-6); - assertEquals(PolygonParser.Token.EOF, pp.next()); - } - - public void testSimpleExpSign() { - PolygonParser pp = new PolygonParser("1e-1"); - assertEquals(PolygonParser.Token.NUMBER, pp.next()); - assertEquals(0.1, pp.getValue(), 1e-6); - assertEquals(PolygonParser.Token.EOF, pp.next()); - } - -} \ No newline at end of file diff --git a/src/test/java/de/neemann/digital/draw/graphics/SVGTokenizerTest.java b/src/test/java/de/neemann/digital/draw/graphics/SVGTokenizerTest.java new file mode 100644 index 000000000..33a75e515 --- /dev/null +++ b/src/test/java/de/neemann/digital/draw/graphics/SVGTokenizerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2018 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.draw.graphics; + +import junit.framework.TestCase; + +public class SVGTokenizerTest extends TestCase { + + public void testSimple() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("m 1,2"); + assertEquals("m", t.readCommand()); + assertEquals(1.0, t.readFloat(), 1e-6); + assertEquals(2.0, t.readFloat(), 1e-6); + assertTrue(t.isEOF()); + assertTrue(t.isEOF()); + } + + public void testSimpleExp() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("1e1"); + assertEquals(10, t.readFloat(), 1e-6); + assertTrue(t.isEOF()); + } + + public void testSimpleExpSign() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("1e-1"); + assertEquals(0.1, t.readFloat(), 1e-6); + assertTrue(t.isEOF()); + } + + public void testRemaining() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("test:World(-)"); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World(-)", t.remaining()); + assertTrue(t.isEOF()); + } + + public void testReadto() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("test:World(-);"); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World(-)", t.readTo(';')); + assertTrue(t.isEOF()); + } + + public void testReadtoEnd() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("test:World(-)"); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World(-)", t.readTo(';')); + assertTrue(t.isEOF()); + } + + public void testReadtoEndNested() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("test:World(;)"); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World(;)", t.readTo(';')); + assertTrue(t.isEOF()); + } + + public void testReadtoEndNested2() throws SVGTokenizer.TokenizerException { + SVGTokenizer t = new SVGTokenizer("test:World(;);test:World"); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World(;)", t.readTo(';')); + assertEquals("test", t.readCommand()); + t.expect(':'); + assertEquals("World", t.readTo(';')); + assertTrue(t.isEOF()); + } + +} \ No newline at end of file