From 7c37f8f5ed5c1f596acbdcdbd10fb8e9440bc6a5 Mon Sep 17 00:00:00 2001 From: Peter Osterlund Date: Wed, 6 Jun 2012 19:50:45 +0000 Subject: [PATCH] DroidFish: Added svg-android code for SVG support. See http://code.google.com/p/svg-android/. --- .../larvalabs/svgandroid/ParserHelper.java | 306 ++++ .../src/com/larvalabs/svgandroid/SVG.java | 120 ++ .../svgandroid/SVGParseException.java | 21 + .../com/larvalabs/svgandroid/SVGParser.java | 1247 +++++++++++++++++ .../larvalabs/svgandroid/package-info.java | 26 + 5 files changed, 1720 insertions(+) create mode 100644 DroidFish/src/com/larvalabs/svgandroid/ParserHelper.java create mode 100644 DroidFish/src/com/larvalabs/svgandroid/SVG.java create mode 100644 DroidFish/src/com/larvalabs/svgandroid/SVGParseException.java create mode 100644 DroidFish/src/com/larvalabs/svgandroid/SVGParser.java create mode 100644 DroidFish/src/com/larvalabs/svgandroid/package-info.java diff --git a/DroidFish/src/com/larvalabs/svgandroid/ParserHelper.java b/DroidFish/src/com/larvalabs/svgandroid/ParserHelper.java new file mode 100644 index 0000000..10a7d63 --- /dev/null +++ b/DroidFish/src/com/larvalabs/svgandroid/ParserHelper.java @@ -0,0 +1,306 @@ +package com.larvalabs.svgandroid; + +/* + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +/** + * Parses numbers from SVG text. Based on the Batik Number Parser (Apache 2 License). + * + * @author Apache Software Foundation, Larva Labs LLC + */ +public class ParserHelper { + + private char current; + private CharSequence s; + public int pos; + private int n; + + public ParserHelper(CharSequence s, int pos) { + this.s = s; + this.pos = pos; + n = s.length(); + current = s.charAt(pos); + } + + private char read() { + if (pos < n) { + pos++; + } + if (pos == n) { + return '\0'; + } else { + return s.charAt(pos); + } + } + + public void skipWhitespace() { + while (pos < n) { + if (Character.isWhitespace(s.charAt(pos))) { + advance(); + } else { + break; + } + } + } + + public void skipNumberSeparator() { + while (pos < n) { + char c = s.charAt(pos); + switch (c) { + case ' ': + case ',': + case '\n': + case '\t': + advance(); + break; + default: + return; + } + } + } + + public void advance() { + current = read(); + } + + /** + * Parses the content of the buffer and converts it to a float. + */ + public float parseFloat() { + int mant = 0; + int mantDig = 0; + boolean mantPos = true; + boolean mantRead = false; + + int exp = 0; + int expDig = 0; + int expAdj = 0; + boolean expPos = true; + + switch (current) { + case '-': + mantPos = false; + // fallthrough + case '+': + current = read(); + } + + m1: switch (current) { + default: + return Float.NaN; + + case '.': + break; + + case '0': + mantRead = true; + l: for (;;) { + current = read(); + switch (current) { + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + break l; + case '.': case 'e': case 'E': + break m1; + default: + return 0.0f; + case '0': + } + } + + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + mantRead = true; + l: for (;;) { + if (mantDig < 9) { + mantDig++; + mant = mant * 10 + (current - '0'); + } else { + expAdj++; + } + current = read(); + switch (current) { + default: + break l; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + } + } + } + + if (current == '.') { + current = read(); + m2: switch (current) { + default: + case 'e': case 'E': + if (!mantRead) { + reportUnexpectedCharacterError( current ); + return 0.0f; + } + break; + + case '0': + if (mantDig == 0) { + l: for (;;) { + current = read(); + expAdj--; + switch (current) { + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + break l; + default: + if (!mantRead) { + return 0.0f; + } + break m2; + case '0': + } + } + } + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + l: for (;;) { + if (mantDig < 9) { + mantDig++; + mant = mant * 10 + (current - '0'); + expAdj--; + } + current = read(); + switch (current) { + default: + break l; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + } + } + } + } + + switch (current) { + case 'e': case 'E': + current = read(); + switch (current) { + default: + reportUnexpectedCharacterError( current ); + return 0f; + case '-': + expPos = false; + case '+': + current = read(); + switch (current) { + default: + reportUnexpectedCharacterError( current ); + return 0f; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + } + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + } + + en: switch (current) { + case '0': + l: for (;;) { + current = read(); + switch (current) { + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + break l; + default: + break en; + case '0': + } + } + + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + l: for (;;) { + if (expDig < 3) { + expDig++; + exp = exp * 10 + (current - '0'); + } + current = read(); + switch (current) { + default: + break l; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + } + } + } + default: + } + + if (!expPos) { + exp = -exp; + } + exp += expAdj; + if (!mantPos) { + mant = -mant; + } + + return buildFloat(mant, exp); + } + + private void reportUnexpectedCharacterError(char c) { + throw new RuntimeException("Unexpected char '" + c + "'."); + } + + /** + * Computes a float from mantissa and exponent. + */ + public static float buildFloat(int mant, int exp) { + if (exp < -125 || mant == 0) { + return 0.0f; + } + + if (exp >= 128) { + return (mant > 0) + ? Float.POSITIVE_INFINITY + : Float.NEGATIVE_INFINITY; + } + + if (exp == 0) { + return mant; + } + + if (mant >= (1 << 26)) { + mant++; // round up trailing bits if they will be dropped. + } + + return (float) ((exp > 0) ? mant * pow10[exp] : mant / pow10[-exp]); + } + + /** + * Array of powers of ten. Using double instead of float gives a tiny bit more precision. + */ + private static final double[] pow10 = new double[128]; + + static { + for (int i = 0; i < pow10.length; i++) { + pow10[i] = Math.pow(10, i); + } + } + + public float nextFloat() { + skipWhitespace(); + float f = parseFloat(); + skipNumberSeparator(); + return f; + } +} diff --git a/DroidFish/src/com/larvalabs/svgandroid/SVG.java b/DroidFish/src/com/larvalabs/svgandroid/SVG.java new file mode 100644 index 0000000..23b149d --- /dev/null +++ b/DroidFish/src/com/larvalabs/svgandroid/SVG.java @@ -0,0 +1,120 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Picture; +import android.graphics.RectF; +import android.graphics.drawable.PictureDrawable; + +/* + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +/** + * Describes a vector Picture object, and optionally its bounds. + * + * @author Larva Labs, LLC + */ +public class SVG { + + /** + * The parsed Picture object. + */ + private Picture picture; + + /** + * These are the bounds for the SVG specified as a hidden "bounds" layer in the SVG. + */ + private RectF bounds; + + /** + * These are the estimated bounds of the SVG computed from the SVG elements while parsing. + * Note that this could be null if there was a failure to compute limits (ie. an empty SVG). + */ + private RectF limits = null; + + /** + * Construct a new SVG. + * @param picture the parsed picture object. + * @param bounds the bounds computed from the "bounds" layer in the SVG. + */ + SVG(Picture picture, RectF bounds) { + this.picture = picture; + this.bounds = bounds; + } + + /** + * Set the limits of the SVG, which are the estimated bounds computed by the parser. + * @param limits the bounds computed while parsing the SVG, may not be entirely accurate. + */ + void setLimits(RectF limits) { + this.limits = limits; + } + + /** + * Create a picture drawable from the SVG. + * @return the PictureDrawable. + */ + public PictureDrawable createPictureDrawable() { + return new PictureDrawable(picture); +// return new PictureDrawable(picture) { +// @Override +// public int getIntrinsicWidth() { +// if (bounds != null) { +// return (int) bounds.width(); +// } else if (limits != null) { +// return (int) limits.width(); +// } else { +// return -1; +// } +// } +// +// @Override +// public int getIntrinsicHeight() { +// if (bounds != null) { +// return (int) bounds.height(); +// } else if (limits != null) { +// return (int) limits.height(); +// } else { +// return -1; +// } +// } +// }; + } + + /** + * Get the parsed SVG picture data. + * @return the picture. + */ + public Picture getPicture() { + return picture; + } + + /** + * Gets the bounding rectangle for the SVG, if one was specified. + * @return rectangle representing the bounds. + */ + public RectF getBounds() { + return bounds; + } + + /** + * Gets the bounding rectangle for the SVG that was computed upon parsing. It may not be entirely accurate for certain curves or transformations, but is often better than nothing. + * @return rectangle representing the computed bounds. + */ + public RectF getLimits() { + return limits; + } +} diff --git a/DroidFish/src/com/larvalabs/svgandroid/SVGParseException.java b/DroidFish/src/com/larvalabs/svgandroid/SVGParseException.java new file mode 100644 index 0000000..2b64298 --- /dev/null +++ b/DroidFish/src/com/larvalabs/svgandroid/SVGParseException.java @@ -0,0 +1,21 @@ +package com.larvalabs.svgandroid; + +/** + * Runtime exception thrown when there is a problem parsing an SVG. + * + * @author Larva Labs, LLC + */ +public class SVGParseException extends RuntimeException { + + public SVGParseException(String s) { + super(s); + } + + public SVGParseException(String s, Throwable throwable) { + super(s, throwable); + } + + public SVGParseException(Throwable throwable) { + super(throwable); + } +} diff --git a/DroidFish/src/com/larvalabs/svgandroid/SVGParser.java b/DroidFish/src/com/larvalabs/svgandroid/SVGParser.java new file mode 100644 index 0000000..1c24d12 --- /dev/null +++ b/DroidFish/src/com/larvalabs/svgandroid/SVGParser.java @@ -0,0 +1,1247 @@ +package com.larvalabs.svgandroid; + +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.*; +import android.graphics.drawable.PictureDrawable; +import android.util.Log; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; + +/* + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/** + * Entry point for parsing SVG files for Android. + * Use one of the various static methods for parsing SVGs by resource, asset or input stream. + * Optionally, a single color can be searched and replaced in the SVG while parsing. + * You can also parse an svg path directly. + * + * @author Larva Labs, LLC + * @see #getSVGFromResource(android.content.res.Resources, int) + * @see #getSVGFromAsset(android.content.res.AssetManager, String) + * @see #getSVGFromString(String) + * @see #getSVGFromInputStream(java.io.InputStream) + * @see #parsePath(String) + */ +public class SVGParser { + + static final String TAG = "SVGAndroid"; + + /** + * Parse SVG data from an input stream. + * + * @param svgData the input stream, with SVG XML data in UTF-8 character encoding. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromInputStream(InputStream svgData) throws SVGParseException { + return SVGParser.parse(svgData, 0, 0, false); + } + + /** + * Parse SVG data from a string. + * + * @param svgData the string containing SVG XML data. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromString(String svgData) throws SVGParseException { + return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), 0, 0, false); + } + + /** + * Parse SVG data from an Android application resource. + * + * @param resources the Android context resources. + * @param resId the ID of the raw resource SVG. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromResource(Resources resources, int resId) throws SVGParseException { + return SVGParser.parse(resources.openRawResource(resId), 0, 0, false); + } + + /** + * Parse SVG data from an Android application asset. + * + * @param assetMngr the Android asset manager. + * @param svgPath the path to the SVG file in the application's assets. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + * @throws IOException if there was a problem reading the file. + */ + public static SVG getSVGFromAsset(AssetManager assetMngr, String svgPath) throws SVGParseException, IOException { + InputStream inputStream = assetMngr.open(svgPath); + SVG svg = getSVGFromInputStream(inputStream); + inputStream.close(); + return svg; + } + + /** + * Parse SVG data from an input stream, replacing a single color with another color. + * + * @param svgData the input stream, with SVG XML data in UTF-8 character encoding. + * @param searchColor the color in the SVG to replace. + * @param replaceColor the color with which to replace the search color. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromInputStream(InputStream svgData, int searchColor, int replaceColor) throws SVGParseException { + return SVGParser.parse(svgData, searchColor, replaceColor, false); + } + + /** + * Parse SVG data from a string. + * + * @param svgData the string containing SVG XML data. + * @param searchColor the color in the SVG to replace. + * @param replaceColor the color with which to replace the search color. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromString(String svgData, int searchColor, int replaceColor) throws SVGParseException { + return SVGParser.parse(new ByteArrayInputStream(svgData.getBytes()), searchColor, replaceColor, false); + } + + /** + * Parse SVG data from an Android application resource. + * + * @param resources the Android context + * @param resId the ID of the raw resource SVG. + * @param searchColor the color in the SVG to replace. + * @param replaceColor the color with which to replace the search color. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + */ + public static SVG getSVGFromResource(Resources resources, int resId, int searchColor, int replaceColor) throws SVGParseException { + return SVGParser.parse(resources.openRawResource(resId), searchColor, replaceColor, false); + } + + /** + * Parse SVG data from an Android application asset. + * + * @param assetMngr the Android asset manager. + * @param svgPath the path to the SVG file in the application's assets. + * @param searchColor the color in the SVG to replace. + * @param replaceColor the color with which to replace the search color. + * @return the parsed SVG. + * @throws SVGParseException if there is an error while parsing. + * @throws IOException if there was a problem reading the file. + */ + public static SVG getSVGFromAsset(AssetManager assetMngr, String svgPath, int searchColor, int replaceColor) throws SVGParseException, IOException { + InputStream inputStream = assetMngr.open(svgPath); + SVG svg = getSVGFromInputStream(inputStream, searchColor, replaceColor); + inputStream.close(); + return svg; + } + + /** + * Parses a single SVG path and returns it as a android.graphics.Path object. + * An example path is M250,150L150,350L350,350Z, which draws a triangle. + * + * @param pathString the SVG path, see the specification here. + */ + public static Path parsePath(String pathString) { + return doPath(pathString); + } + + private static SVG parse(InputStream in, Integer searchColor, Integer replaceColor, boolean whiteMode) throws SVGParseException { +// Util.debug("Parsing SVG..."); + try { + long start = System.currentTimeMillis(); + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + final Picture picture = new Picture(); + SVGHandler handler = new SVGHandler(picture); + handler.setColorSwap(searchColor, replaceColor); + handler.setWhiteMode(whiteMode); + xr.setContentHandler(handler); + xr.parse(new InputSource(in)); +// Util.debug("Parsing complete in " + (System.currentTimeMillis() - start) + " millis."); + SVG result = new SVG(picture, handler.bounds); + // Skip bounds if it was an empty pic + if (!Float.isInfinite(handler.limits.top)) { + result.setLimits(handler.limits); + } + return result; + } catch (Exception e) { + throw new SVGParseException(e); + } + } + + private static NumberParse parseNumbers(String s) { + //Util.debug("Parsing numbers from: '" + s + "'"); + int n = s.length(); + int p = 0; + ArrayList numbers = new ArrayList(); + boolean skipChar = false; + for (int i = 1; i < n; i++) { + if (skipChar) { + skipChar = false; + continue; + } + char c = s.charAt(i); + switch (c) { + // This ends the parsing, as we are on the next element + case 'M': + case 'm': + case 'Z': + case 'z': + case 'L': + case 'l': + case 'H': + case 'h': + case 'V': + case 'v': + case 'C': + case 'c': + case 'S': + case 's': + case 'Q': + case 'q': + case 'T': + case 't': + case 'a': + case 'A': + case ')': { + String str = s.substring(p, i); + if (str.trim().length() > 0) { + //Util.debug(" Last: " + str); + Float f = Float.parseFloat(str); + numbers.add(f); + } + p = i; + return new NumberParse(numbers, p); + } + case '\n': + case '\t': + case ' ': + case ',': + case '-': { + String str = s.substring(p, i); + // Just keep moving if multiple whitespace + if (str.trim().length() > 0) { + //Util.debug(" Next: " + str); + Float f = Float.parseFloat(str); + numbers.add(f); + if (c == '-') { + p = i; + } else { + p = i + 1; + skipChar = true; + } + } else { + p++; + } + break; + } + } + } + String last = s.substring(p); + if (last.length() > 0) { + //Util.debug(" Last: " + last); + try { + numbers.add(Float.parseFloat(last)); + } catch (NumberFormatException nfe) { + // Just white-space, forget it + } + p = s.length(); + } + return new NumberParse(numbers, p); + } + + private static Matrix parseTransform(String s) { + if (s.startsWith("matrix(")) { + NumberParse np = parseNumbers(s.substring("matrix(".length())); + if (np.numbers.size() == 6) { + Matrix matrix = new Matrix(); + matrix.setValues(new float[]{ + // Row 1 + np.numbers.get(0), + np.numbers.get(2), + np.numbers.get(4), + // Row 2 + np.numbers.get(1), + np.numbers.get(3), + np.numbers.get(5), + // Row 3 + 0, + 0, + 1, + }); + return matrix; + } + } else if (s.startsWith("translate(")) { + NumberParse np = parseNumbers(s.substring("translate(".length())); + if (np.numbers.size() > 0) { + float tx = np.numbers.get(0); + float ty = 0; + if (np.numbers.size() > 1) { + ty = np.numbers.get(1); + } + Matrix matrix = new Matrix(); + matrix.postTranslate(tx, ty); + return matrix; + } + } else if (s.startsWith("scale(")) { + NumberParse np = parseNumbers(s.substring("scale(".length())); + if (np.numbers.size() > 0) { + float sx = np.numbers.get(0); + float sy = 0; + if (np.numbers.size() > 1) { + sy = np.numbers.get(1); + } + Matrix matrix = new Matrix(); + matrix.postScale(sx, sy); + return matrix; + } + } else if (s.startsWith("skewX(")) { + NumberParse np = parseNumbers(s.substring("skewX(".length())); + if (np.numbers.size() > 0) { + float angle = np.numbers.get(0); + Matrix matrix = new Matrix(); + matrix.postSkew((float) Math.tan(angle), 0); + return matrix; + } + } else if (s.startsWith("skewY(")) { + NumberParse np = parseNumbers(s.substring("skewY(".length())); + if (np.numbers.size() > 0) { + float angle = np.numbers.get(0); + Matrix matrix = new Matrix(); + matrix.postSkew(0, (float) Math.tan(angle)); + return matrix; + } + } else if (s.startsWith("rotate(")) { + NumberParse np = parseNumbers(s.substring("rotate(".length())); + if (np.numbers.size() > 0) { + float angle = np.numbers.get(0); + float cx = 0; + float cy = 0; + if (np.numbers.size() > 2) { + cx = np.numbers.get(1); + cy = np.numbers.get(2); + } + Matrix matrix = new Matrix(); + matrix.postTranslate(cx, cy); + matrix.postRotate(angle); + matrix.postTranslate(-cx, -cy); + return matrix; + } + } + return null; + } + + /** + * This is where the hard-to-parse paths are handled. + * Uppercase rules are absolute positions, lowercase are relative. + * Types of path rules: + *

+ *

    + *
  1. M/m - (x y)+ - Move to (without drawing) + *
  2. Z/z - (no params) - Close path (back to starting point) + *
  3. L/l - (x y)+ - Line to + *
  4. H/h - x+ - Horizontal ine to + *
  5. V/v - y+ - Vertical line to + *
  6. C/c - (x1 y1 x2 y2 x y)+ - Cubic bezier to + *
  7. S/s - (x2 y2 x y)+ - Smooth cubic bezier to (shorthand that assumes the x2, y2 from previous C/S is the x1, y1 of this bezier) + *
  8. Q/q - (x1 y1 x y)+ - Quadratic bezier to + *
  9. T/t - (x y)+ - Smooth quadratic bezier to (assumes previous control point is "reflection" of last one w.r.t. to current point) + *
+ *

+ * Numbers are separate by whitespace, comma or nothing at all (!) if they are self-delimiting, (ie. begin with a - sign) + * + * @param s the path string from the XML + */ + private static Path doPath(String s) { + int n = s.length(); + ParserHelper ph = new ParserHelper(s, 0); + ph.skipWhitespace(); + Path p = new Path(); + float lastX = 0; + float lastY = 0; + float lastX1 = 0; + float lastY1 = 0; + float subPathStartX = 0; + float subPathStartY = 0; + char prevCmd = 0; + while (ph.pos < n) { + char cmd = s.charAt(ph.pos); + switch (cmd) { + case '-': + case '+': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (prevCmd == 'm' || prevCmd == 'M') { + cmd = (char) (((int) prevCmd) - 1); + break; + } else if (prevCmd == 'c' || prevCmd == 'C') { + cmd = prevCmd; + break; + } else if (prevCmd == 'l' || prevCmd == 'L') { + cmd = prevCmd; + break; + } + default: { + ph.advance(); + prevCmd = cmd; + } + } + + boolean wasCurve = false; + switch (cmd) { + case 'M': + case 'm': { + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (cmd == 'm') { + subPathStartX += x; + subPathStartY += y; + p.rMoveTo(x, y); + lastX += x; + lastY += y; + } else { + subPathStartX = x; + subPathStartY = y; + p.moveTo(x, y); + lastX = x; + lastY = y; + } + break; + } + case 'Z': + case 'z': { + p.close(); + p.moveTo(subPathStartX, subPathStartY); + lastX = subPathStartX; + lastY = subPathStartY; + lastX1 = subPathStartX; + lastY1 = subPathStartY; + wasCurve = true; + break; + } + case 'L': + case 'l': { + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (cmd == 'l') { + p.rLineTo(x, y); + lastX += x; + lastY += y; + } else { + p.lineTo(x, y); + lastX = x; + lastY = y; + } + break; + } + case 'H': + case 'h': { + float x = ph.nextFloat(); + if (cmd == 'h') { + p.rLineTo(x, 0); + lastX += x; + } else { + p.lineTo(x, lastY); + lastX = x; + } + break; + } + case 'V': + case 'v': { + float y = ph.nextFloat(); + if (cmd == 'v') { + p.rLineTo(0, y); + lastY += y; + } else { + p.lineTo(lastX, y); + lastY = y; + } + break; + } + case 'C': + case 'c': { + wasCurve = true; + float x1 = ph.nextFloat(); + float y1 = ph.nextFloat(); + float x2 = ph.nextFloat(); + float y2 = ph.nextFloat(); + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (cmd == 'c') { + x1 += lastX; + x2 += lastX; + x += lastX; + y1 += lastY; + y2 += lastY; + y += lastY; + } + p.cubicTo(x1, y1, x2, y2, x, y); + lastX1 = x2; + lastY1 = y2; + lastX = x; + lastY = y; + break; + } + case 'S': + case 's': { + wasCurve = true; + float x2 = ph.nextFloat(); + float y2 = ph.nextFloat(); + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (cmd == 's') { + x2 += lastX; + x += lastX; + y2 += lastY; + y += lastY; + } + float x1 = 2 * lastX - lastX1; + float y1 = 2 * lastY - lastY1; + p.cubicTo(x1, y1, x2, y2, x, y); + lastX1 = x2; + lastY1 = y2; + lastX = x; + lastY = y; + break; + } + case 'A': + case 'a': { + float rx = ph.nextFloat(); + float ry = ph.nextFloat(); + float theta = ph.nextFloat(); + int largeArc = (int) ph.nextFloat(); + int sweepArc = (int) ph.nextFloat(); + float x = ph.nextFloat(); + float y = ph.nextFloat(); + drawArc(p, lastX, lastY, x, y, rx, ry, theta, largeArc, sweepArc); + lastX = x; + lastY = y; + break; + } + } + if (!wasCurve) { + lastX1 = lastX; + lastY1 = lastY; + } + ph.skipWhitespace(); + } + return p; + } + + private static void drawArc(Path p, float lastX, float lastY, float x, float y, float rx, float ry, float theta, int largeArc, int sweepArc) { + // todo - not implemented yet, may be very hard to do using Android drawing facilities. + } + + private static NumberParse getNumberParseAttr(String name, Attributes attributes) { + int n = attributes.getLength(); + for (int i = 0; i < n; i++) { + if (attributes.getLocalName(i).equals(name)) { + return parseNumbers(attributes.getValue(i)); + } + } + return null; + } + + private static String getStringAttr(String name, Attributes attributes) { + int n = attributes.getLength(); + for (int i = 0; i < n; i++) { + if (attributes.getLocalName(i).equals(name)) { + return attributes.getValue(i); + } + } + return null; + } + + private static Float getFloatAttr(String name, Attributes attributes) { + return getFloatAttr(name, attributes, null); + } + + private static Float getFloatAttr(String name, Attributes attributes, Float defaultValue) { + String v = getStringAttr(name, attributes); + if (v == null) { + return defaultValue; + } else { + if (v.endsWith("px")) { + v = v.substring(0, v.length() - 2); + } +// Log.d(TAG, "Float parsing '" + name + "=" + v + "'"); + return Float.parseFloat(v); + } + } + + private static Integer getHexAttr(String name, Attributes attributes) { + String v = getStringAttr(name, attributes); + //Util.debug("Hex parsing '" + name + "=" + v + "'"); + if (v == null) { + return null; + } else { + try { + return Integer.parseInt(v.substring(1), 16); + } catch (NumberFormatException nfe) { + // todo - parse word-based color here + return null; + } + } + } + + private static class NumberParse { + private ArrayList numbers; + private int nextCmd; + + public NumberParse(ArrayList numbers, int nextCmd) { + this.numbers = numbers; + this.nextCmd = nextCmd; + } + + public int getNextCmd() { + return nextCmd; + } + + public float getNumber(int index) { + return numbers.get(index); + } + + } + + private static class Gradient { + String id; + String xlink; + boolean isLinear; + float x1, y1, x2, y2; + float x, y, radius; + ArrayList positions = new ArrayList(); + ArrayList colors = new ArrayList(); + Matrix matrix = null; + + public Gradient createChild(Gradient g) { + Gradient child = new Gradient(); + child.id = g.id; + child.xlink = id; + child.isLinear = g.isLinear; + child.x1 = g.x1; + child.x2 = g.x2; + child.y1 = g.y1; + child.y2 = g.y2; + child.x = g.x; + child.y = g.y; + child.radius = g.radius; + child.positions = positions; + child.colors = colors; + child.matrix = matrix; + if (g.matrix != null) { + if (matrix == null) { + child.matrix = g.matrix; + } else { + Matrix m = new Matrix(matrix); + m.preConcat(g.matrix); + child.matrix = m; + } + } + return child; + } + } + + private static class StyleSet { + HashMap styleMap = new HashMap(); + + private StyleSet(String string) { + String[] styles = string.split(";"); + for (String s : styles) { + String[] style = s.split(":"); + if (style.length == 2) { + styleMap.put(style[0], style[1]); + } + } + } + + public String getStyle(String name) { + return styleMap.get(name); + } + } + + private static class Properties { + StyleSet styles = null; + Attributes atts; + + private Properties(Attributes atts) { + this.atts = atts; + String styleAttr = getStringAttr("style", atts); + if (styleAttr != null) { + styles = new StyleSet(styleAttr); + } + } + + public String getAttr(String name) { + String v = null; + if (styles != null) { + v = styles.getStyle(name); + } + if (v == null) { + v = getStringAttr(name, atts); + } + return v; + } + + public String getString(String name) { + return getAttr(name); + } + + public Integer getHex(String name) { + String v = getAttr(name); + if (v == null || !v.startsWith("#")) { + return null; + } else { + try { + return Integer.parseInt(v.substring(1), 16); + } catch (NumberFormatException nfe) { + // todo - parse word-based color here + return null; + } + } + } + + public Float getFloat(String name, float defaultValue) { + Float v = getFloat(name); + if (v == null) { + return defaultValue; + } else { + return v; + } + } + + public Float getFloat(String name) { + String v = getAttr(name); + if (v == null) { + return null; + } else { + try { + return Float.parseFloat(v); + } catch (NumberFormatException nfe) { + return null; + } + } + } + } + + private static class SVGHandler extends DefaultHandler { + + Picture picture; + Canvas canvas; + Paint paint; + // Scratch rect (so we aren't constantly making new ones) + RectF rect = new RectF(); + RectF bounds = null; + RectF limits = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + Integer searchColor = null; + Integer replaceColor = null; + + boolean whiteMode = false; + + HashMap gradientMap = new HashMap(); + HashMap gradientRefMap = new HashMap(); + Gradient gradient = null; + + private SVGHandler(Picture picture) { + this.picture = picture; + paint = new Paint(); + paint.setAntiAlias(true); + } + + public void setColorSwap(Integer searchColor, Integer replaceColor) { + this.searchColor = searchColor; + this.replaceColor = replaceColor; + } + + public void setWhiteMode(boolean whiteMode) { + this.whiteMode = whiteMode; + } + + @Override + public void startDocument() throws SAXException { + // Set up prior to parsing a doc + } + + @Override + public void endDocument() throws SAXException { + // Clean up after parsing a doc + } + + private boolean doFill(Properties atts, HashMap gradients) { + if ("none".equals(atts.getString("display"))) { + return false; + } + if (whiteMode) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(0xFFFFFFFF); + return true; + } + String fillString = atts.getString("fill"); + if (fillString != null && fillString.startsWith("url(#")) { + // It's a gradient fill, look it up in our map + String id = fillString.substring("url(#".length(), fillString.length() - 1); + Shader shader = gradients.get(id); + if (shader != null) { + //Util.debug("Found shader!"); + paint.setShader(shader); + paint.setStyle(Paint.Style.FILL); + return true; + } else { + //Util.debug("Didn't find shader!"); + return false; + } + } else { + paint.setShader(null); + Integer color = atts.getHex("fill"); + if (color != null) { + doColor(atts, color, true); + paint.setStyle(Paint.Style.FILL); + return true; + } else if (atts.getString("fill") == null && atts.getString("stroke") == null) { + // Default is black fill + paint.setStyle(Paint.Style.FILL); + paint.setColor(0xFF000000); + return true; + } + } + return false; + } + + private boolean doStroke(Properties atts) { + if (whiteMode) { + // Never stroke in white mode + return false; + } + if ("none".equals(atts.getString("display"))) { + return false; + } + Integer color = atts.getHex("stroke"); + if (color != null) { + doColor(atts, color, false); + // Check for other stroke attributes + Float width = atts.getFloat("stroke-width"); + // Set defaults + + if (width != null) { + paint.setStrokeWidth(width); + } + String linecap = atts.getString("stroke-linecap"); + if ("round".equals(linecap)) { + paint.setStrokeCap(Paint.Cap.ROUND); + } else if ("square".equals(linecap)) { + paint.setStrokeCap(Paint.Cap.SQUARE); + } else if ("butt".equals(linecap)) { + paint.setStrokeCap(Paint.Cap.BUTT); + } + String linejoin = atts.getString("stroke-linejoin"); + if ("miter".equals(linejoin)) { + paint.setStrokeJoin(Paint.Join.MITER); + } else if ("round".equals(linejoin)) { + paint.setStrokeJoin(Paint.Join.ROUND); + } else if ("bevel".equals(linejoin)) { + paint.setStrokeJoin(Paint.Join.BEVEL); + } + paint.setStyle(Paint.Style.STROKE); + return true; + } + return false; + } + + private Gradient doGradient(boolean isLinear, Attributes atts) { + Gradient gradient = new Gradient(); + gradient.id = getStringAttr("id", atts); + gradient.isLinear = isLinear; + if (isLinear) { + gradient.x1 = getFloatAttr("x1", atts, 0f); + gradient.x2 = getFloatAttr("x2", atts, 0f); + gradient.y1 = getFloatAttr("y1", atts, 0f); + gradient.y2 = getFloatAttr("y2", atts, 0f); + } else { + gradient.x = getFloatAttr("cx", atts, 0f); + gradient.y = getFloatAttr("cy", atts, 0f); + gradient.radius = getFloatAttr("r", atts, 0f); + } + String transform = getStringAttr("gradientTransform", atts); + if (transform != null) { + gradient.matrix = parseTransform(transform); + } + String xlink = getStringAttr("href", atts); + if (xlink != null) { + if (xlink.startsWith("#")) { + xlink = xlink.substring(1); + } + gradient.xlink = xlink; + } + return gradient; + } + + private void doColor(Properties atts, Integer color, boolean fillMode) { + int c = (0xFFFFFF & color) | 0xFF000000; + if (searchColor != null && searchColor.intValue() == c) { + c = replaceColor; + } + paint.setColor(c); + Float opacity = atts.getFloat("opacity"); + if (opacity == null) { + opacity = atts.getFloat(fillMode ? "fill-opacity" : "stroke-opacity"); + } + if (opacity == null) { + paint.setAlpha(255); + } else { + paint.setAlpha((int) (255 * opacity)); + } + } + + private boolean hidden = false; + private int hiddenLevel = 0; + private boolean boundsMode = false; + + private void doLimits(float x, float y) { + if (x < limits.left) { + limits.left = x; + } + if (x > limits.right) { + limits.right = x; + } + if (y < limits.top) { + limits.top = y; + } + if (y > limits.bottom) { + limits.bottom = y; + } + } + + private void doLimits(float x, float y, float width, float height) { + doLimits(x, y); + doLimits(x + width, y + height); + } + + private void doLimits(Path path) { + path.computeBounds(rect, false); + doLimits(rect.left, rect.top); + doLimits(rect.right, rect.bottom); + } + + private void pushTransform(Attributes atts) { + final String transform = getStringAttr("transform", atts); + canvas.save(); + if (transform != null) { + final Matrix matrix = parseTransform(transform); + canvas.concat(matrix); + } + } + + private void popTransform() { + canvas.restore(); + } + + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { + // Reset paint opacity + paint.setAlpha(255); + // Ignore everything but rectangles in bounds mode + if (boundsMode) { + if (localName.equals("rect")) { + Float x = getFloatAttr("x", atts); + if (x == null) { + x = 0f; + } + Float y = getFloatAttr("y", atts); + if (y == null) { + y = 0f; + } + Float width = getFloatAttr("width", atts); + Float height = getFloatAttr("height", atts); + bounds = new RectF(x, y, x + width, y + width); + } + return; + } + if (localName.equals("svg")) { + int width = (int) Math.ceil(getFloatAttr("width", atts)); + int height = (int) Math.ceil(getFloatAttr("height", atts)); + canvas = picture.beginRecording(width, height); + } else if (localName.equals("defs")) { + // Ignore + } else if (localName.equals("linearGradient")) { + gradient = doGradient(true, atts); + } else if (localName.equals("radialGradient")) { + gradient = doGradient(false, atts); + } else if (localName.equals("stop")) { + if (gradient != null) { + float offset = getFloatAttr("offset", atts); + String styles = getStringAttr("style", atts); + StyleSet styleSet = new StyleSet(styles); + String colorStyle = styleSet.getStyle("stop-color"); + int color = Color.BLACK; + if (colorStyle != null) { + if (colorStyle.startsWith("#")) { + color = Integer.parseInt(colorStyle.substring(1), 16); + } else { + color = Integer.parseInt(colorStyle, 16); + } + } + String opacityStyle = styleSet.getStyle("stop-opacity"); + if (opacityStyle != null) { + float alpha = Float.parseFloat(opacityStyle); + int alphaInt = Math.round(255 * alpha); + color |= (alphaInt << 24); + } else { + color |= 0xFF000000; + } + gradient.positions.add(offset); + gradient.colors.add(color); + } + } else if (localName.equals("g")) { + // Check to see if this is the "bounds" layer + if ("bounds".equalsIgnoreCase(getStringAttr("id", atts))) { + boundsMode = true; + } + if (hidden) { + hiddenLevel++; + //Util.debug("Hidden up: " + hiddenLevel); + } + // Go in to hidden mode if display is "none" + if ("none".equals(getStringAttr("display", atts))) { + if (!hidden) { + hidden = true; + hiddenLevel = 1; + //Util.debug("Hidden up: " + hiddenLevel); + } + } + pushTransform(atts); + } else if (!hidden && localName.equals("rect")) { + Float x = getFloatAttr("x", atts); + if (x == null) { + x = 0f; + } + Float y = getFloatAttr("y", atts); + if (y == null) { + y = 0f; + } + Float width = getFloatAttr("width", atts); + Float height = getFloatAttr("height", atts); + pushTransform(atts); + Properties props = new Properties(atts); + if (doFill(props, gradientMap)) { + doLimits(x, y, width, height); + canvas.drawRect(x, y, x + width, y + height, paint); + } + if (doStroke(props)) { + canvas.drawRect(x, y, x + width, y + height, paint); + } + popTransform(); + } else if (!hidden && localName.equals("line")) { + Float x1 = getFloatAttr("x1", atts); + Float x2 = getFloatAttr("x2", atts); + Float y1 = getFloatAttr("y1", atts); + Float y2 = getFloatAttr("y2", atts); + Properties props = new Properties(atts); + if (doStroke(props)) { + pushTransform(atts); + doLimits(x1, y1); + doLimits(x2, y2); + canvas.drawLine(x1, y1, x2, y2, paint); + popTransform(); + } + } else if (!hidden && localName.equals("circle")) { + Float centerX = getFloatAttr("cx", atts); + Float centerY = getFloatAttr("cy", atts); + Float radius = getFloatAttr("r", atts); + if (centerX != null && centerY != null && radius != null) { + pushTransform(atts); + Properties props = new Properties(atts); + if (doFill(props, gradientMap)) { + doLimits(centerX - radius, centerY - radius); + doLimits(centerX + radius, centerY + radius); + canvas.drawCircle(centerX, centerY, radius, paint); + } + if (doStroke(props)) { + canvas.drawCircle(centerX, centerY, radius, paint); + } + popTransform(); + } + } else if (!hidden && localName.equals("ellipse")) { + Float centerX = getFloatAttr("cx", atts); + Float centerY = getFloatAttr("cy", atts); + Float radiusX = getFloatAttr("rx", atts); + Float radiusY = getFloatAttr("ry", atts); + if (centerX != null && centerY != null && radiusX != null && radiusY != null) { + pushTransform(atts); + Properties props = new Properties(atts); + rect.set(centerX - radiusX, centerY - radiusY, centerX + radiusX, centerY + radiusY); + if (doFill(props, gradientMap)) { + doLimits(centerX - radiusX, centerY - radiusY); + doLimits(centerX + radiusX, centerY + radiusY); + canvas.drawOval(rect, paint); + } + if (doStroke(props)) { + canvas.drawOval(rect, paint); + } + popTransform(); + } + } else if (!hidden && (localName.equals("polygon") || localName.equals("polyline"))) { + NumberParse numbers = getNumberParseAttr("points", atts); + if (numbers != null) { + Path p = new Path(); + ArrayList points = numbers.numbers; + if (points.size() > 1) { + pushTransform(atts); + Properties props = new Properties(atts); + p.moveTo(points.get(0), points.get(1)); + for (int i = 2; i < points.size(); i += 2) { + float x = points.get(i); + float y = points.get(i + 1); + p.lineTo(x, y); + } + // Don't close a polyline + if (localName.equals("polygon")) { + p.close(); + } + if (doFill(props, gradientMap)) { + doLimits(p); + canvas.drawPath(p, paint); + } + if (doStroke(props)) { + canvas.drawPath(p, paint); + } + popTransform(); + } + } + } else if (!hidden && localName.equals("path")) { + Path p = doPath(getStringAttr("d", atts)); + pushTransform(atts); + Properties props = new Properties(atts); + if (doFill(props, gradientMap)) { + doLimits(p); + canvas.drawPath(p, paint); + } + if (doStroke(props)) { + canvas.drawPath(p, paint); + } + popTransform(); + } else if (!hidden) { + Log.d(TAG, "UNRECOGNIZED SVG COMMAND: " + localName); + } + } + + @Override + public void characters(char ch[], int start, int length) { + // no-op + } + + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + if (localName.equals("svg")) { + picture.endRecording(); + } else if (localName.equals("linearGradient")) { + if (gradient.id != null) { + if (gradient.xlink != null) { + Gradient parent = gradientRefMap.get(gradient.xlink); + if (parent != null) { + gradient = parent.createChild(gradient); + } + } + int[] colors = new int[gradient.colors.size()]; + for (int i = 0; i < colors.length; i++) { + colors[i] = gradient.colors.get(i); + } + float[] positions = new float[gradient.positions.size()]; + for (int i = 0; i < positions.length; i++) { + positions[i] = gradient.positions.get(i); + } + if (colors.length == 0) { + Log.d("BAD", "BAD"); + } + LinearGradient g = new LinearGradient(gradient.x1, gradient.y1, gradient.x2, gradient.y2, colors, positions, Shader.TileMode.CLAMP); + if (gradient.matrix != null) { + g.setLocalMatrix(gradient.matrix); + } + gradientMap.put(gradient.id, g); + gradientRefMap.put(gradient.id, gradient); + } + } else if (localName.equals("radialGradient")) { + if (gradient.id != null) { + if (gradient.xlink != null) { + Gradient parent = gradientRefMap.get(gradient.xlink); + if (parent != null) { + gradient = parent.createChild(gradient); + } + } + int[] colors = new int[gradient.colors.size()]; + for (int i = 0; i < colors.length; i++) { + colors[i] = gradient.colors.get(i); + } + float[] positions = new float[gradient.positions.size()]; + for (int i = 0; i < positions.length; i++) { + positions[i] = gradient.positions.get(i); + } + if (gradient.xlink != null) { + Gradient parent = gradientRefMap.get(gradient.xlink); + if (parent != null) { + gradient = parent.createChild(gradient); + } + } + RadialGradient g = new RadialGradient(gradient.x, gradient.y, gradient.radius, colors, positions, Shader.TileMode.CLAMP); + if (gradient.matrix != null) { + g.setLocalMatrix(gradient.matrix); + } + gradientMap.put(gradient.id, g); + gradientRefMap.put(gradient.id, gradient); + } + } else if (localName.equals("g")) { + if (boundsMode) { + boundsMode = false; + } + // Break out of hidden mode + if (hidden) { + hiddenLevel--; + //Util.debug("Hidden down: " + hiddenLevel); + if (hiddenLevel == 0) { + hidden = false; + } + } + // Clear gradient map + gradientMap.clear(); + popTransform(); + } + } + } +} diff --git a/DroidFish/src/com/larvalabs/svgandroid/package-info.java b/DroidFish/src/com/larvalabs/svgandroid/package-info.java new file mode 100644 index 0000000..7dccb4f --- /dev/null +++ b/DroidFish/src/com/larvalabs/svgandroid/package-info.java @@ -0,0 +1,26 @@ +/** + * Provides a mechanism to parse a limited version of SVG Basic 1.1 files in to android.graphics.Picture + * objects. + * + * This allows vector graphics files to be saved out of illustration software (such as Adobe Illustrator) as SVG Basic + * and then used directly in an Android app. The android.graphics.Picture is a very optimized and + * convenient vector graphics class. Performance is very good on a wide array of Android devices. + *

+ * Note that only SVG features that can be directly converted in to Android graphics calls are supported. + * The following SVG Basic 1.1 features are not supported and will be ignored by the parser: + *

    + *
  • All text and font features. + *
  • Styles. + *
  • Symbols, conditional processing. + *
  • Patterns. + *
  • Masks, filters and views. + *
  • Interactivity, linking, scripting and animation. + *
+ * Even with the above features missing, users will find that most Illustrator drawings will render perfectly well on + * Android with this library. + * + * See the {@link com.larvalabs.svgandroid.SVGParser SVGParser} class for instructions on how to use the parser. + * + * @see com.larvalabs.svgandroid.SVGParser + */ +package com.larvalabs.svgandroid;