mirror of
https://github.com/hneemann/Digital.git
synced 2025-09-16 08:25:09 -04:00
refactoring of SVG tokenizing
This commit is contained in:
parent
53c15330d7
commit
0e09c9fb28
@ -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;
|
||||
}
|
||||
|
||||
|
250
src/main/java/de/neemann/digital/draw/graphics/SVGTokenizer.java
Normal file
250
src/main/java/de/neemann/digital/draw/graphics/SVGTokenizer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<String, AttrParser> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user