package processing.core; import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.util.HashMap; import processing.xml.XMLElement; /** * SVG stands for Scalable Vector Graphics, a portable graphics format. It is * a vector format so it allows for infinite resolution and relatively small * file sizes. Most modern media software can view SVG files, including Adobe * products, Firefox, etc. Illustrator and Inkscape can edit SVG files. * <p> * We have no intention of turning this into a full-featured SVG library. * The goal of this project is a basic shape importer that is small enough * to be included with applets, meaning that its download size should be * in the neighborhood of 25-30k. Starting with release 0149, this library * has been incorporated into the core via the loadShape() command, because * vector shape data is just as important as the image data from loadImage(). * <p> * For more sophisticated import/export, consider the * <A HREF="http://xmlgraphics.apache.org/batik/">Batik</A> * library from the Apache Software Foundation. Future improvements to this * library may focus on this properly supporting a specific subset of SVG, * for instance the simpler SVG profiles known as * <A HREF="http://www.w3.org/TR/SVGMobile/">SVG Tiny or Basic</A>, * although we still would not support the interactivity options. * * <p> <hr noshade> <p> * * A minimal example program using SVG: * (assuming a working moo.svg is in your data folder) * * <PRE> * PShape moo; * * void setup() { * size(400, 400); * moo = loadShape("moo.svg"); * } * void draw() { * background(255); * shape(moo, mouseX, mouseY); * } * </PRE> * * This code is based on the Candy library written by Michael Chang, which was * later revised and expanded for use as a Processing core library by Ben Fry. * Thanks to Ricard Marxer Pinon for help with better Inkscape support in 0154. * * <p> <hr noshade> <p> * * Late October 2008 revisions from ricardmp, incorporated by fry (0154) * <UL> * <LI>Better style attribute handling, enabling better Inkscape support. * </UL> * * October 2008 revisions by fry (Processing 0149, pre-1.0) * <UL> * <LI> Candy is no longer a separate library, and is instead part of core. * <LI> Loading now works through loadShape() * <LI> Shapes are now drawn using the new PGraphics shape() method. * </UL> * * August 2008 revisions by fry (Processing 0149) * <UL> * <LI> Major changes to rework around PShape. * <LI> Now implementing more of the "transform" attribute. * </UL> * * February 2008 revisions by fry (Processing 0136) * <UL> * <LI> Added support for quadratic curves in paths (Q, q, T, and t operators) * <LI> Support for reading SVG font data (though not rendering it yet) * </UL> * * Revisions for "Candy 2" November 2006 by fry * <UL> * <LI> Switch to the new processing.xml library * <LI> Several bug fixes for parsing of shape data * <LI> Support for linear and radial gradients * <LI> Support for additional types of shapes * <LI> Added compound shapes (shapes with interior points) * <LI> Added methods to get shapes from an internal table * </UL> * * Revision 10/31/06 by flux * <UL> * <LI> Now properly supports Processing 0118 * <LI> Fixed a bunch of things for Casey's students and general buggity. * <LI> Will now properly draw #FFFFFFFF colors (were being represented as -1) * <LI> SVGs without <g> tags are now properly caught and loaded * <LI> Added a method customStyle() for overriding SVG colors/styles * <LI> Added a method SVGStyle() to go back to using SVG colors/styles * </UL> * * Some SVG objects and features may not yet be supported. * Here is a partial list of non-included features * <UL> * <LI> Rounded rectangles * <LI> Drop shadow objects * <LI> Typography * <LI> <STRIKE>Layers</STRIKE> added for Candy 2 * <LI> Patterns * <LI> Embedded images * </UL> * * For those interested, the SVG specification can be found * <A HREF="http://www.w3.org/TR/SVG">here</A>. */ public class PShapeSVG extends PShape { XMLElement element; float opacity; float strokeOpacity; float fillOpacity; Gradient strokeGradient; Paint strokeGradientPaint; String strokeName; // id of another object, gradients only? Gradient fillGradient; Paint fillGradientPaint; String fillName; // id of another object /** * Initializes a new SVG Object with the given filename. */ public PShapeSVG(PApplet parent, String filename) { // this will grab the root document, starting <svg ...> // the xml version and initial comments are ignored this(new XMLElement(parent, filename)); } /** * Initializes a new SVG Object from the given XMLElement. */ public PShapeSVG(XMLElement svg) { this(null, svg); if (!svg.getName().equals("svg")) { throw new RuntimeException("root is not <svg>, it's <" + svg.getName() + ">"); } // not proper parsing of the viewBox, but will cover us for cases where // the width and height of the object is not specified String viewBoxStr = svg.getStringAttribute("viewBox"); if (viewBoxStr != null) { int[] viewBox = PApplet.parseInt(PApplet.splitTokens(viewBoxStr)); width = viewBox[2]; height = viewBox[3]; } // TODO if viewbox is not same as width/height, then use it to scale // the original objects. for now, viewbox only used when width/height // are empty values (which by the spec means w/h of "100%" String unitWidth = svg.getStringAttribute("width"); String unitHeight = svg.getStringAttribute("height"); if (unitWidth != null) { width = parseUnitSize(unitWidth); height = parseUnitSize(unitHeight); } else { if ((width == 0) || (height == 0)) { //throw new RuntimeException("width/height not specified"); PGraphics.showWarning("The width and/or height is not " + "readable in the <svg> tag of this file."); // For the spec, the default is 100% and 100%. For purposes // here, insert a dummy value because this is prolly just a // font or something for which the w/h doesn't matter. width = 1; height = 1; } } //root = new Group(null, svg); parseChildren(svg); // ? } public PShapeSVG(PShapeSVG parent, XMLElement properties) { //super(GROUP); if (parent == null) { // set values to their defaults according to the SVG spec stroke = false; strokeColor = 0xff000000; strokeWeight = 1; strokeCap = PConstants.SQUARE; // equivalent to BUTT in svg spec strokeJoin = PConstants.MITER; strokeGradient = null; strokeGradientPaint = null; strokeName = null; fill = true; fillColor = 0xff000000; fillGradient = null; fillGradientPaint = null; fillName = null; //hasTransform = false; //transformation = null; //new float[] { 1, 0, 0, 1, 0, 0 }; strokeOpacity = 1; fillOpacity = 1; opacity = 1; } else { stroke = parent.stroke; strokeColor = parent.strokeColor; strokeWeight = parent.strokeWeight; strokeCap = parent.strokeCap; strokeJoin = parent.strokeJoin; strokeGradient = parent.strokeGradient; strokeGradientPaint = parent.strokeGradientPaint; strokeName = parent.strokeName; fill = parent.fill; fillColor = parent.fillColor; fillGradient = parent.fillGradient; fillGradientPaint = parent.fillGradientPaint; fillName = parent.fillName; //hasTransform = parent.hasTransform; //transformation = parent.transformation; opacity = parent.opacity; } element = properties; name = properties.getStringAttribute("id"); String displayStr = properties.getStringAttribute("display", "inline"); visible = !displayStr.equals("none"); String transformStr = properties.getStringAttribute("transform"); if (transformStr != null) { matrix = parseMatrix(transformStr); } parseColors(properties); parseChildren(properties); } protected void parseChildren(XMLElement graphics) { XMLElement[] elements = graphics.getChildren(); children = new PShape[elements.length]; childCount = 0; for (XMLElement elem : elements) { PShape kid = parseChild(elem); if (kid != null) { addChild(kid); } } } /** * Parse a child XML element. * Override this method to add parsing for more SVG elements. */ protected PShape parseChild(XMLElement elem) { String name = elem.getName(); PShapeSVG shape = null; if (name.equals("g")) { //return new BaseObject(this, elem); shape = new PShapeSVG(this, elem); } else if (name.equals("defs")) { // generally this will contain gradient info, so may // as well just throw it into a group element for parsing //return new BaseObject(this, elem); shape = new PShapeSVG(this, elem); } else if (name.equals("line")) { //return new Line(this, elem); //return new BaseObject(this, elem, LINE); shape = new PShapeSVG(this, elem); shape.parseLine(); } else if (name.equals("circle")) { //return new BaseObject(this, elem, ELLIPSE); shape = new PShapeSVG(this, elem); shape.parseEllipse(true); } else if (name.equals("ellipse")) { //return new BaseObject(this, elem, ELLIPSE); shape = new PShapeSVG(this, elem); shape.parseEllipse(false); } else if (name.equals("rect")) { //return new BaseObject(this, elem, RECT); shape = new PShapeSVG(this, elem); shape.parseRect(); } else if (name.equals("polygon")) { //return new BaseObject(this, elem, POLYGON); shape = new PShapeSVG(this, elem); shape.parsePoly(true); } else if (name.equals("polyline")) { //return new BaseObject(this, elem, POLYGON); shape = new PShapeSVG(this, elem); shape.parsePoly(false); } else if (name.equals("path")) { //return new BaseObject(this, elem, PATH); shape = new PShapeSVG(this, elem); shape.parsePath(); } else if (name.equals("radialGradient")) { return new RadialGradient(this, elem); } else if (name.equals("linearGradient")) { return new LinearGradient(this, elem); } else if (name.equals("text")) { PGraphics.showWarning("Text in SVG files is not currently supported, " + "convert text to outlines instead."); } else if (name.equals("filter")) { PGraphics.showWarning("Filters are not supported."); } else if (name.equals("mask")) { PGraphics.showWarning("Masks are not supported."); } else { PGraphics.showWarning("Ignoring <" + name + "> tag."); } return shape; } protected void parseLine() { kind = LINE; family = PRIMITIVE; params = new float[] { element.getFloatAttribute("x1"), element.getFloatAttribute("y1"), element.getFloatAttribute("x2"), element.getFloatAttribute("y2"), }; // x = params[0]; // y = params[1]; // width = params[2]; // height = params[3]; } /** * Handles parsing ellipse and circle tags. * @param circle true if this is a circle and not an ellipse */ protected void parseEllipse(boolean circle) { kind = ELLIPSE; family = PRIMITIVE; params = new float[4]; params[0] = element.getFloatAttribute("cx"); params[1] = element.getFloatAttribute("cy"); float rx, ry; if (circle) { rx = ry = element.getFloatAttribute("r"); } else { rx = element.getFloatAttribute("rx"); ry = element.getFloatAttribute("ry"); } params[0] -= rx; params[1] -= ry; params[2] = rx*2; params[3] = ry*2; } protected void parseRect() { kind = RECT; family = PRIMITIVE; params = new float[] { element.getFloatAttribute("x"), element.getFloatAttribute("y"), element.getFloatAttribute("width"), element.getFloatAttribute("height"), }; } /** * Parse a polyline or polygon from an SVG file. * @param close true if shape is closed (polygon), false if not (polyline) */ protected void parsePoly(boolean close) { family = PATH; this.close = close; String pointsAttr = element.getStringAttribute("points"); if (pointsAttr != null) { String[] pointsBuffer = PApplet.splitTokens(pointsAttr); vertexCount = pointsBuffer.length; vertices = new float[vertexCount][2]; for (int i = 0; i < vertexCount; i++) { String pb[] = PApplet.split(pointsBuffer[i], ','); vertices[i][X] = Float.valueOf(pb[0]).floatValue(); vertices[i][Y] = Float.valueOf(pb[1]).floatValue(); } } } protected void parsePath() { family = PATH; kind = 0; String pathData = element.getStringAttribute("d"); if (pathData == null) return; char[] pathDataChars = pathData.toCharArray(); StringBuffer pathBuffer = new StringBuffer(); boolean lastSeparate = false; for (int i = 0; i < pathDataChars.length; i++) { char c = pathDataChars[i]; boolean separate = false; if (c == 'M' || c == 'm' || c == 'L' || c == 'l' || c == 'H' || c == 'h' || c == 'V' || c == 'v' || c == 'C' || c == 'c' || // beziers c == 'S' || c == 's' || c == 'Q' || c == 'q' || // quadratic beziers c == 'T' || c == 't' || c == 'Z' || c == 'z' || // closepath c == ',') { separate = true; if (i != 0) { pathBuffer.append("|"); } } if (c == 'Z' || c == 'z') { separate = false; } if (c == '-' && !lastSeparate) { // allow for 'e' notation in numbers, e.g. 2.10e-9 // http://dev.processing.org/bugs/show_bug.cgi?id=1408 if (i == 0 || pathDataChars[i-1] != 'e') { pathBuffer.append("|"); } } if (c != ',') { pathBuffer.append(c); //"" + pathDataBuffer.charAt(i)); } if (separate && c != ',' && c != '-') { pathBuffer.append("|"); } lastSeparate = separate; } // use whitespace constant to get rid of extra spaces and CR or LF String[] pathDataKeys = PApplet.splitTokens(pathBuffer.toString(), "|" + WHITESPACE); vertices = new float[pathDataKeys.length][2]; vertexCodes = new int[pathDataKeys.length]; float cx = 0; float cy = 0; int i = 0; while (i < pathDataKeys.length) { char c = pathDataKeys[i].charAt(0); switch (c) { case 'M': // M - move to (absolute) cx = PApplet.parseFloat(pathDataKeys[i + 1]); cy = PApplet.parseFloat(pathDataKeys[i + 2]); parsePathMoveto(cx, cy); i += 3; break; case 'm': // m - move to (relative) cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]); parsePathMoveto(cx, cy); i += 3; break; case 'L': cx = PApplet.parseFloat(pathDataKeys[i + 1]); cy = PApplet.parseFloat(pathDataKeys[i + 2]); parsePathLineto(cx, cy); i += 3; break; case 'l': cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]); parsePathLineto(cx, cy); i += 3; break; // horizontal lineto absolute case 'H': cx = PApplet.parseFloat(pathDataKeys[i + 1]); parsePathLineto(cx, cy); i += 2; break; // horizontal lineto relative case 'h': cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); parsePathLineto(cx, cy); i += 2; break; case 'V': cy = PApplet.parseFloat(pathDataKeys[i + 1]); parsePathLineto(cx, cy); i += 2; break; case 'v': cy = cy + PApplet.parseFloat(pathDataKeys[i + 1]); parsePathLineto(cx, cy); i += 2; break; // C - curve to (absolute) case 'C': { float ctrlX1 = PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY1 = PApplet.parseFloat(pathDataKeys[i + 2]); float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 3]); float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 4]); float endX = PApplet.parseFloat(pathDataKeys[i + 5]); float endY = PApplet.parseFloat(pathDataKeys[i + 6]); parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 7; } break; // c - curve to (relative) case 'c': { float ctrlX1 = cx + PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY1 = cy + PApplet.parseFloat(pathDataKeys[i + 2]); float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 3]); float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 4]); float endX = cx + PApplet.parseFloat(pathDataKeys[i + 5]); float endY = cy + PApplet.parseFloat(pathDataKeys[i + 6]); parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 7; } break; // S - curve to shorthand (absolute) case 'S': { float ppx = vertices[vertexCount-2][X]; float ppy = vertices[vertexCount-2][Y]; float px = vertices[vertexCount-1][X]; float py = vertices[vertexCount-1][Y]; float ctrlX1 = px + (px - ppx); float ctrlY1 = py + (py - ppy); float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 2]); float endX = PApplet.parseFloat(pathDataKeys[i + 3]); float endY = PApplet.parseFloat(pathDataKeys[i + 4]); parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 5; } break; // s - curve to shorthand (relative) case 's': { float ppx = vertices[vertexCount-2][X]; float ppy = vertices[vertexCount-2][Y]; float px = vertices[vertexCount-1][X]; float py = vertices[vertexCount-1][Y]; float ctrlX1 = px + (px - ppx); float ctrlY1 = py + (py - ppy); float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 2]); float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]); float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]); parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 5; } break; // Q - quadratic curve to (absolute) case 'Q': { float ctrlX = PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY = PApplet.parseFloat(pathDataKeys[i + 2]); float endX = PApplet.parseFloat(pathDataKeys[i + 3]); float endY = PApplet.parseFloat(pathDataKeys[i + 4]); parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 5; } break; // q - quadratic curve to (relative) case 'q': { float ctrlX = cx + PApplet.parseFloat(pathDataKeys[i + 1]); float ctrlY = cy + PApplet.parseFloat(pathDataKeys[i + 2]); float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]); float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]); parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 5; } break; // T - quadratic curve to shorthand (absolute) // The control point is assumed to be the reflection of the // control point on the previous command relative to the // current point. (If there is no previous command or if the // previous command was not a Q, q, T or t, assume the control // point is coincident with the current point.) case 'T': { float ppx = vertices[vertexCount-2][X]; float ppy = vertices[vertexCount-2][Y]; float px = vertices[vertexCount-1][X]; float py = vertices[vertexCount-1][Y]; float ctrlX = px + (px - ppx); float ctrlY = py + (py - ppy); float endX = PApplet.parseFloat(pathDataKeys[i + 1]); float endY = PApplet.parseFloat(pathDataKeys[i + 2]); parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 3; } break; // t - quadratic curve to shorthand (relative) case 't': { float ppx = vertices[vertexCount-2][X]; float ppy = vertices[vertexCount-2][Y]; float px = vertices[vertexCount-1][X]; float py = vertices[vertexCount-1][Y]; float ctrlX = px + (px - ppx); float ctrlY = py + (py - ppy); float endX = cx + PApplet.parseFloat(pathDataKeys[i + 1]); float endY = cy + PApplet.parseFloat(pathDataKeys[i + 2]); parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 3; } break; case 'Z': case 'z': close = true; i++; break; default: String parsed = PApplet.join(PApplet.subset(pathDataKeys, 0, i), ","); String unparsed = PApplet.join(PApplet.subset(pathDataKeys, i), ","); System.err.println("parsed: " + parsed); System.err.println("unparsed: " + unparsed); if (pathDataKeys[i].equals("a") || pathDataKeys[i].equals("A")) { String msg = "Sorry, elliptical arc support for SVG files " + "is not yet implemented (See bug #996 for details)"; throw new RuntimeException(msg); } throw new RuntimeException("shape command not handled: " + pathDataKeys[i]); } } } // private void parsePathCheck(int num) { // if (vertexCount + num-1 >= vertices.length) { // //vertices = (float[][]) PApplet.expand(vertices); // float[][] temp = new float[vertexCount << 1][2]; // System.arraycopy(vertices, 0, temp, 0, vertexCount); // vertices = temp; // } // } private void parsePathVertex(float x, float y) { if (vertexCount == vertices.length) { //vertices = (float[][]) PApplet.expand(vertices); float[][] temp = new float[vertexCount << 1][2]; System.arraycopy(vertices, 0, temp, 0, vertexCount); vertices = temp; } vertices[vertexCount][X] = x; vertices[vertexCount][Y] = y; vertexCount++; } private void parsePathCode(int what) { if (vertexCodeCount == vertexCodes.length) { vertexCodes = PApplet.expand(vertexCodes); } vertexCodes[vertexCodeCount++] = what; } private void parsePathMoveto(float px, float py) { if (vertexCount > 0) { parsePathCode(BREAK); } parsePathCode(VERTEX); parsePathVertex(px, py); } private void parsePathLineto(float px, float py) { parsePathCode(VERTEX); parsePathVertex(px, py); } private void parsePathCurveto(float x1, float y1, float x2, float y2, float x3, float y3) { parsePathCode(BEZIER_VERTEX); parsePathVertex(x1, y1); parsePathVertex(x2, y2); parsePathVertex(x3, y3); } private void parsePathQuadto(float x1, float y1, float cx, float cy, float x2, float y2) { parsePathCode(BEZIER_VERTEX); // x1/y1 already covered by last moveto, lineto, or curveto parsePathVertex(x1 + ((cx-x1)*2/3.0f), y1 + ((cy-y1)*2/3.0f)); parsePathVertex(x2 + ((cx-x2)*2/3.0f), y2 + ((cy-y2)*2/3.0f)); parsePathVertex(x2, y2); } /** * Parse the specified SVG matrix into a PMatrix2D. Note that PMatrix2D * is rotated relative to the SVG definition, so parameters are rearranged * here. More about the transformation matrices in * <a href="http://www.w3.org/TR/SVG/coords.html#TransformAttribute">this section</a> * of the SVG documentation. * @param matrixStr text of the matrix param. * @return a good old-fashioned PMatrix2D */ static protected PMatrix2D parseMatrix(String matrixStr) { String[] pieces = PApplet.match(matrixStr, "\\s*(\\w+)\\((.*)\\)"); if (pieces == null) { System.err.println("Could not parse transform " + matrixStr); return null; } float[] m = PApplet.parseFloat(PApplet.splitTokens(pieces[2], ", ")); if (pieces[1].equals("matrix")) { return new PMatrix2D(m[0], m[2], m[4], m[1], m[3], m[5]); } else if (pieces[1].equals("translate")) { float tx = m[0]; float ty = (m.length == 2) ? m[1] : m[0]; //return new float[] { 1, 0, tx, 0, 1, ty }; return new PMatrix2D(1, 0, tx, 0, 1, ty); } else if (pieces[1].equals("scale")) { float sx = m[0]; float sy = (m.length == 2) ? m[1] : m[0]; //return new float[] { sx, 0, 0, 0, sy, 0 }; return new PMatrix2D(sx, 0, 0, 0, sy, 0); } else if (pieces[1].equals("rotate")) { float angle = m[0]; if (m.length == 1) { float c = PApplet.cos(angle); float s = PApplet.sin(angle); // SVG version is cos(a) sin(a) -sin(a) cos(a) 0 0 return new PMatrix2D(c, -s, 0, s, c, 0); } else if (m.length == 3) { PMatrix2D mat = new PMatrix2D(0, 1, m[1], 1, 0, m[2]); mat.rotate(m[0]); mat.translate(-m[1], -m[2]); return mat; //.get(null); } } else if (pieces[1].equals("skewX")) { return new PMatrix2D(1, 0, 1, PApplet.tan(m[0]), 0, 0); } else if (pieces[1].equals("skewY")) { return new PMatrix2D(1, 0, 1, 0, PApplet.tan(m[0]), 0); } return null; } protected void parseColors(XMLElement properties) { if (properties.hasAttribute("opacity")) { String opacityText = properties.getStringAttribute("opacity"); setOpacity(opacityText); } if (properties.hasAttribute("stroke")) { String strokeText = properties.getStringAttribute("stroke"); setStroke(strokeText); } if (properties.hasAttribute("stroke-width")) { // if NaN (i.e. if it's 'inherit') then default back to the inherit setting String lineweight = properties.getStringAttribute("stroke-width"); setStrokeWeight(lineweight); } if (properties.hasAttribute("stroke-linejoin")) { String linejoin = properties.getStringAttribute("stroke-linejoin"); setStrokeJoin(linejoin); } if (properties.hasAttribute("stroke-linecap")) { String linecap = properties.getStringAttribute("stroke-linecap"); setStrokeCap(linecap); } // fill defaults to black (though stroke defaults to "none") // http://www.w3.org/TR/SVG/painting.html#FillProperties if (properties.hasAttribute("fill")) { String fillText = properties.getStringAttribute("fill"); setFill(fillText); } if (properties.hasAttribute("style")) { String styleText = properties.getStringAttribute("style"); String[] styleTokens = PApplet.splitTokens(styleText, ";"); //PApplet.println(styleTokens); for (int i = 0; i < styleTokens.length; i++) { String[] tokens = PApplet.splitTokens(styleTokens[i], ":"); //PApplet.println(tokens); tokens[0] = PApplet.trim(tokens[0]); if (tokens[0].equals("fill")) { setFill(tokens[1]); } else if(tokens[0].equals("fill-opacity")) { setFillOpacity(tokens[1]); } else if(tokens[0].equals("stroke")) { setStroke(tokens[1]); } else if(tokens[0].equals("stroke-width")) { setStrokeWeight(tokens[1]); } else if(tokens[0].equals("stroke-linecap")) { setStrokeCap(tokens[1]); } else if(tokens[0].equals("stroke-linejoin")) { setStrokeJoin(tokens[1]); } else if(tokens[0].equals("stroke-opacity")) { setStrokeOpacity(tokens[1]); } else if(tokens[0].equals("opacity")) { setOpacity(tokens[1]); } else { // Other attributes are not yet implemented } } } } void setOpacity(String opacityText) { opacity = PApplet.parseFloat(opacityText); strokeColor = ((int) (opacity * 255)) << 24 | strokeColor & 0xFFFFFF; fillColor = ((int) (opacity * 255)) << 24 | fillColor & 0xFFFFFF; } void setStrokeWeight(String lineweight) { strokeWeight = PApplet.parseFloat(lineweight); } void setStrokeOpacity(String opacityText) { strokeOpacity = PApplet.parseFloat(opacityText); strokeColor = ((int) (strokeOpacity * 255)) << 24 | strokeColor & 0xFFFFFF; } void setStroke(String strokeText) { int opacityMask = strokeColor & 0xFF000000; if (strokeText.equals("none")) { stroke = false; } else if (strokeText.startsWith("#")) { stroke = true; strokeColor = opacityMask | (Integer.parseInt(strokeText.substring(1), 16)) & 0xFFFFFF; } else if (strokeText.startsWith("rgb")) { stroke = true; strokeColor = opacityMask | parseRGB(strokeText); } else if (strokeText.startsWith("url(#")) { strokeName = strokeText.substring(5, strokeText.length() - 1); Object strokeObject = findChild(strokeName); if (strokeObject instanceof Gradient) { strokeGradient = (Gradient) strokeObject; strokeGradientPaint = calcGradientPaint(strokeGradient); //, opacity); } else { System.err.println("url " + strokeName + " refers to unexpected data"); } } } void setStrokeJoin(String linejoin) { if (linejoin.equals("inherit")) { // do nothing, will inherit automatically } else if (linejoin.equals("miter")) { strokeJoin = PConstants.MITER; } else if (linejoin.equals("round")) { strokeJoin = PConstants.ROUND; } else if (linejoin.equals("bevel")) { strokeJoin = PConstants.BEVEL; } } void setStrokeCap(String linecap) { if (linecap.equals("inherit")) { // do nothing, will inherit automatically } else if (linecap.equals("butt")) { strokeCap = PConstants.SQUARE; } else if (linecap.equals("round")) { strokeCap = PConstants.ROUND; } else if (linecap.equals("square")) { strokeCap = PConstants.PROJECT; } } void setFillOpacity(String opacityText) { fillOpacity = PApplet.parseFloat(opacityText); fillColor = ((int) (fillOpacity * 255)) << 24 | fillColor & 0xFFFFFF; } void setFill(String fillText) { int opacityMask = fillColor & 0xFF000000; if (fillText.equals("none")) { fill = false; } else if (fillText.startsWith("#")) { fill = true; fillColor = opacityMask | (Integer.parseInt(fillText.substring(1), 16)) & 0xFFFFFF; //System.out.println("hex for fill is " + PApplet.hex(fillColor)); } else if (fillText.startsWith("rgb")) { fill = true; fillColor = opacityMask | parseRGB(fillText); } else if (fillText.startsWith("url(#")) { fillName = fillText.substring(5, fillText.length() - 1); //PApplet.println("looking for " + fillName); Object fillObject = findChild(fillName); //PApplet.println("found " + fillObject); if (fillObject instanceof Gradient) { fill = true; fillGradient = (Gradient) fillObject; fillGradientPaint = calcGradientPaint(fillGradient); //, opacity); //PApplet.println("got filla " + fillObject); } else { System.err.println("url " + fillName + " refers to unexpected data"); } } } static protected int parseRGB(String what) { int leftParen = what.indexOf('(') + 1; int rightParen = what.indexOf(')'); String sub = what.substring(leftParen, rightParen); int[] values = PApplet.parseInt(PApplet.splitTokens(sub, ", ")); return (values[0] << 16) | (values[1] << 8) | (values[2]); } static protected HashMap<String, String> parseStyleAttributes(String style) { HashMap<String, String> table = new HashMap<String, String>(); String[] pieces = style.split(";"); for (int i = 0; i < pieces.length; i++) { String[] parts = pieces[i].split(":"); table.put(parts[0], parts[1]); } return table; } /** * Parse a size that may have a suffix for its units. * Ignoring cases where this could also be a percentage. * The <A HREF="http://www.w3.org/TR/SVG/coords.html#Units">units</A> spec: * <UL> * <LI>"1pt" equals "1.25px" (and therefore 1.25 user units) * <LI>"1pc" equals "15px" (and therefore 15 user units) * <LI>"1mm" would be "3.543307px" (3.543307 user units) * <LI>"1cm" equals "35.43307px" (and therefore 35.43307 user units) * <LI>"1in" equals "90px" (and therefore 90 user units) * </UL> */ static protected float parseUnitSize(String text) { int len = text.length() - 2; if (text.endsWith("pt")) { return PApplet.parseFloat(text.substring(0, len)) * 1.25f; } else if (text.endsWith("pc")) { return PApplet.parseFloat(text.substring(0, len)) * 15; } else if (text.endsWith("mm")) { return PApplet.parseFloat(text.substring(0, len)) * 3.543307f; } else if (text.endsWith("cm")) { return PApplet.parseFloat(text.substring(0, len)) * 35.43307f; } else if (text.endsWith("in")) { return PApplet.parseFloat(text.substring(0, len)) * 90; } else if (text.endsWith("px")) { return PApplet.parseFloat(text.substring(0, len)); } else { return PApplet.parseFloat(text); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . static class Gradient extends PShapeSVG { AffineTransform transform; float[] offset; int[] color; int count; public Gradient(PShapeSVG parent, XMLElement properties) { super(parent, properties); XMLElement elements[] = properties.getChildren(); offset = new float[elements.length]; color = new int[elements.length]; // <stop offset="0" style="stop-color:#967348"/> for (int i = 0; i < elements.length; i++) { XMLElement elem = elements[i]; String name = elem.getName(); if (name.equals("stop")) { offset[count] = elem.getFloatAttribute("offset"); String style = elem.getStringAttribute("style"); HashMap<String, String> styles = parseStyleAttributes(style); String colorStr = styles.get("stop-color"); if (colorStr == null) colorStr = "#000000"; String opacityStr = styles.get("stop-opacity"); if (opacityStr == null) opacityStr = "1"; int tupacity = (int) (PApplet.parseFloat(opacityStr) * 255); color[count] = (tupacity << 24) | Integer.parseInt(colorStr.substring(1), 16); count++; } } offset = PApplet.subset(offset, 0, count); color = PApplet.subset(color, 0, count); } } class LinearGradient extends Gradient { float x1, y1, x2, y2; public LinearGradient(PShapeSVG parent, XMLElement properties) { super(parent, properties); this.x1 = properties.getFloatAttribute("x1"); this.y1 = properties.getFloatAttribute("y1"); this.x2 = properties.getFloatAttribute("x2"); this.y2 = properties.getFloatAttribute("y2"); String transformStr = properties.getStringAttribute("gradientTransform"); if (transformStr != null) { float t[] = parseMatrix(transformStr).get(null); this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]); Point2D t1 = transform.transform(new Point2D.Float(x1, y1), null); Point2D t2 = transform.transform(new Point2D.Float(x2, y2), null); this.x1 = (float) t1.getX(); this.y1 = (float) t1.getY(); this.x2 = (float) t2.getX(); this.y2 = (float) t2.getY(); } } } class RadialGradient extends Gradient { float cx, cy, r; public RadialGradient(PShapeSVG parent, XMLElement properties) { super(parent, properties); this.cx = properties.getFloatAttribute("cx"); this.cy = properties.getFloatAttribute("cy"); this.r = properties.getFloatAttribute("r"); String transformStr = properties.getStringAttribute("gradientTransform"); if (transformStr != null) { float t[] = parseMatrix(transformStr).get(null); this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]); Point2D t1 = transform.transform(new Point2D.Float(cx, cy), null); Point2D t2 = transform.transform(new Point2D.Float(cx + r, cy), null); this.cx = (float) t1.getX(); this.cy = (float) t1.getY(); this.r = (float) (t2.getX() - t1.getX()); } } } class LinearGradientPaint implements Paint { float x1, y1, x2, y2; float[] offset; int[] color; int count; float opacity; public LinearGradientPaint(float x1, float y1, float x2, float y2, float[] offset, int[] color, int count, float opacity) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; this.offset = offset; this.color = color; this.count = count; this.opacity = opacity; } public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) { Point2D t1 = xform.transform(new Point2D.Float(x1, y1), null); Point2D t2 = xform.transform(new Point2D.Float(x2, y2), null); return new LinearGradientContext((float) t1.getX(), (float) t1.getY(), (float) t2.getX(), (float) t2.getY()); } public int getTransparency() { return TRANSLUCENT; // why not.. rather than checking each color } public class LinearGradientContext implements PaintContext { int ACCURACY = 2; float tx1, ty1, tx2, ty2; public LinearGradientContext(float tx1, float ty1, float tx2, float ty2) { this.tx1 = tx1; this.ty1 = ty1; this.tx2 = tx2; this.ty2 = ty2; } public void dispose() { } public ColorModel getColorModel() { return ColorModel.getRGBdefault(); } public Raster getRaster(int x, int y, int w, int h) { WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h); int[] data = new int[w * h * 4]; // make normalized version of base vector float nx = tx2 - tx1; float ny = ty2 - ty1; float len = (float) Math.sqrt(nx*nx + ny*ny); if (len != 0) { nx /= len; ny /= len; } int span = (int) PApplet.dist(tx1, ty1, tx2, ty2) * ACCURACY; if (span <= 0) { //System.err.println("span is too small"); // annoying edge case where the gradient isn't legit int index = 0; for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { data[index++] = 0; data[index++] = 0; data[index++] = 0; data[index++] = 255; } } } else { int[][] interp = new int[span][4]; int prev = 0; for (int i = 1; i < count; i++) { int c0 = color[i-1]; int c1 = color[i]; int last = (int) (offset[i] * (span-1)); //System.out.println("last is " + last); for (int j = prev; j <= last; j++) { float btwn = PApplet.norm(j, prev, last); interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn); interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn); interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn); interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity); //System.out.println(j + " " + interp[j][0] + " " + interp[j][1] + " " + interp[j][2]); } prev = last; } int index = 0; for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { //float distance = 0; //PApplet.dist(cx, cy, x + i, y + j); //int which = PApplet.min((int) (distance * ACCURACY), interp.length-1); float px = (x + i) - tx1; float py = (y + j) - ty1; // distance up the line is the dot product of the normalized // vector of the gradient start/stop by the point being tested int which = (int) ((px*nx + py*ny) * ACCURACY); if (which < 0) which = 0; if (which > interp.length-1) which = interp.length-1; //if (which > 138) System.out.println("grabbing " + which); data[index++] = interp[which][0]; data[index++] = interp[which][1]; data[index++] = interp[which][2]; data[index++] = interp[which][3]; } } } raster.setPixels(0, 0, w, h, data); return raster; } } } class RadialGradientPaint implements Paint { float cx, cy, radius; float[] offset; int[] color; int count; float opacity; public RadialGradientPaint(float cx, float cy, float radius, float[] offset, int[] color, int count, float opacity) { this.cx = cx; this.cy = cy; this.radius = radius; this.offset = offset; this.color = color; this.count = count; this.opacity = opacity; } public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) { return new RadialGradientContext(); } public int getTransparency() { return TRANSLUCENT; } public class RadialGradientContext implements PaintContext { int ACCURACY = 5; public void dispose() {} public ColorModel getColorModel() { return ColorModel.getRGBdefault(); } public Raster getRaster(int x, int y, int w, int h) { WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h); int span = (int) radius * ACCURACY; int[][] interp = new int[span][4]; int prev = 0; for (int i = 1; i < count; i++) { int c0 = color[i-1]; int c1 = color[i]; int last = (int) (offset[i] * (span - 1)); for (int j = prev; j <= last; j++) { float btwn = PApplet.norm(j, prev, last); interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn); interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn); interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn); interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity); } prev = last; } int[] data = new int[w * h * 4]; int index = 0; for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { float distance = PApplet.dist(cx, cy, x + i, y + j); int which = PApplet.min((int) (distance * ACCURACY), interp.length-1); data[index++] = interp[which][0]; data[index++] = interp[which][1]; data[index++] = interp[which][2]; data[index++] = interp[which][3]; } } raster.setPixels(0, 0, w, h, data); return raster; } } } protected Paint calcGradientPaint(Gradient gradient) { if (gradient instanceof LinearGradient) { LinearGradient grad = (LinearGradient) gradient; return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2, grad.offset, grad.color, grad.count, opacity); } else if (gradient instanceof RadialGradient) { RadialGradient grad = (RadialGradient) gradient; return new RadialGradientPaint(grad.cx, grad.cy, grad.r, grad.offset, grad.color, grad.count, opacity); } return null; } // protected Paint calcGradientPaint(Gradient gradient, // float x1, float y1, float x2, float y2) { // if (gradient instanceof LinearGradient) { // LinearGradient grad = (LinearGradient) gradient; // return new LinearGradientPaint(x1, y1, x2, y2, // grad.offset, grad.color, grad.count, // opacity); // } // throw new RuntimeException("Not a linear gradient."); // } // protected Paint calcGradientPaint(Gradient gradient, // float cx, float cy, float r) { // if (gradient instanceof RadialGradient) { // RadialGradient grad = (RadialGradient) gradient; // return new RadialGradientPaint(cx, cy, r, // grad.offset, grad.color, grad.count, // opacity); // } // throw new RuntimeException("Not a radial gradient."); // } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected void styles(PGraphics g) { super.styles(g); if (g instanceof PGraphicsJava2D) { PGraphicsJava2D p2d = (PGraphicsJava2D) g; if (strokeGradient != null) { p2d.strokeGradient = true; p2d.strokeGradientObject = strokeGradientPaint; } else { // need to shut off, in case parent object has a gradient applied //p2d.strokeGradient = false; } if (fillGradient != null) { p2d.fillGradient = true; p2d.fillGradientObject = fillGradientPaint; } else { // need to shut off, in case parent object has a gradient applied //p2d.fillGradient = false; } } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . //public void drawImpl(PGraphics g) { // do nothing //} // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Get a particular element based on its SVG ID. When editing SVG by hand, * this is the id="" tag on any SVG element. When editing from Illustrator, * these IDs can be edited by expanding the layers palette. The names used * in the layers palette, both for the layers or the shapes and groups * beneath them can be used here. * <PRE> * // This code grabs "Layer 3" and the shapes beneath it. * SVG layer3 = svg.getChild("Layer 3"); * </PRE> */ public PShape getChild(String name) { PShape found = super.getChild(name); if (found == null) { // Otherwise try with underscores instead of spaces // (this is how Illustrator handles spaces in the layer names). found = super.getChild(name.replace(' ', '_')); } // Set bounding box based on the parent bounding box if (found != null) { // found.x = this.x; // found.y = this.y; found.width = this.width; found.height = this.height; } return found; } /** * Prints out the SVG document. Useful for parsing. */ public void print() { PApplet.println(element.toString()); } }