| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
* 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 |