FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GitHub Viewer

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2012-15 The Processing Foundation Copyright (c) 2006-12 Ben Fry and Casey Reas Copyright (c) 2004-06 Michael Chang This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.core; import processing.data.*; // TODO replace these with PMatrix2D import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.Map; import java.util.HashMap; /** * This class is not part of the Processing API and should not be used * directly. Instead, use loadShape() and methods like it, which will make * use of this class. Using this class directly will cause your code to break * when combined with future versions of Processing. *

* 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. * View the SVG specification here. *

* We have no intention of turning this into a full-featured SVG library. * The goal of this project is a basic shape importer that originally was small * enough to be included with applets, meaning that its download size should be * in the neighborhood of 25-30 Kb. Though we're far less limited nowadays on * size constraints, we remain extremely limited in terms of time, and do not * have volunteers who are available to maintain a larger SVG library. *

* For more sophisticated import/export, consider the * Batik * library from the Apache Software Foundation. *

* Batik is used in the SVG Export library in Processing 3, however using it * for full SVG import is still a considerable amount of work. Wiring it to * Java2D wouldn't be too bad, but using it with OpenGL, JavaFX, and features * like begin/endRecord() and begin/endRaw() would be considerable effort. *

* Future improvements to this library may focus on this properly supporting * a specific subset of SVG, for instance the simpler SVG profiles known as * SVG Tiny or Basic, * although we still would not support the interactivity options. * *


* * A minimal example program using SVG: * (assuming a working moo.svg is in your data folder) * *

 * PShape moo;
 *
 * void setup() {
 *   size(400, 400);
 *   moo = loadShape("moo.svg");
 * }
 * void draw() {
 *   background(255);
 *   shape(moo, mouseX, mouseY);
 * }
 * 
*/ public class PShapeSVG extends PShape { XML element; /// Values between 0 and 1. protected float opacity; float strokeOpacity; float fillOpacity; /** Width of containing SVG (used for percentages). */ protected float svgWidth; /** Height of containing SVG (used for percentages). */ protected float svgHeight; /** √((w² + h²)/2) of containing SVG (used for percentages). */ protected float svgSizeXY; protected Gradient strokeGradient; String strokeName; // id of another object, gradients only? protected Gradient fillGradient; String fillName; // id of another object /** * Initializes a new SVG object from the given XML object. */ public PShapeSVG(XML svg) { this(null, svg, true); if (!svg.getName().equals("svg")) { if (svg.getName().toLowerCase().equals("html")) { // Common case is that files aren't downloaded properly throw new RuntimeException("This appears to be a web page, not an SVG file."); } else { throw new RuntimeException("The root node is not , it's "); } } } protected PShapeSVG(PShapeSVG parent, XML properties, boolean parseKids) { setParent(parent); // Need to get width/height in early. if (properties.getName().equals("svg")) { String unitWidth = properties.getString("width"); String unitHeight = properties.getString("height"); // Can't handle width/height as percentages easily. I'm just going // to put in 100 as a dummy value, beacuse this means that it will // come out as a reasonable value. if (unitWidth != null) width = parseUnitSize(unitWidth, 100); if (unitHeight != null) height = parseUnitSize(unitHeight, 100); String viewBoxStr = properties.getString("viewBox"); if (viewBoxStr != null) { float[] viewBox = PApplet.parseFloat(PApplet.splitTokens(viewBoxStr)); if (unitWidth == null || unitHeight == null) { // Not proper parsing of the viewBox, but will cover us for cases where // the width and height of the object is not specified. width = viewBox[2]; height = viewBox[3]; } else { // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute // TODO: preserveAspectRatio. if (matrix == null) matrix = new PMatrix2D(); matrix.scale(width/viewBox[2], height/viewBox[3]); matrix.translate(-viewBox[0], -viewBox[1]); } } // Negative size is illegal. if (width < 0 || height < 0) throw new RuntimeException(": width (" + width + ") and height (" + height + ") must not be negative."); // It's technically valid to have width or height == 0. Not specified at // all is what to test for. if ((unitWidth == null || unitHeight == null) && viewBoxStr == null) { //throw new RuntimeException("width/height not specified"); PGraphics.showWarning("The width and/or height is not " + "readable in the 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; } svgWidth = width; svgHeight = height; svgSizeXY = PApplet.sqrt((svgWidth*svgWidth + svgHeight*svgHeight)/2.0f); } element = properties; name = properties.getString("id"); // @#$(* adobe illustrator mangles names of objects when re-saving if (name != null) { while (true) { String[] m = PApplet.match(name, "_x([A-Za-z0-9]{2})_"); if (m == null) break; char repair = (char) PApplet.unhex(m[1]); name = name.replace(m[0], "" + repair); } } String displayStr = properties.getString("display", "inline"); visible = !displayStr.equals("none"); String transformStr = properties.getString("transform"); if (transformStr != null) { if (matrix == null) { matrix = parseTransform(transformStr); } else { matrix.preApply(parseTransform(transformStr)); } } if (parseKids) { parseColors(properties); parseChildren(properties); } } // Broken out so that subclasses can copy any additional variables // (i.e. fillGradientPaint and strokeGradientPaint) protected void setParent(PShapeSVG parent) { // Need to set this so that findChild() works. // Otherwise 'parent' is null until addChild() is called later. this.parent = parent; 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 }; // svgWidth, svgHeight, and svgXYSize done below. 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; svgWidth = parent.svgWidth; svgHeight = parent.svgHeight; svgSizeXY = parent.svgSizeXY; opacity = parent.opacity; } // The rect and ellipse modes are set to CORNER since it is the expected // mode for svg shapes. rectMode = CORNER; ellipseMode = CORNER; } /** Factory method for subclasses. */ protected PShapeSVG createShape(PShapeSVG parent, XML properties, boolean parseKids) { return new PShapeSVG(parent, properties, parseKids); } protected void parseChildren(XML graphics) { XML[] elements = graphics.getChildren(); children = new PShape[elements.length]; childCount = 0; for (XML elem : elements) { PShape kid = parseChild(elem); if (kid != null) addChild(kid); } children = (PShape[]) PApplet.subset(children, 0, childCount); } /** * Parse a child XML element. * Override this method to add parsing for more SVG elements. */ protected PShape parseChild(XML elem) { // System.err.println("parsing child in pshape " + elem.getName()); String name = elem.getName(); PShapeSVG shape = null; if (name == null) { // just some whitespace that can be ignored (hopefully) } else if (name.equals("g")) { shape = createShape(this, elem, true); } else if (name.equals("defs")) { // generally this will contain gradient info, so may // as well just throw it into a group element for parsing shape = createShape(this, elem, true); } else if (name.equals("line")) { shape = createShape(this, elem, true); shape.parseLine(); } else if (name.equals("circle")) { shape = createShape(this, elem, true); shape.parseEllipse(true); } else if (name.equals("ellipse")) { shape = createShape(this, elem, true); shape.parseEllipse(false); } else if (name.equals("rect")) { shape = createShape(this, elem, true); shape.parseRect(); } else if (name.equals("polygon")) { shape = createShape(this, elem, true); shape.parsePoly(true); } else if (name.equals("polyline")) { shape = createShape(this, elem, true); shape.parsePoly(false); } else if (name.equals("path")) { shape = createShape(this, elem, true); 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("font")) { return new Font(this, elem); // } else if (name.equals("font-face")) { // return new FontFace(this, elem); // } else if (name.equals("glyph") || name.equals("missing-glyph")) { // return new FontGlyph(this, elem); } else if (name.equals("text")) { // || name.equals("font")) { PGraphics.showWarning("Text and fonts in SVG files are " + "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 if (name.equals("pattern")) { PGraphics.showWarning("Patterns are not supported."); } else if (name.equals("stop")) { // stop tag is handled by gradient parser, so don't warn about it } else if (name.equals("sodipodi:namedview")) { // these are always in Inkscape files, the warnings get tedious } else if (name.equals("metadata") || name.equals("title") || name.equals("desc")) { // fontforge just stuffs in as a comment. // All harmless stuff, irrelevant to rendering. return null; } else if (!name.startsWith("#")) { PGraphics.showWarning("Ignoring tag."); // new Exception().printStackTrace(); } return shape; } protected void parseLine() { kind = LINE; family = PRIMITIVE; params = new float[] { getFloatWithUnit(element, "x1", svgWidth), getFloatWithUnit(element, "y1", svgHeight), getFloatWithUnit(element, "x2", svgWidth), getFloatWithUnit(element, "y2", svgHeight) }; } /** * 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] = getFloatWithUnit(element, "cx", svgWidth); params[1] = getFloatWithUnit(element, "cy", svgHeight); float rx, ry; if (circle) { rx = ry = getFloatWithUnit(element, "r", svgSizeXY); } else { rx = getFloatWithUnit(element, "rx", svgWidth); ry = getFloatWithUnit(element, "ry", svgHeight); } params[0] -= rx; params[1] -= ry; params[2] = rx*2; params[3] = ry*2; } protected void parseRect() { kind = RECT; family = PRIMITIVE; params = new float[] { getFloatWithUnit(element, "x", svgWidth), getFloatWithUnit(element, "y", svgHeight), getFloatWithUnit(element, "width", svgWidth), getFloatWithUnit(element, "height", svgHeight) }; } /** * Parse a polyline or polygon from an SVG file. * Syntax defined at http://www.w3.org/TR/SVG/shapes.html#PointsBNF * @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.getString("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.splitTokens(pointsBuffer[i], ", \t\r\n"); vertices[i][X] = Float.parseFloat(pb[0]); vertices[i][Y] = Float.parseFloat(pb[1]); } } } protected void parsePath() { family = PATH; kind = 0; String pathData = element.getString("d"); if (pathData == null || PApplet.trim(pathData).length() == 0) { return; } char[] pathDataChars = pathData.toCharArray(); StringBuilder pathBuffer = new StringBuilder(); 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 == 'A' || c == 'a' || // elliptical arc 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[] pathTokens = PApplet.splitTokens(pathBuffer.toString(), "|" + WHITESPACE); vertices = new float[pathTokens.length][2]; vertexCodes = new int[pathTokens.length]; float cx = 0; float cy = 0; int i = 0; char implicitCommand = '\0'; // char prevCommand = '\0'; boolean prevCurve = false; float ctrlX, ctrlY; // store values for closepath so that relative coords work properly float movetoX = 0; float movetoY = 0; while (i < pathTokens.length) { char c = pathTokens[i].charAt(0); if (((c >= '0' && c = vertices.length) { // //vertices = (float[][]) PApplet.expand(vertices); // float[][] temp = new float[vertexCount 1) { // No solution, scale ellipse up according to SVG standard float sqrtA = PApplet.sqrt(A); rx *= sqrtA; cxr = 0; ry *= sqrtA; cyr = 0; } else { float k = ((fa == fs) ? -1f : 1f) * PApplet.sqrt((rx*rx * ry*ry) / ((rx*rx * y1r*y1r) + (ry*ry * x1r*x1r)) - 1f); cxr = k * rx * y1r / ry; cyr = -k * ry * x1r / rx; } } float cx = cosPhi * cxr - sinPhi * cyr + (x1 + x2) / 2; float cy = sinPhi * cxr + cosPhi * cyr + (y1 + y2) / 2; float phi1, phiDelta; { float sx = ( x1r - cxr) / rx, sy = ( y1r - cyr) / ry; float tx = (-x1r - cxr) / rx, ty = (-y1r - cyr) / ry; phi1 = PApplet.atan2(sy, sx); phiDelta = (((PApplet.atan2(ty, tx) - phi1) % TWO_PI) + TWO_PI) % TWO_PI; if (!fs) phiDelta -= TWO_PI; } // One segment can not cover more that PI, less than PI/2 is // recommended to avoid visible inaccuracies caused by rounding errors int segmentCount = PApplet.ceil(PApplet.abs(phiDelta) / TWO_PI * 4); float inc = phiDelta / segmentCount; float a = PApplet.sin(inc) * (PApplet.sqrt(4 + 3 * PApplet.sq(PApplet.tan(inc / 2))) - 1) / 3; float sinPhi1 = PApplet.sin(phi1), cosPhi1 = PApplet.cos(phi1); float p1x = x1; float p1y = y1; float relq1x = a * (-rx * cosPhi * sinPhi1 - ry * sinPhi * cosPhi1); float relq1y = a * (-rx * sinPhi * sinPhi1 + ry * cosPhi * cosPhi1); for (int i = 0; i < segmentCount; i++) { float eta = phi1 + (i + 1) * inc; float sinEta = PApplet.sin(eta), cosEta = PApplet.cos(eta); float p2x = cx + rx * cosPhi * cosEta - ry * sinPhi * sinEta; float p2y = cy + rx * sinPhi * cosEta + ry * cosPhi * sinEta; float relq2x = a * (-rx * cosPhi * sinEta - ry * sinPhi * cosEta); float relq2y = a * (-rx * sinPhi * sinEta + ry * cosPhi * cosEta); if (i == segmentCount - 1) { p2x = x2; p2y = y2; } parsePathCode(BEZIER_VERTEX); parsePathVertex(p1x + relq1x, p1y + relq1y); parsePathVertex(p2x - relq2x, p2y - relq2y); parsePathVertex(p2x, p2y); p1x = p2x; relq1x = relq2x; p1y = p2y; relq1y = relq2y; } } /** * 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 * this section * of the SVG documentation. * @param matrixStr text of the matrix param. * @return a good old-fashioned PMatrix2D */ static protected PMatrix2D parseTransform(String matrixStr) { matrixStr = matrixStr.trim(); PMatrix2D outgoing = null; int start = 0; int stop = -1; while ((stop = matrixStr.indexOf(')', start)) != -1) { PMatrix2D m = parseSingleTransform(matrixStr.substring(start, stop+1)); if (outgoing == null) { outgoing = m; } else { outgoing.apply(m); } start = stop + 1; } return outgoing; } static protected PMatrix2D parseSingleTransform(String matrixStr) { //String[] pieces = PApplet.match(matrixStr, "^\\s*(\\w+)\\((.*)\\)\\s*$"); 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 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 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; } } 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(XML properties) { if (properties.hasAttribute("opacity")) { String opacityText = properties.getString("opacity"); setOpacity(opacityText); } if (properties.hasAttribute("stroke")) { String strokeText = properties.getString("stroke"); setColor(strokeText, false); } if (properties.hasAttribute("stroke-opacity")) { String strokeOpacityText = properties.getString("stroke-opacity"); setStrokeOpacity(strokeOpacityText); } if (properties.hasAttribute("stroke-width")) { // if NaN (i.e. if it's 'inherit') then default back to the inherit setting String lineweight = properties.getString("stroke-width"); setStrokeWeight(lineweight); } if (properties.hasAttribute("stroke-linejoin")) { String linejoin = properties.getString("stroke-linejoin"); setStrokeJoin(linejoin); } if (properties.hasAttribute("stroke-linecap")) { String linecap = properties.getString("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.getString("fill"); setColor(fillText, true); } if (properties.hasAttribute("fill-opacity")) { String fillOpacityText = properties.getString("fill-opacity"); setFillOpacity(fillOpacityText); } if (properties.hasAttribute("style")) { String styleText = properties.getString("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")) { setColor(tokens[1], true); } else if(tokens[0].equals("fill-opacity")) { setFillOpacity(tokens[1]); } else if(tokens[0].equals("stroke")) { setColor(tokens[1], false); } 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))

Back | FazBrowse Home | New Git URL