refactoring of SVG tokenizing

This commit is contained in:
hneemann 2018-12-04 21:48:31 +01:00
parent 53c15330d7
commit 0e09c9fb28
6 changed files with 539 additions and 397 deletions

View File

@ -9,19 +9,13 @@ package de.neemann.digital.draw.graphics;
* Creates a polygon from a path * Creates a polygon from a path
*/ */
public class PolygonParser { public class PolygonParser {
private final SVGTokenizer t;
enum Token {EOF, COMMAND, NUMBER}
private final String path;
private int lastTokenPos;
private int pos;
private char command;
private float value;
private float x; private float x;
private float y; private float y;
private VectorFloat polyStart; private VectorFloat polyStart;
private VectorInterface lastQuadraticControlPoint; private VectorInterface lastQuadraticControlPoint;
private VectorInterface lastCubicControlPoint; private VectorInterface lastCubicControlPoint;
private String command = "";
/** /**
* Creates a new instance * Creates a new instance
@ -29,84 +23,26 @@ public class PolygonParser {
* @param path the path to parse * @param path the path to parse
*/ */
public PolygonParser(String path) { public PolygonParser(String path) {
this.path = path; t = new SVGTokenizer(path);
pos = 0;
} }
Token next() { private float nextValue() throws SVGTokenizer.TokenizerException {
lastTokenPos = pos; return t.readFloat();
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 char peekChar() { private VectorFloat nextVector() throws SVGTokenizer.TokenizerException {
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 {
x = nextValue(); x = nextValue();
y = nextValue(); y = nextValue();
return new VectorFloat(x, y); return new VectorFloat(x, y);
} }
private VectorFloat nextVectorInc() throws ParserException { private VectorFloat nextVectorInc() throws SVGTokenizer.TokenizerException {
x += nextValue(); x += nextValue();
y += nextValue(); y += nextValue();
return new VectorFloat(x, y); return new VectorFloat(x, y);
} }
private VectorFloat nextVectorRel() throws ParserException { private VectorFloat nextVectorRel() throws SVGTokenizer.TokenizerException {
return new VectorFloat(x + nextValue(), y + nextValue()); return new VectorFloat(x + nextValue(), y + nextValue());
} }
@ -117,19 +53,19 @@ public class PolygonParser {
* @throws ParserException ParserException * @throws ParserException ParserException
*/ */
public Polygon create() throws ParserException { public Polygon create() throws ParserException {
try {
Polygon p = new Polygon(false); Polygon p = new Polygon(false);
Token tok;
boolean closedPending = false; boolean closedPending = false;
while ((tok = next()) != Token.EOF) { while (!t.isEOF()) {
if (tok == Token.NUMBER) { if (t.nextIsNumber()) {
unreadToken(); if (command.equals("m"))
if (command == 'm') command = "l";
command = 'l'; else if (command.equals("M"))
else if (command == 'M') command = "L";
command = 'L'; } else
} command = t.readCommand();
switch (command) { switch (command) {
case 'M': case "M":
if (closedPending) { if (closedPending) {
closedPending = false; closedPending = false;
p.addClosePath(); p.addClosePath();
@ -137,7 +73,7 @@ public class PolygonParser {
p.addMoveTo(setPolyStart(nextVector())); p.addMoveTo(setPolyStart(nextVector()));
clearControl(); clearControl();
break; break;
case 'm': case "m":
if (closedPending) { if (closedPending) {
closedPending = false; closedPending = false;
p.addClosePath(); p.addClosePath();
@ -145,68 +81,68 @@ public class PolygonParser {
p.addMoveTo(setPolyStart(nextVectorInc())); p.addMoveTo(setPolyStart(nextVectorInc()));
clearControl(); clearControl();
break; break;
case 'V': case "V":
y = nextValue(); y = nextValue();
p.add(getCurrent()); p.add(getCurrent());
clearControl(); clearControl();
break; break;
case 'v': case "v":
y += nextValue(); y += nextValue();
p.add(getCurrent()); p.add(getCurrent());
clearControl(); clearControl();
break; break;
case 'H': case "H":
x = nextValue(); x = nextValue();
p.add(getCurrent()); p.add(getCurrent());
clearControl(); clearControl();
break; break;
case 'h': case "h":
x += nextValue(); x += nextValue();
p.add(getCurrent()); p.add(getCurrent());
clearControl(); clearControl();
break; break;
case 'l': case "l":
p.add(nextVectorInc()); p.add(nextVectorInc());
clearControl(); clearControl();
break; break;
case 'L': case "L":
p.add(nextVector()); p.add(nextVector());
clearControl(); clearControl();
break; break;
case 'c': case "c":
p.add(nextVectorRel(), setLastC3(nextVectorRel()), nextVectorInc()); p.add(nextVectorRel(), setLastC3(nextVectorRel()), nextVectorInc());
break; break;
case 'C': case "C":
p.add(nextVector(), setLastC3(nextVector()), nextVector()); p.add(nextVector(), setLastC3(nextVector()), nextVector());
break; break;
case 'q': case "q":
p.add(setLastC2(nextVectorRel()), nextVectorInc()); p.add(setLastC2(nextVectorRel()), nextVectorInc());
break; break;
case 'Q': case "Q":
p.add(setLastC2(nextVector()), nextVector()); p.add(setLastC2(nextVector()), nextVector());
break; break;
case 's': case "s":
addCubicWithReflect(p, getCurrent(), nextVectorRel(), nextVectorInc()); addCubicWithReflect(p, getCurrent(), nextVectorRel(), nextVectorInc());
break; break;
case 'S': case "S":
addCubicWithReflect(p, getCurrent(), nextVector(), nextVector()); addCubicWithReflect(p, getCurrent(), nextVector(), nextVector());
break; break;
case 't': case "t":
addQuadraticWithReflect(p, getCurrent(), nextVectorInc()); addQuadraticWithReflect(p, getCurrent(), nextVectorInc());
break; break;
case 'T': case "T":
addQuadraticWithReflect(p, getCurrent(), nextVector()); addQuadraticWithReflect(p, getCurrent(), nextVector());
break; break;
case 'a': case "a":
addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc()); addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVectorInc());
clearControl(); clearControl();
break; break;
case 'A': case "A":
addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector()); addArc(p, getCurrent(), nextValue(), nextValue(), nextValue(), nextValue() != 0, nextValue() != 0, nextVector());
clearControl(); clearControl();
break; break;
case 'Z': case "Z":
case 'z': case "z":
closedPending = true; closedPending = true;
if (polyStart != null) { if (polyStart != null) {
x = polyStart.getXFloat(); x = polyStart.getXFloat();
@ -221,6 +157,9 @@ public class PolygonParser {
if (closedPending) if (closedPending)
p.setClosed(true); p.setClosed(true);
return p; return p;
} catch (SVGTokenizer.TokenizerException e) {
throw new ParserException("error parsing a path", e);
}
} }
private VectorFloat setPolyStart(VectorFloat v) { private VectorFloat setPolyStart(VectorFloat v) {
@ -408,6 +347,10 @@ public class PolygonParser {
private ParserException(String message) { private ParserException(String message) {
super(message); super(message);
} }
private ParserException(String message, Exception cause) {
super(message, cause);
}
} }
/** /**
@ -417,7 +360,11 @@ public class PolygonParser {
* @throws ParserException ParserException * @throws ParserException ParserException
*/ */
public Polygon parsePolygon() throws ParserException { public Polygon parsePolygon() throws ParserException {
try {
return parsePolygonPolyline(true); 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 * @throws ParserException ParserException
*/ */
public Polygon parsePolyline() throws ParserException { public Polygon parsePolyline() throws ParserException {
try {
return parsePolygonPolyline(false); 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); Polygon p = new Polygon(closed);
while (next() != Token.EOF) while (!t.isEOF())
p.add(new VectorFloat(value, nextValue())); p.add(new VectorFloat(nextValue(), nextValue()));
return p; return p;
} }

View File

@ -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);
}
}
}

View File

@ -5,10 +5,7 @@
*/ */
package de.neemann.digital.draw.shapes.custom.svg; package de.neemann.digital.draw.shapes.custom.svg;
import de.neemann.digital.draw.graphics.Orientation; import de.neemann.digital.draw.graphics.*;
import de.neemann.digital.draw.graphics.Transform;
import de.neemann.digital.draw.graphics.VectorFloat;
import de.neemann.digital.draw.graphics.VectorInterface;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node; import org.w3c.dom.Node;
@ -21,7 +18,6 @@ class Context {
private static final HashMap<String, AttrParser> PARSER = new HashMap<>(); private static final HashMap<String, AttrParser> PARSER = new HashMap<>();
static { static {
PARSER.put("transform", (c1, value1) -> c1.tr = Transform.mul(new TransformParser(value1).parse(), c1.tr)); PARSER.put("transform", (c1, value1) -> c1.tr = Transform.mul(new TransformParser(value1).parse(), c1.tr));
PARSER.put("fill", (c, value) -> c.fill = getColorFromString(value)); PARSER.put("fill", (c, value) -> c.fill = getColorFromString(value));
@ -82,16 +78,18 @@ class Context {
} }
static Context readStyle(Context context, String style) throws SvgException { static Context readStyle(Context context, String style) throws SvgException {
StringTokenizer st = new StringTokenizer(style, ";"); SVGTokenizer t = new SVGTokenizer(style);
while (st.hasMoreTokens()) { try {
String[] t = st.nextToken().split(":"); while (!t.isEOF()) {
if (t.length == 2) { String command = t.readTo(':');
AttrParser p = PARSER.get(t[0].trim()); AttrParser p = PARSER.get(command);
if (p != null) if (p != null)
p.parse(context, t[1].trim()); p.parse(context, t.readTo(';'));
}
} }
return context; return context;
} catch (SVGTokenizer.TokenizerException e) {
throw new SvgException("invalid svg style: " + style, e);
}
} }
Transform getTransform() { Transform getTransform() {
@ -155,16 +153,15 @@ class Context {
} }
void addClasses(String classes) { void addClasses(String classes) {
classes = classes.trim(); SVGTokenizer t = new SVGTokenizer(classes);
while (classes.startsWith(".")) { try {
int p1 = classes.indexOf("{"); while (t.nextIsChar('.')) {
int p2 = classes.indexOf("}"); String key=t.readTo('{');
if (p1 < 0 || p2 < 0) String val = t.readTo('}');
return;
String key = classes.substring(1, p1);
String val = classes.substring(p1 + 1, p2);
classesMap.put(key, val); classesMap.put(key, val);
classes = classes.substring(p2 + 1).trim(); }
} catch (SVGTokenizer.TokenizerException e) {
// ignore errors
} }
} }
@ -187,31 +184,39 @@ class Context {
} }
private static Color getColorFromString(String v) { private static Color getColorFromString(String v) {
if (v.equalsIgnoreCase("none"))
return null;
try { try {
if (v.startsWith("#")) { SVGTokenizer t = new SVGTokenizer(v);
if (v.length() == 4) if (t.nextIsChar('#')) {
return new Color(sRGB(v.charAt(1)), sRGB(v.charAt(2)), sRGB(v.charAt(3))); String c = t.remaining();
if (c.length() == 3)
return new Color(sRGB(c.charAt(0)), sRGB(c.charAt(1)), sRGB(c.charAt(2)));
else else
return Color.decode(v); return Color.decode(v);
} else if (v.startsWith("rgb(")) { } else {
StringTokenizer st = new StringTokenizer(v.substring(4), " ,)"); final String command = t.readCommand();
return new Color(rgb(st.nextToken()), rgb(st.nextToken()), rgb(st.nextToken())); 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 (Exception e) {
} catch (RuntimeException | IllegalAccessException | NoSuchFieldException e) {
return Color.BLACK; return Color.BLACK;
} }
} }
private static int rgb(String s) { private static int rgb(SVGTokenizer t) throws SVGTokenizer.TokenizerException {
if (s.endsWith("%")) float v = t.readFloat();
return (int) (Float.parseFloat(s.substring(0, s.length() - 1)) / 100 * 255); if (t.nextIsChar('%'))
return (int) (v * 2.55f);
else else
return Integer.parseInt(s); return (int) v;
} }
private static int sRGB(char c) { private static int sRGB(char c) {
@ -220,10 +225,11 @@ class Context {
} }
private static float getFloatFromString(String inp) { private static float getFloatFromString(String inp) {
inp = inp.replaceAll("[^0-9.]", ""); try {
if (inp.isEmpty())
return 1;
return Float.parseFloat(inp); return Float.parseFloat(inp);
} catch (NumberFormatException e) {
return 1;
}
} }
} }

View File

@ -5,24 +5,14 @@
*/ */
package de.neemann.digital.draw.shapes.custom.svg; package de.neemann.digital.draw.shapes.custom.svg;
import de.neemann.digital.draw.graphics.Transform; import de.neemann.digital.draw.graphics.*;
import de.neemann.digital.draw.graphics.TransformMatrix;
import de.neemann.digital.draw.graphics.TransformTranslate;
import de.neemann.digital.draw.graphics.VectorFloat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
class TransformParser { class TransformParser {
private static final Logger LOGGER = LoggerFactory.getLogger(TransformParser.class); private static final Logger LOGGER = LoggerFactory.getLogger(TransformParser.class);
enum Token {EOF, COMMAND, NUMBER, CHAR} private SVGTokenizer tok;
private final String transform;
private int lastTokenPos;
private int pos;
private StringBuilder command;
private float value;
private char character;
/** /**
* Creates a new instance * Creates a new instance
@ -30,94 +20,7 @@ class TransformParser {
* @param transform the path to parse * @param transform the path to parse
*/ */
TransformParser(String transform) { TransformParser(String transform) {
this.transform = transform; tok = new SVGTokenizer(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;
} }
/** /**
@ -129,36 +32,36 @@ class TransformParser {
Transform combined = Transform.IDENTITY; Transform combined = Transform.IDENTITY;
try { try {
Transform t; Transform t;
while (true) { while (!tok.isEOF()) {
final Token tok = next(); final String command = tok.readCommand();
if (tok == Token.EOF) switch (command) {
break;
if (tok != Token.COMMAND)
throw new RuntimeException("invalid transform", null);
switch (getCommand()) {
case "translate": case "translate":
expect('('); tok.expect('(');
final float x = readFloat(); final float x = tok.readFloat();
float y = readOptionalFloat(0); float y = 0;
expect(')'); if (tok.nextIsNumber())
y = tok.readFloat();
tok.expect(')');
t = new TransformTranslate(new VectorFloat(x, y)); t = new TransformTranslate(new VectorFloat(x, y));
break; break;
case "scale": case "scale":
expect('('); tok.expect('(');
final float xs = readFloat(); final float xs = tok.readFloat();
float ys = readOptionalFloat(xs); float ys = xs;
expect(')'); if (tok.nextIsNumber())
ys = tok.readFloat();
tok.expect(')');
t = new TransformMatrix(xs, 0, 0, ys, 0, 0); t = new TransformMatrix(xs, 0, 0, ys, 0, 0);
break; break;
case "matrix": case "matrix":
expect('('); tok.expect('(');
final float ma = readFloat(); final float ma = tok.readFloat();
final float mb = readFloat(); final float mb = tok.readFloat();
final float mc = readFloat(); final float mc = tok.readFloat();
final float md = readFloat(); final float md = tok.readFloat();
final float mx = readFloat(); final float mx = tok.readFloat();
final float my = readFloat(); final float my = tok.readFloat();
expect(')'); tok.expect(')');
t = new TransformMatrix( t = new TransformMatrix(
ma, ma,
mc, mc,
@ -168,27 +71,25 @@ class TransformParser {
my); my);
break; break;
case "rotate": case "rotate":
expect('('); tok.expect('(');
float w = readFloat(); float w = tok.readFloat();
if (next() == Token.NUMBER) { if (tok.nextIsNumber()) {
t = TransformMatrix.rotate(w); t = TransformMatrix.rotate(w);
float xc = getValue(); float xc = tok.readFloat();
float yc = readFloat(); float yc = tok.readFloat();
t = Transform.mul(new TransformTranslate(-xc, -yc), t); t = Transform.mul(new TransformTranslate(-xc, -yc), t);
t = Transform.mul(t, new TransformTranslate(xc, yc)); t = Transform.mul(t, new TransformTranslate(xc, yc));
} else { } else
unreadToken();
t = TransformMatrix.rotate(w); t = TransformMatrix.rotate(w);
} tok.expect(')');
expect(')');
break; break;
default: default:
throw new RuntimeException("unknown transform: " + value, null); throw new RuntimeException("unknown transform: " + command, null);
} }
combined = Transform.mul(t, combined); combined = Transform.mul(t, combined);
} }
} catch (RuntimeException e) { } catch (Exception e) {
LOGGER.warn(transform + ": " + e.getMessage()); LOGGER.warn(tok + ": " + e.getMessage());
} }
return combined; return combined;
} }

View File

@ -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());
}
}

View File

@ -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());
}
}