From ef7b16f78d0b7db1d73f0fee7b86e68e9ee231b5 Mon Sep 17 00:00:00 2001 From: James Wilding Date: Wed, 21 May 2014 14:42:00 +0100 Subject: [PATCH] Refactor SVGParser and SVGHandler into smaller components. Provided PathHandler alternative to load path data instead of a drawable --- .../com/larvalabs/svgandroid/BaseBuilder.java | 113 + .../com/larvalabs/svgandroid/BaseHandler.java | 534 ++++ .../larvalabs/svgandroid/CanvasHandler.java | 62 + .../larvalabs/svgandroid/DrawTextHandler.java | 92 + .../larvalabs/svgandroid/PaintBuilder.java | 88 + .../larvalabs/svgandroid/PaintHandler.java | 321 +++ .../com/larvalabs/svgandroid/PathBuilder.java | 25 + .../com/larvalabs/svgandroid/PathHandler.java | 131 + .../larvalabs/svgandroid/PathPaintLength.java | 32 + .../com/larvalabs/svgandroid/SVGBuilder.java | 219 +- .../com/larvalabs/svgandroid/SVGHandler.java | 71 + .../com/larvalabs/svgandroid/SVGParser.java | 2374 ++++++----------- .../com/larvalabs/svgandroid/SVGPaths.java | 43 + 13 files changed, 2366 insertions(+), 1739 deletions(-) create mode 100644 src/main/java/com/larvalabs/svgandroid/BaseBuilder.java create mode 100644 src/main/java/com/larvalabs/svgandroid/BaseHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/CanvasHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/DrawTextHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/PaintBuilder.java create mode 100644 src/main/java/com/larvalabs/svgandroid/PaintHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/PathBuilder.java create mode 100644 src/main/java/com/larvalabs/svgandroid/PathHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/PathPaintLength.java create mode 100644 src/main/java/com/larvalabs/svgandroid/SVGHandler.java create mode 100644 src/main/java/com/larvalabs/svgandroid/SVGPaths.java diff --git a/src/main/java/com/larvalabs/svgandroid/BaseBuilder.java b/src/main/java/com/larvalabs/svgandroid/BaseBuilder.java new file mode 100644 index 0000000..f06d30b --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/BaseBuilder.java @@ -0,0 +1,113 @@ +package com.larvalabs.svgandroid; + +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.util.Log; + +import org.xml.sax.InputSource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +/** + * Created by James on 02/05/2014. + */ +public abstract class BaseBuilder { + private InputStream data; + private boolean closeInputStream = true; + + /** + * 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. + */ + public T readFromInputStream(InputStream svgData) { + this.data = svgData; + return (T) this; + } + + /** + * Parse SVG data from a string. + * + * @param svgData the string containing SVG XML data. + */ + public T readFromString(String svgData) { + this.data = new ByteArrayInputStream(svgData.getBytes()); + return (T) this; + } + + /** + * Parse SVG data from an Android application resource. + * + * @param resources the Android context resources. + * @param resId the ID of the raw resource SVG. + */ + public T readFromResource(Resources resources, int resId) { + this.data = resources.openRawResource(resId); + return (T) this; + } + + /** + * 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. + * @throws java.io.IOException if there was a problem reading the file. + */ + public T readFromAsset(AssetManager assetMngr, String svgPath) throws IOException { + this.data = assetMngr.open(svgPath); + return (T) this; + } + + /** + * Whether or not to close the input stream after reading (ie. after calling build).
+ * (default is true) + */ + public T setCloseInputStreamWhenDone(boolean closeInputStream) { + this.closeInputStream = closeInputStream; + return (T) this; + } + + protected boolean hasData() { + return data != null; + } + + protected InputSource openData() { + // SVGZ support (based on https://github.com/josefpavlik/svg-android/commit/fc0522b2e1): + if (!data.markSupported()) { + data = new BufferedInputStream(data); // decorate stream so we can use mark/reset + } + try { + data.mark(4); + byte[] magic = new byte[2]; + int r = data.read(magic, 0, 2); + int magicInt = (magic[0] + ((magic[1]) << 8)) & 0xffff; + data.reset(); + if (r == 2 && magicInt == GZIPInputStream.GZIP_MAGIC) { + // Log.d(SVGParser.TAG, "SVG is gzipped"); + GZIPInputStream gin = new GZIPInputStream(data); + data = gin; + } + } + catch (IOException ioe) { + throw new SVGParseException(ioe); + } + + return new InputSource(data); + } + + protected void closeData() { + if (closeInputStream) { + try { + data.close(); + } + catch (IOException e) { + Log.e(SVGParser.TAG, "Error closing SVG input stream.", e); + } + } + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/BaseHandler.java b/src/main/java/com/larvalabs/svgandroid/BaseHandler.java new file mode 100644 index 0000000..b7587a0 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/BaseHandler.java @@ -0,0 +1,534 @@ +package com.larvalabs.svgandroid; + +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.RadialGradient; +import android.graphics.RectF; +import android.graphics.Shader; +import android.util.FloatMath; +import android.util.Log; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Created by James on 02/05/2014. + */ +public abstract class BaseHandler extends DefaultHandler { + + private Float limitsAdjustmentX, limitsAdjustmentY; + + private boolean boundsMode = false; + + // Scratch rect (so we aren't constantly making new ones) + private final RectF rect = new RectF(); + protected RectF bounds = null; + protected final RectF limits = new RectF( + Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + protected String SVG_FILL = null; + + private final HashMap gradientMap = new HashMap(); + private SVGParser.Gradient gradient = null; + private final LinkedList layerAttributeStack = new LinkedList(); + + private boolean hidden = false; + private int hiddenLevel = 0; + + private final LinkedList transformStack = new LinkedList(); + private final LinkedList matrixStack = new LinkedList(); + + private final RectF tmpLimitRect = new RectF(); + + public BaseHandler() { + matrixStack.addFirst(new Matrix()); + layerAttributeStack.addFirst(new SVGParser.LayerAttributes(1f)); + } + + protected SVGParser.Gradient getGradient(String id) { + return gradientMap.get(id); + } + + protected Collection getGradients() { + return gradientMap.values(); + } + + private void finishGradients() { + for (SVGParser.Gradient gradient : getGradients()) { + if (gradient.xlink != null) { + SVGParser.Gradient parent = getGradient(gradient.xlink); + if (parent != null) { + gradient.inherit(parent); + } + } + 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 gradient, id=" + gradient.id); + } + if (gradient.isLinear) { + gradient.shader = new LinearGradient(gradient.x1, gradient.y1, gradient.x2, gradient.y2, colors, positions, gradient.tilemode); + } + else { + gradient.shader = new RadialGradient(gradient.x, gradient.y, gradient.radius, colors, positions, gradient.tilemode); + } + } + } + + private void doLimits2(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(RectF box, float strokeWidth) { + Matrix m = matrixStack.getLast(); + m.mapRect(tmpLimitRect, box); + float width2 = strokeWidth / 2; + doLimits2(tmpLimitRect.left - width2, tmpLimitRect.top - width2); + doLimits2(tmpLimitRect.right + width2, tmpLimitRect.bottom + width2); + } + + private void doLimits(RectF box) { + doLimits(box, 0); + } + + public SVGParser.LayerAttributes currentLayerAttributes() { + return layerAttributeStack.getLast(); + } + + private void pushTransform(Attributes atts) { + final String transform = SVGParser.getStringAttr("transform", atts); + boolean pushed = transform != null; + transformStack.addLast(pushed); + if (pushed) { + final Matrix matrix = SVGParser.parseTransform(transform); + pushMatrix(matrix); + matrix.postConcat(matrixStack.getLast()); + matrixStack.addLast(matrix); + } + } + + private void popTransform() { + if (transformStack.removeLast()) { + popMatrix(); + matrixStack.removeLast(); + } + } + + private SVGParser.Gradient doGradient(boolean isLinear, Attributes atts) { + SVGParser.Gradient gradient = new SVGParser.Gradient(); + gradient.id = SVGParser.getStringAttr("id", atts); + gradient.isLinear = isLinear; + if (isLinear) { + gradient.x1 = SVGParser.getFloatAttr("x1", atts, 0f); + gradient.x2 = SVGParser.getFloatAttr("x2", atts, 1f); + gradient.y1 = SVGParser.getFloatAttr("y1", atts, 0f); + gradient.y2 = SVGParser.getFloatAttr("y2", atts, 0f); + } + else { + gradient.x = SVGParser.getFloatAttr("cx", atts, 0f); + gradient.y = SVGParser.getFloatAttr("cy", atts, 0f); + gradient.radius = SVGParser.getFloatAttr("r", atts, 0f); + } + String transform = SVGParser.getStringAttr("gradientTransform", atts); + if (transform != null) { + gradient.matrix = SVGParser.parseTransform(transform); + } + String spreadMethod = SVGParser.getStringAttr("spreadMethod", atts); + if (spreadMethod == null) { + spreadMethod = "pad"; + } + + gradient.tilemode = + (spreadMethod.equals("reflect")) ? Shader.TileMode.MIRROR + : (spreadMethod.equals("repeat")) ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP; + + String unit = SVGParser.getStringAttr("gradientUnits", atts); + if (unit == null) { + unit = "objectBoundingBox"; + } + gradient.boundingBox = !unit.equals("userSpaceOnUse"); + + String xlink = SVGParser.getStringAttr("href", atts); + if (xlink != null) { + if (xlink.startsWith("#")) { + xlink = xlink.substring(1); + } + gradient.xlink = xlink; + } + return gradient; + } + + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes atts) + throws SAXException { + onStartElement(); + + // Ignore everything but rectangles in bounds mode + if (boundsMode) { + if (localName.equals("rect")) { + Float x = SVGParser.getFloatAttr("x", atts); + if (x == null) { + x = 0f; + } + Float y = SVGParser.getFloatAttr("y", atts); + if (y == null) { + y = 0f; + } + Float width = SVGParser.getFloatAttr("width", atts); + Float height = SVGParser.getFloatAttr("height", atts); + bounds = new RectF(x, y, x + width, y + height); + } + return; + } + if (localName.equals("svg")) { + onSvg(); + SVG_FILL = SVGParser.getStringAttr("fill", atts); + String viewboxStr = SVGParser.getStringAttr("viewBox", atts); + if (viewboxStr != null) { + String[] dims = viewboxStr.replace(',', ' ').split("\\s+"); + if (dims.length == 4) { + Float x1 = SVGParser.parseFloatValue(dims[0], null); + Float y1 = SVGParser.parseFloatValue(dims[1], null); + Float x2 = SVGParser.parseFloatValue(dims[2], null); + Float y2 = SVGParser.parseFloatValue(dims[3], null); + if (x1 != null && x2 != null && y1 != null && y2 != null) { + x2 += x1; + y2 += y1; + + float width = FloatMath.ceil(x2 - x1); + float height = FloatMath.ceil(y2 - y1); + limitsAdjustmentX = -x1; + limitsAdjustmentY = -y1; + + onViewBox((int) -x1, (int) -y1, (int) width, (int) height); + } + } + } + + // No viewbox + if (checkViewbox()) { + int width = (int) FloatMath.ceil(SVGParser.getFloatAttr("width", atts)); + int height = (int) FloatMath.ceil(SVGParser.getFloatAttr("height", atts)); + onNoViewbox(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) { + final SVGParser.Properties props = new SVGParser.Properties(atts); + + final int colour; + final Integer stopColour = props.getColor(props.getAttr("stop-color")); + if (stopColour == null) { + colour = 0; + } + else { + float alpha = props.getFloat("stop-opacity", 1) * currentLayerAttributes().opacity; + int alphaInt = Math.round(255 * alpha); + colour = stopColour.intValue() | (alphaInt << 24); + } + gradient.colors.add(colour); + + float offset = props.getFloat("offset", 0); + gradient.positions.add(offset); + } + } + else if (localName.equals("g")) { + final SVGParser.Properties props = new SVGParser.Properties(atts); + + // Check to see if this is the "bounds" layer + if ("bounds".equalsIgnoreCase(SVGParser.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(SVGParser.getStringAttr("display", atts)) || "none".equals(props.getString("display"))) { + if (!hidden) { + hidden = true; + hiddenLevel = 1; + // Util.debug("Hidden up: " + hiddenLevel); + } + } + + // Create layer attributes + final float opacity = props.getFloat("opacity", 1f); + SVGParser.LayerAttributes curLayerAttr = currentLayerAttributes(); + SVGParser.LayerAttributes newLayerAttr = new SVGParser.LayerAttributes(curLayerAttr.opacity * opacity); + layerAttributeStack.addLast(newLayerAttr); + + pushTransform(atts); + onNewLayer(props); + + } + else if (!hidden && localName.equals("rect")) { + Float x = SVGParser.getFloatAttr("x", atts); + if (x == null) { + x = 0f; + } + Float y = SVGParser.getFloatAttr("y", atts); + if (y == null) { + y = 0f; + } + Float width = SVGParser.getFloatAttr("width", atts); + Float height = SVGParser.getFloatAttr("height", atts); + Float rx = SVGParser.getFloatAttr("rx", atts, 0f); + Float ry = SVGParser.getFloatAttr("ry", atts, 0f); + SVGParser.Properties props = new SVGParser.Properties(atts); + + pushTransform(atts); + rect.set(x, y, x + width, y + height); + if (onFill(props, rect)) { + float strokeWidth = onRect(rect, rx, ry, props, true); + doLimits(rect, strokeWidth); + } + + if (onStroke(props)) { + float strokeWidth = onRect(rect, rx, ry, props, false); + doLimits(rect, strokeWidth); + } + + popTransform(); + } + else if (!hidden && localName.equals("line")) { + Float x1 = SVGParser.getFloatAttr("x1", atts); + Float x2 = SVGParser.getFloatAttr("x2", atts); + Float y1 = SVGParser.getFloatAttr("y1", atts); + Float y2 = SVGParser.getFloatAttr("y2", atts); + SVGParser.Properties props = new SVGParser.Properties(atts); + if (onStroke(props)) { + pushTransform(atts); + rect.set(x1, y1, x2, y2); + float strokeWidth = onLine(rect); + if (strokeWidth >= 0) { + doLimits(rect, strokeWidth); + } + popTransform(); + } + } + else if (!hidden && localName.equals("text")) { + Float textX = SVGParser.getFloatAttr("x", atts); + Float textY = SVGParser.getFloatAttr("y", atts); + Float fontSize = SVGParser.getFloatAttr("font-size", atts); + Matrix font_matrix = SVGParser.parseTransform(SVGParser.getStringAttr("transform", + atts)); + pushTransform(atts); + SVGParser.Properties props = new SVGParser.Properties(atts); + onTextConfig(textX, textY, fontSize, font_matrix, props); + popTransform(); + } + else if (!hidden && (localName.equals("circle") || localName.equals("ellipse"))) { + Float centerX, centerY, radiusX, radiusY; + + centerX = SVGParser.getFloatAttr("cx", atts); + centerY = SVGParser.getFloatAttr("cy", atts); + if (localName.equals("ellipse")) { + radiusX = SVGParser.getFloatAttr("rx", atts); + radiusY = SVGParser.getFloatAttr("ry", atts); + + } + else { + radiusX = radiusY = SVGParser.getFloatAttr("r", atts); + } + if (centerX != null && centerY != null && radiusX != null && radiusY != null) { + pushTransform(atts); + SVGParser.Properties props = new SVGParser.Properties(atts); + rect.set(centerX - radiusX, centerY - radiusY, centerX + radiusX, centerY + radiusY); + if (onFill(props, rect)) { + float strokeWidth = onOval(rect, true); + doLimits(rect, strokeWidth); + } + if (onStroke(props)) { + float strokeWidth = onOval(rect, false); + doLimits(rect, strokeWidth); + } + popTransform(); + } + } + else if (!hidden && (localName.equals("polygon") || localName.equals("polyline"))) { + SVGParser.NumberParse numbers = SVGParser.getNumberParseAttr("points", atts); + if (numbers != null) { + Path p = new Path(); + ArrayList points = numbers.numbers; + if (points.size() > 1) { + pushTransform(atts); + SVGParser.Properties props = new SVGParser.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(); + } + p.computeBounds(rect, false); + if (onFill(props, rect)) { + float strokeWidth = onPoly(p, true); + doLimits(rect, strokeWidth); + } + if (onStroke(props)) { + float strokeWidth = onPoly(p, false); + doLimits(rect, strokeWidth); + } + popTransform(); + } + } + } + else if (!hidden && localName.equals("path")) { + Path p = SVGParser.parsePath(SVGParser.getStringAttr("d", atts)); + pushTransform(atts); + SVGParser.Properties props = new SVGParser.Properties(atts); + p.computeBounds(rect, false); + if (onFill(props, rect)) { + onPath(p, true); + doLimits(rect); + } + if (onStroke(props)) { + float strokeWidth = onPath(p, false); + doLimits(rect, strokeWidth); + } + popTransform(); + } + else if (!hidden) { + Log.w(SVGParser.TAG, "UNRECOGNIZED SVG COMMAND: " + localName); + } + } + + + @Override + public void endElement(String namespaceURI, String localName, String qName) throws SAXException { + if (localName.equals("svg")) { + onEndSvg(); + if (limitsAdjustmentX != null) { + limits.left += limitsAdjustmentX; + limits.right += limitsAdjustmentX; + } + if (limitsAdjustmentY != null) { + limits.top += limitsAdjustmentY; + limits.bottom += limitsAdjustmentY; + } + + onEndElement(); + } + else if (localName.equals("linearGradient") || localName.equals("radialGradient")) { + if (gradient.id != null) { + gradientMap.put(gradient.id, gradient); + } + } + else if (localName.equals("defs")) { + finishGradients(); + } + 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 + // gradientRefMap.clear(); + popTransform(); + onEndLayer(); + + if (!layerAttributeStack.isEmpty()) { + layerAttributeStack.removeLast(); + } + } + else if (localName.equals("text")) { + onEndText(); + } + } + + protected abstract void pushMatrix(Matrix matrix); + + protected abstract void popMatrix(); + + protected abstract void onStartElement(); + + protected abstract void onSvg(); + + protected abstract void onViewBox(int x, int y, int width, int height); + + protected abstract boolean checkViewbox(); + + protected abstract void onNoViewbox(int width, int height); + + protected abstract void onNewLayer(SVGParser.Properties props); + + protected abstract boolean onFill(SVGParser.Properties props, RectF bounding); + + protected abstract void onTextConfig(Float textX, Float textY, Float fontSize, Matrix font_matrix, SVGParser.Properties props); + + protected abstract boolean onStroke(SVGParser.Properties props); + + protected abstract float onRect(RectF rect, Float rx, Float ry, SVGParser.Properties props, boolean fill); + + protected abstract float onLine(RectF rect); + + protected abstract float onOval(RectF rect, boolean fill); + + protected abstract float onPoly(Path p, boolean fill); + + protected abstract float onPath(Path p, boolean fill); + + protected abstract void onEndLayer(); + + protected abstract void onEndText(); + + protected abstract void onEndSvg(); + + protected abstract void onEndElement(); + + @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 + } + +} diff --git a/src/main/java/com/larvalabs/svgandroid/CanvasHandler.java b/src/main/java/com/larvalabs/svgandroid/CanvasHandler.java new file mode 100644 index 0000000..042c663 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/CanvasHandler.java @@ -0,0 +1,62 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Canvas; +import android.graphics.Matrix; + +/** + * Created by James on 02/05/2014. + */ +public abstract class CanvasHandler extends PaintHandler { + + protected Canvas canvas; + Integer canvasRestoreCount; + + public CanvasHandler() { + super(); + } + + @Override + protected void pushMatrix(Matrix matrix) { + canvas.save(); + canvas.concat(matrix); + } + + @Override + protected void popMatrix() { + canvas.restore(); + } + + @Override + protected void onSvg() { + canvas = null; + } + + @Override + protected void onViewBox(int x, int y, int width, int height) { + canvas = onCreateCanvas(width, height); + canvasRestoreCount = canvas.save(); + canvas.clipRect(0f, 0f, width, height); + canvas.translate(x, y); + } + + @Override + protected boolean checkViewbox() { + return canvas == null; + } + + @Override + protected void onNoViewbox(int width, int height) { + canvas = onCreateCanvas(width, height); + canvasRestoreCount = null; + } + + @Override + protected void onEndSvg() { + if (canvasRestoreCount != null) { + canvas.restoreToCount(canvasRestoreCount); + } + } + + protected abstract Canvas onCreateCanvas(int width, int height); +} + diff --git a/src/main/java/com/larvalabs/svgandroid/DrawTextHandler.java b/src/main/java/com/larvalabs/svgandroid/DrawTextHandler.java new file mode 100644 index 0000000..0928fb5 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/DrawTextHandler.java @@ -0,0 +1,92 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; + +/** + * Created by James on 02/05/2014. + */ +public abstract class DrawTextHandler extends CanvasHandler { + + private Paint textPaint; + private Float textX; + private Float textY; + private int newLineCount; + private Matrix font_matrix; + private boolean drawCharacters; + private Float textSize; + + public DrawTextHandler() { + super(); + textPaint = new Paint(); + textPaint.setAntiAlias(true); + } + + @Override + protected void onStartElement() { + super.onStartElement(); + textPaint.setAlpha(255); + drawCharacters = false; + } + + @Override + protected void onTextConfig(Float textX, Float textY, Float fontSize, Matrix font_matrix, SVGParser.Properties props) { + drawCharacters = true; + if (fontSize != null) { + textSize = fontSize; + if (textX != null && textY != null) { + this.textX = textX; + this.textY = textY; + } + else if (font_matrix != null) { + this.font_matrix = font_matrix; + } + + Integer color = props.getColor("fill"); + if (color != null) { + doColor(props, color, true, textPaint); + } + else { + textPaint.setColor(Color.BLACK); + } + this.newLineCount = 0; + textPaint.setTextSize(textSize); + canvas.save(); + } + } + + @Override + public void characters(char ch[], int start, int length) { + if (this.drawCharacters) { + if (length == 1 && ch[0] == '\n') { + canvas.restore(); + canvas.save(); + + newLineCount += 1; + canvas.translate(0, newLineCount * textSize); + } + else { + String text = new String(ch, start, length); + if (this.textX != null && this.textY != null) { + canvas.drawText(text, this.textX, this.textY, textPaint); + } + else { + canvas.setMatrix(font_matrix); + canvas.drawText(text, 0, 0, textPaint); + } + Float delta = textPaint.measureText(text); + + canvas.translate(delta, 0); + } + } + } + + @Override + protected void onEndText() { + if (drawCharacters) { + canvas.restore(); + } + drawCharacters = false; + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/PaintBuilder.java b/src/main/java/com/larvalabs/svgandroid/PaintBuilder.java new file mode 100644 index 0000000..719a06c --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/PaintBuilder.java @@ -0,0 +1,88 @@ +package com.larvalabs.svgandroid; + +import android.graphics.ColorFilter; + +/** + * Created by James Wilding on 03/05/2014. + */ +public class PaintBuilder extends BaseBuilder { + + private Integer searchColor = null; + private Integer replaceColor = null; + private ColorFilter strokeColorFilter = null, fillColorFilter = null; + private boolean whiteMode = false; + private boolean overideOpacity = false; + + + public T clearColorSwap() { + searchColor = replaceColor = null; + return (T) this; + } + + /** + * Replaces a single colour with another. + * + * @param searchColor The colour in the SVG. + * @param replaceColor The desired colour. + */ + public T setColorSwap(int searchColor, int replaceColor) { + return setColorSwap(searchColor, replaceColor, false); + } + + /** + * Replaces a single colour with another, affecting the opacity. + * + * @param searchColor The colour in the SVG. + * @param replaceColor The desired colour. + * @param overideOpacity If true, combines the opacity defined in the SVG resource with the alpha of replaceColor. + */ + public T setColorSwap(int searchColor, int replaceColor, boolean overideOpacity) { + this.searchColor = searchColor; + this.replaceColor = replaceColor; + this.overideOpacity = overideOpacity; + return (T) this; + } + + /** + * In white-mode, fills are drawn in white and strokes are not drawn at all. + */ + public T setWhiteMode(boolean whiteMode) { + this.whiteMode = whiteMode; + return (T) this; + } + + /** + * Applies a {@link android.graphics.ColorFilter} to the paint objects used to render the SVG. + */ + public T setColorFilter(ColorFilter colorFilter) { + this.strokeColorFilter = this.fillColorFilter = colorFilter; + return (T) this; + } + + /** + * Applies a {@link android.graphics.ColorFilter} to strokes in the SVG. + */ + public T setStrokeColorFilter(ColorFilter colorFilter) { + this.strokeColorFilter = colorFilter; + return (T) this; + } + + /** + * Applies a {@link android.graphics.ColorFilter} to fills in the SVG. + */ + public T setFillColorFilter(ColorFilter colorFilter) { + this.fillColorFilter = colorFilter; + return (T) this; + } + + protected void applyPaintSettings(PaintHandler handler) { + handler.setColorSwap(searchColor, replaceColor, overideOpacity); + handler.setWhiteMode(whiteMode); + if (fillColorFilter != null) { + handler.setColorFilter(fillColorFilter, true); + } + if (strokeColorFilter != null) { + handler.setColorFilter(strokeColorFilter, false); + } + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/PaintHandler.java b/src/main/java/com/larvalabs/svgandroid/PaintHandler.java new file mode 100644 index 0000000..7bdda44 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/PaintHandler.java @@ -0,0 +1,321 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.util.Log; + +import java.util.LinkedList; +import java.util.StringTokenizer; + +/** + * Created by James on 02/05/2014. + */ +public abstract class PaintHandler extends BaseHandler { + + private Paint strokePaint; + private boolean strokeSet = false; + private final LinkedList strokePaintStack = new LinkedList(); + private final LinkedList strokeSetStack = new LinkedList(); + + private Paint fillPaint; + private boolean fillSet = false; + private final LinkedList fillPaintStack = new LinkedList(); + private final LinkedList fillSetStack = new LinkedList(); + + Integer searchColor = null; + Integer replaceColor = null; + Float opacityMultiplier = null; + + private final Matrix gradMatrix = new Matrix(); + + private boolean whiteMode = false; + + public PaintHandler() { + super(); + strokePaint = new Paint(); + strokePaint.setAntiAlias(true); + strokePaint.setStyle(Paint.Style.STROKE); + fillPaint = new Paint(); + fillPaint.setAntiAlias(true); + fillPaint.setStyle(Paint.Style.FILL); + fillPaint.setStrokeWidth(0); + } + + protected Paint getPaint(boolean fill) { + return fill ? fillPaint : strokePaint; + } + + private static float toFloat(String s, float dflt) { + float result = dflt; + try { + result = Float.parseFloat(s); + } + catch (NumberFormatException e) { + // ignore + } + return result; + } + + /** + * set the path style (if any) stroke-dasharray="n1,n2,..." stroke-dashoffset=n + */ + private void pathStyleHelper(String style, String offset) { + if (style == null) { + return; + } + + if (style.equals("none")) { + strokePaint.setPathEffect(null); + return; + } + + StringTokenizer st = new StringTokenizer(style, " ,"); + int count = st.countTokens(); + float[] intervals = new float[(count & 1) == 1 ? count * 2 : count]; + float max = 0; + float current = 1f; + int i = 0; + while (st.hasMoreTokens()) { + intervals[i++] = current = toFloat(st.nextToken(), current); + max += current; + } + + // in svg speak, we double the intervals on an odd count + for (int start = 0; i < intervals.length; i++, start++) { + max += intervals[i] = intervals[start]; + } + + float off = 0f; + if (offset != null) { + try { + off = Float.parseFloat(offset) % max; + } + catch (NumberFormatException e) { + // ignore + } + } + + strokePaint.setPathEffect(new DashPathEffect(intervals, off)); + } + + public void setColorSwap(Integer searchColor, Integer replaceColor, boolean overideOpacity) { + this.searchColor = searchColor; + this.replaceColor = replaceColor; + if (replaceColor != null && overideOpacity) { + opacityMultiplier = ((replaceColor >> 24) & 0x000000FF) / 255f; + } + else { + opacityMultiplier = null; + } + } + + protected void doColor(SVGParser.Properties atts, Integer color, boolean fillMode, Paint paint) { + int c = (0xFFFFFF & color) | 0xFF000000; + if (searchColor != null && searchColor.intValue() == c) { + c = replaceColor; + } + paint.setShader(null); + paint.setColor(c); + Float opacityAttr = atts.getFloat("opacity"); + if (opacityAttr == null) { + opacityAttr = atts.getFloat(fillMode ? "fill-opacity" : "stroke-opacity"); + } + + float opacity = opacityAttr != null ? opacityAttr : 1f; + opacity *= currentLayerAttributes().opacity; + if (opacityMultiplier != null) { + opacity *= opacityMultiplier; + } + paint.setAlpha((int) (255f * opacity)); + } + + @Override + protected boolean onFill(SVGParser.Properties atts, RectF bounding_box) { + if ("none".equals(atts.getString("display"))) { + return false; + } + if (whiteMode) { + fillPaint.setShader(null); + fillPaint.setColor(Color.WHITE); + return true; + } + String fillString = atts.getString("fill"); + if (fillString == null && SVG_FILL != null) { + fillString = SVG_FILL; + } + if (fillString != null) { + if (fillString.startsWith("url(#")) { + + // It's a gradient fill, look it up in our map + String id = fillString.substring("url(#".length(), fillString.length() - 1); + SVGParser.Gradient g = getGradient(id); + Shader shader = null; + if (g != null) { + shader = g.shader; + } + if (shader != null) { + // Util.debug("Found shader!"); + fillPaint.setShader(shader); + gradMatrix.set(g.matrix); + if (g.boundingBox && bounding_box != null) { + // Log.d("svg", "gradient is bounding box"); + gradMatrix.preTranslate(bounding_box.left, bounding_box.top); + gradMatrix.preScale(bounding_box.width(), bounding_box.height()); + } + shader.setLocalMatrix(gradMatrix); + return true; + } + else { + Log.w(SVGParser.TAG, "Didn't find shader, using black: " + id); + fillPaint.setShader(null); + doColor(atts, Color.BLACK, true, fillPaint); + return true; + } + } + else if (fillString.equalsIgnoreCase("none")) { + fillPaint.setShader(null); + fillPaint.setColor(Color.TRANSPARENT); + return true; + } + else { + fillPaint.setShader(null); + Integer color = atts.getColor(fillString); + if (color != null) { + doColor(atts, color, true, fillPaint); + return true; + } + else { + Log.w(SVGParser.TAG, "Unrecognized fill color, using black: " + fillString); + doColor(atts, Color.BLACK, true, fillPaint); + return true; + } + } + } + else { + if (fillSet) { + // If fill is set, inherit from parent + return fillPaint.getColor() != Color.TRANSPARENT; // optimization + } + else { + // Default is black fill + fillPaint.setShader(null); + fillPaint.setColor(Color.BLACK); + return true; + } + } + } + + @Override + protected boolean onStroke(SVGParser.Properties atts) { + if (whiteMode) { + // Never stroke in white mode + return false; + } + if ("none".equals(atts.getString("display"))) { + return false; + } + + // Check for other stroke attributes + Float width = atts.getFloat("stroke-width"); + if (width != null) { + strokePaint.setStrokeWidth(width); + } + + String linecap = atts.getString("stroke-linecap"); + if ("round".equals(linecap)) { + strokePaint.setStrokeCap(Paint.Cap.ROUND); + } + else if ("square".equals(linecap)) { + strokePaint.setStrokeCap(Paint.Cap.SQUARE); + } + else if ("butt".equals(linecap)) { + strokePaint.setStrokeCap(Paint.Cap.BUTT); + } + + String linejoin = atts.getString("stroke-linejoin"); + if ("miter".equals(linejoin)) { + strokePaint.setStrokeJoin(Paint.Join.MITER); + } + else if ("round".equals(linejoin)) { + strokePaint.setStrokeJoin(Paint.Join.ROUND); + } + else if ("bevel".equals(linejoin)) { + strokePaint.setStrokeJoin(Paint.Join.BEVEL); + } + + pathStyleHelper(atts.getString("stroke-dasharray"), atts.getString("stroke-dashoffset")); + + String strokeString = atts.getAttr("stroke"); + if (strokeString != null) { + if (strokeString.equalsIgnoreCase("none")) { + strokePaint.setColor(Color.TRANSPARENT); + return false; + } + else { + Integer color = atts.getColor(strokeString); + if (color != null) { + doColor(atts, color, false, strokePaint); + return true; + } + else { + Log.w(SVGParser.TAG, "Unrecognized stroke color, using none: " + strokeString); + strokePaint.setColor(Color.TRANSPARENT); + return false; + } + } + } + else { + if (strokeSet) { + // Inherit from parent + return strokePaint.getColor() != Color.TRANSPARENT; // optimization + } + else { + // Default is none + strokePaint.setColor(Color.TRANSPARENT); + return false; + } + } + } + + @Override + protected void onStartElement() { + // Reset paint opacity + strokePaint.setAlpha(255); + fillPaint.setAlpha(255); + } + + @Override + protected void onNewLayer(SVGParser.Properties props) { + fillPaintStack.addLast(new Paint(fillPaint)); + strokePaintStack.addLast(new Paint(strokePaint)); + fillSetStack.addLast(fillSet); + strokeSetStack.addLast(strokeSet); + + onFill(props, null); // Added by mrn but a boundingBox is now required by josef. + onStroke(props); + + fillSet |= (props.getString("fill") != null); + strokeSet |= (props.getString("stroke") != null); + } + + @Override + protected void onEndLayer() { + fillPaint = fillPaintStack.removeLast(); + fillSet = fillSetStack.removeLast(); + strokePaint = strokePaintStack.removeLast(); + strokeSet = strokeSetStack.removeLast(); + } + + public void setColorFilter(ColorFilter colorFilter, boolean fill) { + getPaint(fill).setColorFilter(colorFilter); + } + + public void setWhiteMode(boolean whiteMode) { + this.whiteMode = whiteMode; + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/PathBuilder.java b/src/main/java/com/larvalabs/svgandroid/PathBuilder.java new file mode 100644 index 0000000..3423389 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/PathBuilder.java @@ -0,0 +1,25 @@ +package com.larvalabs.svgandroid; + +import org.xml.sax.InputSource; + +/** + * Created by James on 02/05/2014. + */ +public class PathBuilder extends PaintBuilder { + + public SVGPaths build() { + if (!hasData()) { + throw new IllegalStateException("SVG input not specified. Call one of the readFrom...() methods first."); + } + + try { + final PathHandler handler = new PathHandler(); + applyPaintSettings(handler); + final InputSource source = openData(); + return com.larvalabs.svgandroid.SVGParser.parse(source, handler); + } + finally { + closeData(); + } + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/PathHandler.java b/src/main/java/com/larvalabs/svgandroid/PathHandler.java new file mode 100644 index 0000000..7b578e8 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/PathHandler.java @@ -0,0 +1,131 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by James on 02/05/2014. + */ +public class PathHandler extends PaintHandler { + List mPaths = new ArrayList(); + Path mPath; + Matrix mMatrix = new Matrix(); + float mMatrixX; + float mMatrixY; + + List getPaths() { + return mPaths; + } + + PathHandler() { + super(); + } + + @Override + protected void pushMatrix(Matrix matrix) { +// mPath = new Path(); + mMatrix.postConcat(matrix); + float[] values = new float[16]; + matrix.getValues(values); + mMatrixX = values[2]; + mMatrixY = values[5]; + } + + @Override + protected void popMatrix() { +// List<> + mMatrix.reset(); + } + + @Override + protected void onSvg() { + + } + + @Override + protected void onViewBox(int x, int y, int width, int height) { + + } + + @Override + protected boolean checkViewbox() { + return false; + } + + @Override + protected void onNoViewbox(int width, int height) { + + } + + @Override + protected void onTextConfig(Float textX, Float textY, Float fontSize, Matrix font_matrix, SVGParser.Properties props) { + + } + + @Override + protected float onRect(RectF rect, Float rx, Float ry, SVGParser.Properties props, boolean fill) { + Path p = new Path(); + Paint paint = getPaint(fill); + p.addRect(rect, Path.Direction.CCW); + addPath(p, paint, true); + return paint.getStrokeWidth(); + } + + @Override + protected float onLine(RectF rect) { + Path p = new Path(); + Paint paint = getPaint(false); + p.moveTo(rect.left, rect.top); + p.lineTo(rect.right, rect.bottom); + addPath(p, paint, true); + return paint.getStrokeWidth(); + } + + @Override + protected float onOval(RectF rect, boolean fill) { + Path p = new Path(); + Paint paint = getPaint(fill); + p.addCircle(rect.centerX(), rect.centerY(), rect.width() / 2, Path.Direction.CCW); + addPath(p, paint, true); + return paint.getStrokeWidth(); + } + + @Override + protected float onPoly(Path p, boolean fill) { + return onPath(p, fill); + } + + @Override + protected float onPath(Path p, boolean fill) { + Paint paint = getPaint(fill); + addPath(p, paint, false); + return paint.getStrokeWidth(); + } + + @Override + protected void onEndText() { + } + + @Override + protected void onEndSvg() { + } + + @Override + protected void onEndElement() { + } + + private void addPath(Path p, Paint paint, boolean pathIsNew) { + if(!pathIsNew) { + p = new Path(p); + } + + p.offset(mMatrixX, mMatrixY); + paint = new Paint(paint); + mPaths.add(new PathPaintLength(p, paint)); + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/PathPaintLength.java b/src/main/java/com/larvalabs/svgandroid/PathPaintLength.java new file mode 100644 index 0000000..1e63085 --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/PathPaintLength.java @@ -0,0 +1,32 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; + +/** + * Created by James on 02/05/2014. + */ +public class PathPaintLength { + public final Path path; + public final Paint paint; + public final float length; + + PathPaintLength(Path path, Paint paint) { + this.path = path; + this.paint = paint; + this.length = calcLength(path); + } + + + public static float calcLength(Path path) { + PathMeasure measure = new PathMeasure(path, false); + float length = 0.0f; + do { + length += measure.getLength(); + } + while (measure.nextContour()); + + return length; + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/SVGBuilder.java b/src/main/java/com/larvalabs/svgandroid/SVGBuilder.java index e90ddb3..9058a0e 100644 --- a/src/main/java/com/larvalabs/svgandroid/SVGBuilder.java +++ b/src/main/java/com/larvalabs/svgandroid/SVGBuilder.java @@ -1,200 +1,37 @@ package com.larvalabs.svgandroid; -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.graphics.ColorFilter; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - import org.xml.sax.InputSource; -import com.larvalabs.svgandroid.SVGParser.SVGHandler; - /** * Builder for reading SVGs. Specify input, specify any parsing options (optional), then call {@link #build()} to parse - * and return a {@link SVG}. - * + * and return a {@link com.larvalabs.svgandroid.SVG}. + * * @since 24/12/2012 */ -public class SVGBuilder { - private InputStream data; - private Integer searchColor = null; - private Integer replaceColor = null; - private ColorFilter strokeColorFilter = null, fillColorFilter = null; - private boolean whiteMode = false; - private boolean overideOpacity = false; - private boolean closeInputStream = true; - - /** - * 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. - */ - public SVGBuilder readFromInputStream(InputStream svgData) { - this.data = svgData; - return this; - } - - /** - * Parse SVG data from a string. - * - * @param svgData the string containing SVG XML data. - */ - public SVGBuilder readFromString(String svgData) { - this.data = new ByteArrayInputStream(svgData.getBytes()); - return this; - } - - /** - * Parse SVG data from an Android application resource. - * - * @param resources the Android context resources. - * @param resId the ID of the raw resource SVG. - */ - public SVGBuilder readFromResource(Resources resources, int resId) { - this.data = resources.openRawResource(resId); - return this; - } - - /** - * 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. - * @throws IOException if there was a problem reading the file. - */ - public SVGBuilder readFromAsset(AssetManager assetMngr, String svgPath) throws IOException { - this.data = assetMngr.open(svgPath); - return this; - } - - public SVGBuilder clearColorSwap() { - searchColor = replaceColor = null; - return this; - } - - /** - * Replaces a single colour with another. - * - * @param searchColor The colour in the SVG. - * @param replaceColor The desired colour. - */ - public SVGBuilder setColorSwap(int searchColor, int replaceColor) { - return setColorSwap(searchColor, replaceColor, false); - } - - /** - * Replaces a single colour with another, affecting the opacity. - * - * @param searchColor The colour in the SVG. - * @param replaceColor The desired colour. - * @param overideOpacity If true, combines the opacity defined in the SVG resource with the alpha of replaceColor. - */ - public SVGBuilder setColorSwap(int searchColor, int replaceColor, boolean overideOpacity) { - this.searchColor = searchColor; - this.replaceColor = replaceColor; - this.overideOpacity = overideOpacity; - return this; - } - - /** - * In white-mode, fills are drawn in white and strokes are not drawn at all. - */ - public SVGBuilder setWhiteMode(boolean whiteMode) { - this.whiteMode = whiteMode; - return this; - } - - /** - * Applies a {@link ColorFilter} to the paint objects used to render the SVG. - */ - public SVGBuilder setColorFilter(ColorFilter colorFilter) { - this.strokeColorFilter = this.fillColorFilter = colorFilter; - return this; - } - - /** - * Applies a {@link ColorFilter} to strokes in the SVG. - */ - public SVGBuilder setStrokeColorFilter(ColorFilter colorFilter) { - this.strokeColorFilter = colorFilter; - return this; - } - - /** - * Applies a {@link ColorFilter} to fills in the SVG. - */ - public SVGBuilder setFillColorFilter(ColorFilter colorFilter) { - this.fillColorFilter = colorFilter; - return this; - } - - /** - * Whether or not to close the input stream after reading (ie. after calling {@link #build()}.
- * (default is true) - */ - public SVGBuilder setCloseInputStreamWhenDone(boolean closeInputStream) { - this.closeInputStream = closeInputStream; - return this; - } - - /** - * Loads, reads, parses the SVG (or SVGZ). - * - * @return the parsed SVG. - * @throws SVGParseException if there is an error while parsing. - */ - public SVG build() throws SVGParseException { - if (data == null) { - throw new IllegalStateException("SVG input not specified. Call one of the readFrom...() methods first."); - } - - try { - final SVGHandler handler = new SVGHandler(); - handler.setColorSwap(searchColor, replaceColor, overideOpacity); - handler.setWhiteMode(whiteMode); - if (strokeColorFilter != null) { - handler.strokePaint.setColorFilter(strokeColorFilter); - } - if (fillColorFilter != null) { - handler.fillPaint.setColorFilter(fillColorFilter); - } - - // SVGZ support (based on https://github.com/josefpavlik/svg-android/commit/fc0522b2e1): - if(!data.markSupported()) - data = new BufferedInputStream(data); // decorate stream so we can use mark/reset - try { - data.mark(4); - byte[] magic = new byte[2]; - int r = data.read(magic, 0, 2); - int magicInt = (magic[0] + ((magic[1]) << 8)) & 0xffff; - data.reset(); - if (r == 2 && magicInt == GZIPInputStream.GZIP_MAGIC) { - // Log.d(SVGParser.TAG, "SVG is gzipped"); - GZIPInputStream gin = new GZIPInputStream(data); - data = gin; - } - } catch (IOException ioe) { - throw new SVGParseException(ioe); - } - - final SVG svg = SVGParser.parse(new InputSource(data), handler); - return svg; - - } finally { - if (closeInputStream) { - try { - data.close(); - } catch (IOException e) { - Log.e(SVGParser.TAG, "Error closing SVG input stream.", e); - } - } - } - } +public class SVGBuilder extends PaintBuilder { + + /** + * Loads, reads, parses the SVG (or SVGZ). + * + * @return the parsed SVG. + * @throws com.larvalabs.svgandroid.SVGParseException if there is an error while parsing. + */ + public com.larvalabs.svgandroid.SVG build() throws SVGParseException { + if (!hasData()) { + throw new IllegalStateException("SVG input not specified. Call one of the readFrom...() methods first."); + } + + try { + final SVGHandler handler = new SVGHandler(); + applyPaintSettings(handler); + + final InputSource source = openData(); + final com.larvalabs.svgandroid.SVG svg = com.larvalabs.svgandroid.SVGParser.parse(source, handler); + return svg; + + } + finally { + closeData(); + } + } } diff --git a/src/main/java/com/larvalabs/svgandroid/SVGHandler.java b/src/main/java/com/larvalabs/svgandroid/SVGHandler.java new file mode 100644 index 0000000..f3a882a --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/SVGHandler.java @@ -0,0 +1,71 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.RectF; + +/** + * Created by James on 02/05/2014. + */ +class SVGHandler extends DrawTextHandler { + + private Picture picture; + + public SVGHandler() { + super(); + } + + void setPicture(Picture picture) { + this.picture = picture; + } + + @Override + protected Canvas onCreateCanvas(int width, int height) { + return picture.beginRecording(width, height); + } + + @Override + protected float onRect(RectF rect, Float rx, Float ry, SVGParser.Properties props, boolean fill) { + Paint paint = getPaint(fill); + + if (rx <= 0f && ry <= 0f) { + canvas.drawRect(rect, paint); + } + else { + canvas.drawRoundRect(rect, rx, ry, paint); + } + return paint.getStrokeWidth(); + } + + @Override + protected float onLine(RectF rect) { + Paint paint = getPaint(false); + canvas.drawLine(rect.left, rect.top, rect.right, rect.bottom, paint); + return paint.getStrokeWidth(); + } + + @Override + protected float onOval(RectF rect, boolean fill) { + Paint paint = getPaint(fill); + canvas.drawOval(rect, paint); + return paint.getStrokeWidth(); + } + + @Override + protected float onPoly(Path p, boolean fill) { + return onPath(p, fill); + } + + protected float onPath(Path p, boolean fill) { + Paint paint = getPaint(fill); + canvas.drawPath(p, paint); + return paint.getStrokeWidth(); + } + + @Override + protected void onEndElement() { + picture.endRecording(); + } +} diff --git a/src/main/java/com/larvalabs/svgandroid/SVGParser.java b/src/main/java/com/larvalabs/svgandroid/SVGParser.java index 5257e89..27e0bf8 100644 --- a/src/main/java/com/larvalabs/svgandroid/SVGParser.java +++ b/src/main/java/com/larvalabs/svgandroid/SVGParser.java @@ -1,36 +1,31 @@ package com.larvalabs.svgandroid; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.DashPathEffect; -import android.graphics.LinearGradient; import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.Path; import android.graphics.Picture; -import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.util.FloatMath; import android.util.Log; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.XMLReader; + +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; -import java.util.StringTokenizer; +import java.util.List; import java.util.regex.Pattern; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.SAXNotRecognizedException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.DefaultHandler; - /* * 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 @@ -46,648 +41,692 @@ */ public class SVGParser { - static final String TAG = "SVGAndroid"; - - private static boolean DISALLOW_DOCTYPE_DECL = true; - - /** - * 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); - } - - static SVG parse(InputSource data, SVGHandler handler) throws SVGParseException { - try { - final Picture picture = new Picture(); - handler.setPicture(picture); - - SAXParserFactory spf = SAXParserFactory.newInstance(); - SAXParser sp = spf.newSAXParser(); - XMLReader xr = sp.getXMLReader(); - xr.setContentHandler(handler); - xr.setFeature("http://xml.org/sax/features/validation", false); - if (DISALLOW_DOCTYPE_DECL) { - try { - xr.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - } catch (SAXNotRecognizedException e) { - DISALLOW_DOCTYPE_DECL = false; - } - } - xr.parse(data); - - 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) { - Log.e(TAG, "Failed to parse SVG.", 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; - boolean prevWasE = 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 '-': - // Allow numbers with negative exp such as 7.23e-4 - if (prevWasE) { - prevWasE = false; - break; - } - // fall-through - case '\n': - case '\t': - 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++; - } - prevWasE = false; - break; - } - case 'e': - prevWasE = true; - break; - default: - prevWasE = false; - } - } - - 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 final Pattern TRANSFORM_SEP = Pattern.compile("[\\s,]*"); - - /** - * Parse a list of transforms such as: foo(n,n,n...) bar(n,n,n..._ ...) Delimiters are whitespaces or commas - */ - private static Matrix parseTransform(String s) { - Matrix matrix = new Matrix(); - while (true) { - parseTransformItem(s, matrix); - // Log.i(TAG, "Transformed: (" + s + ") " + matrix); - final int rparen = s.indexOf(")"); - if (rparen > 0 && s.length() > rparen + 1) { - s = TRANSFORM_SEP.matcher(s.substring(rparen + 1)).replaceFirst(""); - } else { - break; - } - } - return matrix; - } - - private static Matrix parseTransformItem(String s, Matrix matrix) { - if (s.startsWith("matrix(")) { - NumberParse np = parseNumbers(s.substring("matrix(".length())); - if (np.numbers.size() == 6) { - Matrix mat = new Matrix(); - mat.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, }); - matrix.preConcat(mat); - } - } 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.preTranslate(tx, ty); - } - } 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 = sx; - if (np.numbers.size() > 1) { - sy = np.numbers.get(1); - } - matrix.preScale(sx, sy); - } - } else if (s.startsWith("skewX(")) { - NumberParse np = parseNumbers(s.substring("skewX(".length())); - if (np.numbers.size() > 0) { - float angle = np.numbers.get(0); - matrix.preSkew((float) Math.tan(angle), 0); - } - } else if (s.startsWith("skewY(")) { - NumberParse np = parseNumbers(s.substring("skewY(".length())); - if (np.numbers.size() > 0) { - float angle = np.numbers.get(0); - matrix.preSkew(0, (float) Math.tan(angle)); - } - } 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.preTranslate(-cx, -cy); - matrix.preRotate(angle); - matrix.preTranslate(cx, cy); - } - } else { - Log.w(TAG, "Invalid transform (" + s + ")"); - } - return matrix; - } - - /** - * 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) ((prevCmd) - 1); - break; - } else if (("lhvcsqta").indexOf(Character.toLowerCase(prevCmd)) >= 0) { - 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 'T': - case 't': - // todo - smooth quadratic Bezier (two parameters) - 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 'Q': - case 'q': - // todo - quadratic Bezier (four parameters) - case 'S': - case 's': { - wasCurve = true; - float x2 = ph.nextFloat(); - float y2 = ph.nextFloat(); - float x = ph.nextFloat(); - float y = ph.nextFloat(); - if (Character.isLowerCase(cmd)) { - 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 = ph.nextFlag(); - int sweepArc = ph.nextFlag(); - float x = ph.nextFloat(); - float y = ph.nextFloat(); - if (cmd == 'a') { - x += lastX; - y += lastY; - } - drawArc(p, lastX, lastY, x, y, rx, ry, theta, largeArc, sweepArc); - lastX = x; - lastY = y; - break; - } - default: - Log.w(TAG, "Invalid path command: " + cmd); - ph.advance(); - } - if (!wasCurve) { - lastX1 = lastX; - lastY1 = lastY; - } - ph.skipWhitespace(); - } - return p; - } - - private static float angle(float x1, float y1, float x2, float y2) { - - return (float) Math.toDegrees(Math.atan2(x1, y1) - Math.atan2(x2, y2)) % 360; - } - - private static final RectF arcRectf = new RectF(); - private static final Matrix arcMatrix = new Matrix(); - private static final Matrix arcMatrix2 = new Matrix(); - - private static void drawArc(Path p, float lastX, float lastY, float x, float y, float rx, float ry, float theta, - int largeArc, int sweepArc) { - // Log.d("drawArc", "from (" + lastX + "," + lastY + ") to (" + x + ","+ y + ") r=(" + rx + "," + ry + - // ") theta=" + theta + " flags="+ largeArc + "," + sweepArc); - - // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes - - if (rx == 0 || ry == 0) { - p.lineTo(x, y); - return; - } - - if (x == lastX && y == lastY) { - return; // nothing to draw - } - - rx = Math.abs(rx); - ry = Math.abs(ry); - - final float thrad = theta * (float) Math.PI / 180; - final float st = FloatMath.sin(thrad); - final float ct = FloatMath.cos(thrad); - - final float xc = (lastX - x) / 2; - final float yc = (lastY - y) / 2; - final float x1t = ct * xc + st * yc; - final float y1t = -st * xc + ct * yc; - - final float x1ts = x1t * x1t; - final float y1ts = y1t * y1t; - float rxs = rx * rx; - float rys = ry * ry; - - float lambda = (x1ts / rxs + y1ts / rys) * 1.001f; // add 0.1% to be sure that no out of range occurs due to - // limited precision - if (lambda > 1) { - float lambdasr = FloatMath.sqrt(lambda); - rx *= lambdasr; - ry *= lambdasr; - rxs = rx * rx; - rys = ry * ry; - } - - final float R = - FloatMath.sqrt((rxs * rys - rxs * y1ts - rys * x1ts) / (rxs * y1ts + rys * x1ts)) - * ((largeArc == sweepArc) ? -1 : 1); - final float cxt = R * rx * y1t / ry; - final float cyt = -R * ry * x1t / rx; - final float cx = ct * cxt - st * cyt + (lastX + x) / 2; - final float cy = st * cxt + ct * cyt + (lastY + y) / 2; - - final float th1 = angle(1, 0, (x1t - cxt) / rx, (y1t - cyt) / ry); - float dth = angle((x1t - cxt) / rx, (y1t - cyt) / ry, (-x1t - cxt) / rx, (-y1t - cyt) / ry); - - if (sweepArc == 0 && dth > 0) { - dth -= 360; - } else if (sweepArc != 0 && dth < 0) { - dth += 360; - } - - // draw - if ((theta % 360) == 0) { - // no rotate and translate need - arcRectf.set(cx - rx, cy - ry, cx + rx, cy + ry); - p.arcTo(arcRectf, th1, dth); - } else { - // this is the hard and slow part :-) - arcRectf.set(-rx, -ry, rx, ry); - - arcMatrix.reset(); - arcMatrix.postRotate(theta); - arcMatrix.postTranslate(cx, cy); - arcMatrix.invert(arcMatrix2); - - p.transform(arcMatrix2); - p.arcTo(arcRectf, th1, dth); - p.transform(arcMatrix); - } - } - - 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); - return parseFloatValue(v, defaultValue); - } - - private static float getFloatAttr(String name, Attributes attributes, float defaultValue) { - String v = getStringAttr(name, attributes); - return parseFloatValue(v, defaultValue); - } - - private static Float parseFloatValue(String str, Float defaultValue) { - if (str == null) { - return defaultValue; - } else if (str.endsWith("px")) { - str = str.substring(0, str.length() - 2); - } else if (str.endsWith("%")) { - str = str.substring(0, str.length() - 1); - return Float.parseFloat(str) / 100; - } - // Log.d(TAG, "Float parsing '" + name + "=" + v + "'"); - return Float.parseFloat(str); - } - - 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 Shader shader = null; - public boolean boundingBox = false; - public TileMode tilemode; + static final String TAG = "SVGAndroid"; -/* - 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; - } - } - child.boundingBox = g.boundingBox; - child.shader = g.shader; - child.tilemode = g.tilemode; - return child; - } -*/ + private static boolean DISALLOW_DOCTYPE_DECL = true; + + /** + * 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); + } + + static com.larvalabs.svgandroid.SVG parse(InputSource data, SVGHandler handler) throws SVGParseException { + try { + final Picture picture = new Picture(); + handler.setPicture(picture); + + processSax(data, handler); + + com.larvalabs.svgandroid.SVG result = new com.larvalabs.svgandroid.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) { + Log.e(TAG, "Failed to parse SVG.", e); + throw new SVGParseException(e); + } + } + + private static void processSax(InputSource data, BaseHandler handler) throws ParserConfigurationException, SAXException, IOException { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + xr.setContentHandler(handler); + xr.setFeature("http://xml.org/sax/features/validation", false); + if (DISALLOW_DOCTYPE_DECL) { + try { + xr.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } + catch (SAXNotRecognizedException e) { + DISALLOW_DOCTYPE_DECL = false; + } + } + xr.parse(data); + } + + static SVGPaths parse(InputSource data, PathHandler handler) { + try { + processSax(data, handler); + + List paths = handler.getPaths(); + RectF bounds = handler.bounds; + if(bounds == null) { + bounds = new RectF(handler.limits); + bounds.offsetTo(0, 0); + } + SVGPaths result = new SVGPaths(paths, bounds); + return result; + } + catch (Exception e) { + Log.e(TAG, "Failed to parse SVG.", 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; + boolean prevWasE = 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 '-': + // Allow numbers with negative exp such as 7.23e-4 + if (prevWasE) { + prevWasE = false; + break; + } + // fall-through + case '\n': + case '\t': + 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++; + } + prevWasE = false; + break; + } + case 'e': + prevWasE = true; + break; + default: + prevWasE = false; + } + } + + 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 final Pattern TRANSFORM_SEP = Pattern.compile("[\\s,]*"); + + /** + * Parse a list of transforms such as: foo(n,n,n...) bar(n,n,n..._ ...) Delimiters are whitespaces or commas + */ + static Matrix parseTransform(String s) { + Matrix matrix = new Matrix(); + while (true) { + parseTransformItem(s, matrix); + // Log.i(TAG, "Transformed: (" + s + ") " + matrix); + final int rparen = s.indexOf(")"); + if (rparen > 0 && s.length() > rparen + 1) { + s = TRANSFORM_SEP.matcher(s.substring(rparen + 1)).replaceFirst(""); + } + else { + break; + } + } + return matrix; + } + + private static Matrix parseTransformItem(String s, Matrix matrix) { + if (s.startsWith("matrix(")) { + NumberParse np = parseNumbers(s.substring("matrix(".length())); + if (np.numbers.size() == 6) { + Matrix mat = new Matrix(); + mat.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,}); + matrix.preConcat(mat); + } + } + 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.preTranslate(tx, ty); + } + } + 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 = sx; + if (np.numbers.size() > 1) { + sy = np.numbers.get(1); + } + matrix.preScale(sx, sy); + } + } + else if (s.startsWith("skewX(")) { + NumberParse np = parseNumbers(s.substring("skewX(".length())); + if (np.numbers.size() > 0) { + float angle = np.numbers.get(0); + matrix.preSkew((float) Math.tan(angle), 0); + } + } + else if (s.startsWith("skewY(")) { + NumberParse np = parseNumbers(s.substring("skewY(".length())); + if (np.numbers.size() > 0) { + float angle = np.numbers.get(0); + matrix.preSkew(0, (float) Math.tan(angle)); + } + } + 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.preTranslate(-cx, -cy); + matrix.preRotate(angle); + matrix.preTranslate(cx, cy); + } + } + else { + Log.w(TAG, "Invalid transform (" + s + ")"); + } + return matrix; + } + + /** + * 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) ((prevCmd) - 1); + break; + } + else if (("lhvcsqta").indexOf(Character.toLowerCase(prevCmd)) >= 0) { + 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 'T': + case 't': + // todo - smooth quadratic Bezier (two parameters) + 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 'Q': + case 'q': + // todo - quadratic Bezier (four parameters) + case 'S': + case 's': { + wasCurve = true; + float x2 = ph.nextFloat(); + float y2 = ph.nextFloat(); + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (Character.isLowerCase(cmd)) { + 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 = ph.nextFlag(); + int sweepArc = ph.nextFlag(); + float x = ph.nextFloat(); + float y = ph.nextFloat(); + if (cmd == 'a') { + x += lastX; + y += lastY; + } + drawArc(p, lastX, lastY, x, y, rx, ry, theta, largeArc, sweepArc); + lastX = x; + lastY = y; + break; + } + default: + Log.w(TAG, "Invalid path command: " + cmd); + ph.advance(); + } + if (!wasCurve) { + lastX1 = lastX; + lastY1 = lastY; + } + ph.skipWhitespace(); + } + return p; + } + + private static float angle(float x1, float y1, float x2, float y2) { + + return (float) Math.toDegrees(Math.atan2(x1, y1) - Math.atan2(x2, y2)) % 360; + } + + private static final RectF arcRectf = new RectF(); + private static final Matrix arcMatrix = new Matrix(); + private static final Matrix arcMatrix2 = new Matrix(); + + private static void drawArc(Path p, float lastX, float lastY, float x, float y, float rx, float ry, float theta, + int largeArc, int sweepArc) { + // Log.d("drawArc", "from (" + lastX + "," + lastY + ") to (" + x + ","+ y + ") r=(" + rx + "," + ry + + // ") theta=" + theta + " flags="+ largeArc + "," + sweepArc); + + // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + + if (rx == 0 || ry == 0) { + p.lineTo(x, y); + return; + } + + if (x == lastX && y == lastY) { + return; // nothing to draw + } + + rx = Math.abs(rx); + ry = Math.abs(ry); + + final float thrad = theta * (float) Math.PI / 180; + final float st = FloatMath.sin(thrad); + final float ct = FloatMath.cos(thrad); + + final float xc = (lastX - x) / 2; + final float yc = (lastY - y) / 2; + final float x1t = ct * xc + st * yc; + final float y1t = -st * xc + ct * yc; + + final float x1ts = x1t * x1t; + final float y1ts = y1t * y1t; + float rxs = rx * rx; + float rys = ry * ry; + + float lambda = (x1ts / rxs + y1ts / rys) * 1.001f; // add 0.1% to be sure that no out of range occurs due to + // limited precision + if (lambda > 1) { + float lambdasr = FloatMath.sqrt(lambda); + rx *= lambdasr; + ry *= lambdasr; + rxs = rx * rx; + rys = ry * ry; + } + + final float R = + FloatMath.sqrt((rxs * rys - rxs * y1ts - rys * x1ts) / (rxs * y1ts + rys * x1ts)) + * ((largeArc == sweepArc) ? -1 : 1); + final float cxt = R * rx * y1t / ry; + final float cyt = -R * ry * x1t / rx; + final float cx = ct * cxt - st * cyt + (lastX + x) / 2; + final float cy = st * cxt + ct * cyt + (lastY + y) / 2; + + final float th1 = angle(1, 0, (x1t - cxt) / rx, (y1t - cyt) / ry); + float dth = angle((x1t - cxt) / rx, (y1t - cyt) / ry, (-x1t - cxt) / rx, (-y1t - cyt) / ry); + + if (sweepArc == 0 && dth > 0) { + dth -= 360; + } + else if (sweepArc != 0 && dth < 0) { + dth += 360; + } + + // draw + if ((theta % 360) == 0) { + // no rotate and translate need + arcRectf.set(cx - rx, cy - ry, cx + rx, cy + ry); + p.arcTo(arcRectf, th1, dth); + } + else { + // this is the hard and slow part :-) + arcRectf.set(-rx, -ry, rx, ry); + + arcMatrix.reset(); + arcMatrix.postRotate(theta); + arcMatrix.postTranslate(cx, cy); + arcMatrix.invert(arcMatrix2); + + p.transform(arcMatrix2); + p.arcTo(arcRectf, th1, dth); + p.transform(arcMatrix); + } + } + + 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; + } + + 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; + } + + static Float getFloatAttr(String name, Attributes attributes) { + return getFloatAttr(name, attributes, null); + } + + static Float getFloatAttr(String name, Attributes attributes, Float defaultValue) { + String v = getStringAttr(name, attributes); + return parseFloatValue(v, defaultValue); + } + + static float getFloatAttr(String name, Attributes attributes, float defaultValue) { + String v = getStringAttr(name, attributes); + return parseFloatValue(v, defaultValue); + } + + static Float parseFloatValue(String str, Float defaultValue) { + if (str == null) { + return defaultValue; + } + else if (str.endsWith("px")) { + str = str.substring(0, str.length() - 2); + } + else if (str.endsWith("%")) { + str = str.substring(0, str.length() - 1); + return Float.parseFloat(str) / 100; + } + // Log.d(TAG, "Float parsing '" + name + "=" + v + "'"); + return Float.parseFloat(str); + } + + static class NumberParse { + 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); + } + + } + + 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 Shader shader = null; + public boolean boundingBox = false; + public TileMode tilemode; + + /* + 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; + } + } + child.boundingBox = g.boundingBox; + child.shader = g.shader; + child.tilemode = g.tilemode; + return child; + } + */ public void inherit(Gradient parent) { Gradient child = this; child.xlink = parent.id; @@ -695,910 +734,149 @@ public void inherit(Gradient parent) { child.colors = parent.colors; if (child.matrix == null) { child.matrix = parent.matrix; - } else if (parent.matrix != null) { - Matrix m = new Matrix(parent.matrix); + } + else if (parent.matrix != null) { + Matrix m = new Matrix(parent.matrix); m.preConcat(child.matrix); child.matrix = m; } } - } - - 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); - } - - private Integer rgb(int r, int g, int b) { - return ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff); - } - - private int parseNum(String v) throws NumberFormatException { - if (v.endsWith("%")) { - v = v.substring(0, v.length() - 1); - return Math.round(Float.parseFloat(v) / 100 * 255); - } - return Integer.parseInt(v); - } - - public Integer getColor(String name) { + } + + 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); + } + } + + static class Properties { + StyleSet styles = null; + Attributes atts; + + 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); + } + + private Integer rgb(int r, int g, int b) { + return ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff); + } + + private int parseNum(String v) throws NumberFormatException { + if (v.endsWith("%")) { + v = v.substring(0, v.length() - 1); + return Math.round(Float.parseFloat(v) / 100 * 255); + } + return Integer.parseInt(v); + } + + public Integer getColor(String name) { String v = name; - if (v == null) { - return null; - } else if (v.startsWith("#")) { - try { // #RRGGBB or #AARRGGBB - return Color.parseColor(v); - } catch (IllegalArgumentException iae) { - return null; - } - } else if (v.startsWith("rgb(") && v.endsWith(")")) { - String values[] = v.substring(4, v.length() - 1).split(","); - try { - return rgb(parseNum(values[0]), parseNum(values[1]), parseNum(values[2])); - } catch (NumberFormatException nfe) { - return null; - } catch (ArrayIndexOutOfBoundsException e) { - return null; - } - } else { - return SVGColors.mapColour(v); - } - } - - // convert 0xRGB into 0xRRGGBB - private int hex3Tohex6(int x) { - return (x & 0xF00) << 8 | (x & 0xF00) << 12 | (x & 0xF0) << 4 | (x & 0xF0) << 8 | (x & 0xF) << 4 - | (x & 0xF); - } - - public float getFloat(String name, float defaultValue) { - String v = getAttr(name); - if (v == null) { - return defaultValue; - } else { - try { - return Float.parseFloat(v); - } catch (NumberFormatException nfe) { - return defaultValue; - } - } - } - - public Float getFloat(String name, Float defaultValue) { - String v = getAttr(name); - if (v == null) { - return defaultValue; - } else { - try { - return Float.parseFloat(v); - } catch (NumberFormatException nfe) { - return defaultValue; - } - } - } - - public Float getFloat(String name) { - return getFloat(name, null); - } - } - - private static class LayerAttributes { - public final float opacity; - - public LayerAttributes(float opacity) { - this.opacity = opacity; - } - } - - static class SVGHandler extends DefaultHandler { - - private Picture picture; - private Canvas canvas; - private Float limitsAdjustmentX, limitsAdjustmentY; - - final LinkedList layerAttributeStack = new LinkedList(); - - Paint strokePaint; - boolean strokeSet = false; - final LinkedList strokePaintStack = new LinkedList(); - final LinkedList strokeSetStack = new LinkedList(); - - Paint fillPaint; - boolean fillSet = false; - final LinkedList fillPaintStack = new LinkedList(); - final LinkedList fillSetStack = new LinkedList(); - - Paint textPaint; - boolean drawCharacters; - Float textX; - Float textY; - int newLineCount; - Float textSize; - Matrix font_matrix; - - // Scratch rect (so we aren't constantly making new ones) - final RectF rect = new RectF(); - RectF bounds = null; - final RectF limits = new RectF( - Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); - - Integer searchColor = null; - Integer replaceColor = null; - Float opacityMultiplier = null; - - boolean whiteMode = false; - - Integer canvasRestoreCount; - - final LinkedList transformStack = new LinkedList(); - final LinkedList matrixStack = new LinkedList(); - - final HashMap gradientMap = new HashMap(); - Gradient gradient = null; - - public SVGHandler() { - strokePaint = new Paint(); - strokePaint.setAntiAlias(true); - strokePaint.setStyle(Paint.Style.STROKE); - fillPaint = new Paint(); - fillPaint.setAntiAlias(true); - fillPaint.setStyle(Paint.Style.FILL); - textPaint = new Paint(); - textPaint.setAntiAlias(true); - matrixStack.addFirst(new Matrix()); - layerAttributeStack.addFirst(new LayerAttributes(1f)); - } - - void setPicture(Picture picture) { - this.picture = picture; - } - - public void setColorSwap(Integer searchColor, Integer replaceColor, boolean overideOpacity) { - this.searchColor = searchColor; - this.replaceColor = replaceColor; - if (replaceColor != null && overideOpacity) { - opacityMultiplier = ((replaceColor >> 24) & 0x000000FF) / 255f; - } else { - opacityMultiplier = null; - } - } - - 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 final Matrix gradMatrix = new Matrix(); - - private boolean doFill(Properties atts, RectF bounding_box) { - if ("none".equals(atts.getString("display"))) { - return false; - } - if (whiteMode) { - fillPaint.setShader(null); - fillPaint.setColor(Color.WHITE); - return true; - } - String fillString = atts.getString("fill"); - if (fillString == null && SVG_FILL != null) { - fillString = SVG_FILL; + if (v == null) { + return null; } - if (fillString != null) { - if (fillString.startsWith("url(#")) { - - // It's a gradient fill, look it up in our map - String id = fillString.substring("url(#".length(), fillString.length() - 1); - Gradient g = gradientMap.get(id); - Shader shader = null; - if (g != null) { - shader = g.shader; - } - if (shader != null) { - // Util.debug("Found shader!"); - fillPaint.setShader(shader); - gradMatrix.set(g.matrix); - if (g.boundingBox && bounding_box != null) { - // Log.d("svg", "gradient is bounding box"); - gradMatrix.preTranslate(bounding_box.left, bounding_box.top); - gradMatrix.preScale(bounding_box.width(), bounding_box.height()); - } - shader.setLocalMatrix(gradMatrix); - return true; - } else { - Log.w(TAG, "Didn't find shader, using black: " + id); - fillPaint.setShader(null); - doColor(atts, Color.BLACK, true, fillPaint); - return true; - } - } else if (fillString.equalsIgnoreCase("none")) { - fillPaint.setShader(null); - fillPaint.setColor(Color.TRANSPARENT); - return true; - } else { - fillPaint.setShader(null); - Integer color = atts.getColor(fillString); - if (color != null) { - doColor(atts, color, true, fillPaint); - return true; - } else { - Log.w(TAG, "Unrecognized fill color, using black: " + fillString); - doColor(atts, Color.BLACK, true, fillPaint); - return true; - } - } - } else { - if (fillSet) { - // If fill is set, inherit from parent - return fillPaint.getColor() != Color.TRANSPARENT; // optimization - } else { - // Default is black fill - fillPaint.setShader(null); - fillPaint.setColor(Color.BLACK); - return true; - } - } - } - - private boolean doStroke(Properties atts) { - if (whiteMode) { - // Never stroke in white mode - return false; - } - if ("none".equals(atts.getString("display"))) { - return false; - } - - // Check for other stroke attributes - Float width = atts.getFloat("stroke-width"); - if (width != null) { - strokePaint.setStrokeWidth(width); - } - - String linecap = atts.getString("stroke-linecap"); - if ("round".equals(linecap)) { - strokePaint.setStrokeCap(Paint.Cap.ROUND); - } else if ("square".equals(linecap)) { - strokePaint.setStrokeCap(Paint.Cap.SQUARE); - } else if ("butt".equals(linecap)) { - strokePaint.setStrokeCap(Paint.Cap.BUTT); - } - - String linejoin = atts.getString("stroke-linejoin"); - if ("miter".equals(linejoin)) { - strokePaint.setStrokeJoin(Paint.Join.MITER); - } else if ("round".equals(linejoin)) { - strokePaint.setStrokeJoin(Paint.Join.ROUND); - } else if ("bevel".equals(linejoin)) { - strokePaint.setStrokeJoin(Paint.Join.BEVEL); - } - - pathStyleHelper(atts.getString("stroke-dasharray"), atts.getString("stroke-dashoffset")); - - String strokeString = atts.getAttr("stroke"); - if (strokeString != null) { - if (strokeString.equalsIgnoreCase("none")) { - strokePaint.setColor(Color.TRANSPARENT); - return false; - } else { - Integer color = atts.getColor(strokeString); - if (color != null) { - doColor(atts, color, false, strokePaint); - return true; - } else { - Log.w(TAG, "Unrecognized stroke color, using none: " + strokeString); - strokePaint.setColor(Color.TRANSPARENT); - return false; - } - } - } else { - if (strokeSet) { - // Inherit from parent - return strokePaint.getColor() != Color.TRANSPARENT; // optimization - } else { - // Default is none - strokePaint.setColor(Color.TRANSPARENT); - 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, 1f); - 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 spreadMethod = getStringAttr("spreadMethod", atts); - if (spreadMethod == null) { - spreadMethod = "pad"; - } - - gradient.tilemode = - (spreadMethod.equals("reflect")) ? Shader.TileMode.MIRROR - : (spreadMethod.equals("repeat")) ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP; - - String unit = getStringAttr("gradientUnits", atts); - if (unit == null) { - unit = "objectBoundingBox"; - } - gradient.boundingBox = !unit.equals("userSpaceOnUse"); - - String xlink = getStringAttr("href", atts); - if (xlink != null) { - if (xlink.startsWith("#")) { - xlink = xlink.substring(1); - } - gradient.xlink = xlink; - } - return gradient; - } - - private void finishGradients() { - for(Gradient gradient : gradientMap.values()) { - if (gradient.xlink != null) { - Gradient parent = gradientMap.get(gradient.xlink); - if (parent != null) { - gradient.inherit(parent); - } + else if (v.startsWith("#")) { + try { // #RRGGBB or #AARRGGBB + return Color.parseColor(v); + } + catch (IllegalArgumentException iae) { + return null; } - int[] colors = new int[gradient.colors.size()]; - for (int i = 0; i < colors.length; i++) { - colors[i] = gradient.colors.get(i); + } + else if (v.startsWith("rgb(") && v.endsWith(")")) { + String values[] = v.substring(4, v.length() - 1).split(","); + try { + return rgb(parseNum(values[0]), parseNum(values[1]), parseNum(values[2])); } - float[] positions = new float[gradient.positions.size()]; - for (int i = 0; i < positions.length; i++) { - positions[i] = gradient.positions.get(i); + catch (NumberFormatException nfe) { + return null; } - if (colors.length == 0) { - Log.d("BAD", "BAD gradient, id="+gradient.id); + catch (ArrayIndexOutOfBoundsException e) { + return null; } - if (gradient.isLinear) { - gradient.shader= new LinearGradient(gradient.x1, gradient.y1, gradient.x2, gradient.y2, colors, positions, gradient.tilemode); - } else { - gradient.shader= new RadialGradient(gradient.x, gradient.y, gradient.radius, colors, positions, gradient.tilemode); + } + else { + return SVGColors.mapColour(v); + } + } + + // convert 0xRGB into 0xRRGGBB + private int hex3Tohex6(int x) { + return (x & 0xF00) << 8 | (x & 0xF00) << 12 | (x & 0xF0) << 4 | (x & 0xF0) << 8 | (x & 0xF) << 4 + | (x & 0xF); + } + + public float getFloat(String name, float defaultValue) { + String v = getAttr(name); + if (v == null) { + return defaultValue; + } + else { + try { + return Float.parseFloat(v); + } + catch (NumberFormatException nfe) { + return defaultValue; } - } + } + } + + public Float getFloat(String name, Float defaultValue) { + String v = getAttr(name); + if (v == null) { + return defaultValue; + } + else { + try { + return Float.parseFloat(v); + } + catch (NumberFormatException nfe) { + return defaultValue; + } + } + } + + public Float getFloat(String name) { + return getFloat(name, null); + } + } + + static class LayerAttributes { + public final float opacity; + + public LayerAttributes(float opacity) { + this.opacity = opacity; } + } - private void doColor(Properties atts, Integer color, boolean fillMode, Paint paint) { - int c = (0xFFFFFF & color) | 0xFF000000; - if (searchColor != null && searchColor.intValue() == c) { - c = replaceColor; - } - paint.setShader(null); - paint.setColor(c); - Float opacityAttr = atts.getFloat("opacity"); - if (opacityAttr == null) { - opacityAttr = atts.getFloat(fillMode ? "fill-opacity" : "stroke-opacity"); - } - - float opacity = opacityAttr != null ? opacityAttr : 1f; - opacity *= currentLayerAttributes().opacity; - if (opacityMultiplier != null) { - opacity *= opacityMultiplier; - } - paint.setAlpha((int) (255f * opacity)); - } - - /** - * set the path style (if any) stroke-dasharray="n1,n2,..." stroke-dashoffset=n - */ - private void pathStyleHelper(String style, String offset) { - if (style == null) { - return; - } - - if (style.equals("none")) { - strokePaint.setPathEffect(null); - return; - } - - StringTokenizer st = new StringTokenizer(style, " ,"); - int count = st.countTokens(); - float[] intervals = new float[(count & 1) == 1 ? count * 2 : count]; - float max = 0; - float current = 1f; - int i = 0; - while (st.hasMoreTokens()) { - intervals[i++] = current = toFloat(st.nextToken(), current); - max += current; - } - - // in svg speak, we double the intervals on an odd count - for (int start = 0; i < intervals.length; i++, start++) { - max += intervals[i] = intervals[start]; - } - - float off = 0f; - if (offset != null) { - try { - off = Float.parseFloat(offset) % max; - } catch (NumberFormatException e) { - // ignore - } - } - - strokePaint.setPathEffect(new DashPathEffect(intervals, off)); - } - - private static float toFloat(String s, float dflt) { - float result = dflt; - try { - result = Float.parseFloat(s); - } catch (NumberFormatException e) { - // ignore - } - return result; - } - - private boolean hidden = false; - private int hiddenLevel = 0; - private boolean boundsMode = false; - - private void doLimits2(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 final RectF tmpLimitRect = new RectF(); - - private void doLimits(RectF box, Paint paint) { - Matrix m = matrixStack.getLast(); - m.mapRect(tmpLimitRect, box); - float width2 = (paint == null) ? 0 : paint.getStrokeWidth() / 2; - doLimits2(tmpLimitRect.left - width2, tmpLimitRect.top - width2); - doLimits2(tmpLimitRect.right + width2, tmpLimitRect.bottom + width2); - } - - private void doLimits(RectF box) { - doLimits(box, null); - } - - private void pushTransform(Attributes atts) { - final String transform = getStringAttr("transform", atts); - boolean pushed = transform != null; - transformStack.addLast(pushed); - if (pushed) { - final Matrix matrix = parseTransform(transform); - canvas.save(); - canvas.concat(matrix); - matrix.postConcat(matrixStack.getLast()); - matrixStack.addLast(matrix); - } - - } - - private void popTransform() { - if (transformStack.removeLast()) { - canvas.restore(); - matrixStack.removeLast(); - } - } - - private String SVG_FILL = null; - - @Override - public void startElement(String namespaceURI, String localName, String qName, Attributes atts) - throws SAXException { - // Reset paint opacity - strokePaint.setAlpha(255); - fillPaint.setAlpha(255); - textPaint.setAlpha(255); - - this.drawCharacters = false; - - // 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 + height); - } - return; - } - if (localName.equals("svg")) { - canvas = null; - SVG_FILL = getStringAttr("fill", atts); - String viewboxStr = getStringAttr("viewBox", atts); - if (viewboxStr != null) { - String[] dims = viewboxStr.replace(',', ' ').split("\\s+"); - if (dims.length == 4) { - Float x1 = parseFloatValue(dims[0], null); - Float y1 = parseFloatValue(dims[1], null); - Float x2 = parseFloatValue(dims[2], null); - Float y2 = parseFloatValue(dims[3], null); - if (x1 != null && x2 != null && y1 != null && y2 != null) { - x2 += x1; - y2 += y1; - - float width = FloatMath.ceil(x2 - x1); - float height = FloatMath.ceil(y2 - y1); - canvas = picture.beginRecording((int) width, (int) height); - canvasRestoreCount = canvas.save(); - canvas.clipRect(0f, 0f, width, height); - limitsAdjustmentX = -x1; - limitsAdjustmentY = -y1; - canvas.translate(limitsAdjustmentX, limitsAdjustmentY); - } - } - } - // No viewbox - if (canvas == null) { - int width = (int) FloatMath.ceil(getFloatAttr("width", atts)); - int height = (int) FloatMath.ceil(getFloatAttr("height", atts)); - canvas = picture.beginRecording(width, height); - canvasRestoreCount = null; - } - - } 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) { - final Properties props = new Properties(atts); - - final int colour; - final Integer stopColour = props.getColor(props.getAttr("stop-color")); - if (stopColour == null) { - colour = 0; - } else { - float alpha = props.getFloat("stop-opacity", 1) * currentLayerAttributes().opacity; - int alphaInt = Math.round(255 * alpha); - colour = stopColour.intValue() | (alphaInt << 24); - } - gradient.colors.add(colour); - - float offset = props.getFloat("offset", 0); - gradient.positions.add(offset); - } - } else if (localName.equals("g")) { - final Properties props = new Properties(atts); - - // 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)) || "none".equals(props.getString("display"))) { - if (!hidden) { - hidden = true; - hiddenLevel = 1; - // Util.debug("Hidden up: " + hiddenLevel); - } - } - - // Create layer attributes - final float opacity = props.getFloat("opacity", 1f); - LayerAttributes curLayerAttr = currentLayerAttributes(); - LayerAttributes newLayerAttr = new LayerAttributes(curLayerAttr.opacity * opacity); - layerAttributeStack.addLast(newLayerAttr); - - pushTransform(atts); - fillPaintStack.addLast(new Paint(fillPaint)); - strokePaintStack.addLast(new Paint(strokePaint)); - fillSetStack.addLast(fillSet); - strokeSetStack.addLast(strokeSet); - - doFill(props, null); // Added by mrn but a boundingBox is now required by josef. - doStroke(props); - - fillSet |= (props.getString("fill") != null); - strokeSet |= (props.getString("stroke") != null); - - } 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); - Float rx = getFloatAttr("rx", atts, 0f); - Float ry = getFloatAttr("ry", atts, 0f); - pushTransform(atts); - Properties props = new Properties(atts); - rect.set(x, y, x + width, y + height); - if (doFill(props, rect)) { - rect.set(x, y, x + width, y + height); - if (rx <= 0f && ry <= 0f) { - canvas.drawRect(rect, fillPaint); - } else { - canvas.drawRoundRect(rect, rx, ry, fillPaint); - } - doLimits(rect); - } - if (doStroke(props)) { - rect.set(x, y, x + width, y + height); - if (rx <= 0f && ry <= 0f) { - canvas.drawRect(rect, strokePaint); - } else { - canvas.drawRoundRect(rect, rx, ry, strokePaint); - } - doLimits(rect, strokePaint); - } - 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); - rect.set(x1, y1, x2, y2); - canvas.drawLine(x1, y1, x2, y2, strokePaint); - doLimits(rect, strokePaint); - popTransform(); - } - } else if (!hidden && localName.equals("text")) { - Float textX = getFloatAttr("x", atts); - Float textY = getFloatAttr("y", atts); - Float fontSize = getFloatAttr("font-size", atts); - Matrix font_matrix = parseTransform(getStringAttr("transform", - atts)); - drawCharacters = true; - if (fontSize != null) { - textSize = fontSize; - pushTransform(atts); - if (textX != null && textY != null) { - this.textX = textX; - this.textY = textY; - } else if (font_matrix != null) { - this.font_matrix = font_matrix; - } - Properties props = new Properties(atts); - Integer color = props.getColor("fill"); - if (color != null) { - doColor(props, color, true, textPaint); - } else { - textPaint.setColor(Color.BLACK); - } - this.newLineCount = 0; - textPaint.setTextSize(textSize); - canvas.save(); - popTransform(); - } - } else if (!hidden && (localName.equals("circle") || localName.equals("ellipse"))) { - Float centerX, centerY, radiusX, radiusY; - - centerX = getFloatAttr("cx", atts); - centerY = getFloatAttr("cy", atts); - if (localName.equals("ellipse")) { - radiusX = getFloatAttr("rx", atts); - radiusY = getFloatAttr("ry", atts); - - } else { - radiusX = radiusY = getFloatAttr("r", 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, rect)) { - canvas.drawOval(rect, fillPaint); - doLimits(rect); - } - if (doStroke(props)) { - canvas.drawOval(rect, strokePaint); - doLimits(rect, strokePaint); - } - 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(); - } - p.computeBounds(rect, false); - if (doFill(props, rect)) { - canvas.drawPath(p, fillPaint); - doLimits(rect); - } - if (doStroke(props)) { - canvas.drawPath(p, strokePaint); - doLimits(rect, strokePaint); - } - popTransform(); - } - } - } else if (!hidden && localName.equals("path")) { - Path p = doPath(getStringAttr("d", atts)); - pushTransform(atts); - Properties props = new Properties(atts); - p.computeBounds(rect, false); - if (doFill(props, rect)) { - canvas.drawPath(p, fillPaint); - doLimits(rect); - } - if (doStroke(props)) { - canvas.drawPath(p, strokePaint); - doLimits(rect, strokePaint); - } - popTransform(); - } else if (!hidden) { - Log.w(TAG, "UNRECOGNIZED SVG COMMAND: " + localName); - } - } - - public LayerAttributes currentLayerAttributes() { - return layerAttributeStack.getLast(); - } - - @Override - public void characters(char ch[], int start, int length) { - if (this.drawCharacters) { - if (length == 1 && ch[0] == '\n') { - canvas.restore(); - canvas.save(); - - newLineCount += 1; - canvas.translate(0, newLineCount * textSize); - } else { - String text = new String(ch, start, length); - if (this.textX != null && this.textY != null) { - canvas.drawText(text, this.textX, this.textY, textPaint); - } else { - canvas.setMatrix(font_matrix); - canvas.drawText(text, 0, 0, textPaint); - } - Float delta = textPaint.measureText(text); - - canvas.translate(delta, 0); - } - } - } - - @Override - public void endElement(String namespaceURI, String localName, String qName) throws SAXException { - if (localName.equals("svg")) { - if (canvasRestoreCount != null) { - canvas.restoreToCount(canvasRestoreCount); - } - if (limitsAdjustmentX != null) { - limits.left += limitsAdjustmentX; - limits.right += limitsAdjustmentX; - } - if (limitsAdjustmentY != null) { - limits.top += limitsAdjustmentY; - limits.bottom += limitsAdjustmentY; - } - picture.endRecording(); - - } else if (localName.equals("linearGradient") || localName.equals("radialGradient")) { - if (gradient.id != null) { - gradientMap.put(gradient.id, gradient); - } - } else if (localName.equals("defs")) { - finishGradients(); - } 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 - // gradientRefMap.clear(); - popTransform(); - fillPaint = fillPaintStack.removeLast(); - fillSet = fillSetStack.removeLast(); - strokePaint = strokePaintStack.removeLast(); - strokeSet = strokeSetStack.removeLast(); - if (!layerAttributeStack.isEmpty()) { - layerAttributeStack.removeLast(); - } - } else if (localName.equals("text")) { - if (this.drawCharacters) { - this.drawCharacters = false; - canvas.restore(); - } - } - } - } } diff --git a/src/main/java/com/larvalabs/svgandroid/SVGPaths.java b/src/main/java/com/larvalabs/svgandroid/SVGPaths.java new file mode 100644 index 0000000..a1b1d5e --- /dev/null +++ b/src/main/java/com/larvalabs/svgandroid/SVGPaths.java @@ -0,0 +1,43 @@ +package com.larvalabs.svgandroid; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by James Wilding on 07/05/2014. + */ +public class SVGPaths { + List mPathPaintLengths; + RectF mBounds; + + public SVGPaths(List pathPains, RectF bounds) { + mPathPaintLengths = pathPains; + mBounds = bounds; + } + + public List getPathPaintLengths() { + return mPathPaintLengths; + } + + public RectF getBounds() { + return mBounds; + } + + public static SVGPaths scaleBy(SVGPaths paths, float x, float y) { + Matrix m = new Matrix(); + m.postScale(x, y); + RectF bounds = new RectF(); + m.mapRect(bounds, paths.mBounds); + SVGPaths out = new SVGPaths(new ArrayList(paths.mPathPaintLengths.size()), bounds); + for(PathPaintLength pathPaintLength : paths.mPathPaintLengths) { + Path p = new Path(pathPaintLength.path); + p.transform(m); + out.mPathPaintLengths.add(new PathPaintLength(p, pathPaintLength.paint)); + } + return out; + } +}