From 517e3d380658e4d11028c7b6cc1f7ee66508e070 Mon Sep 17 00:00:00 2001 From: George Bateman Date: Wed, 22 Apr 2015 17:50:56 +0100 Subject: [PATCH 1/2] Improvements to SVG. Fixes #2992. Proper support for percentages, partial support for viewbox, support for more named colors, allow gradients to specify color-stops using style or direct attributes on , nicer error message on loading an HTML document as SVG (since I assume it can happen to novices downloading SVGs from the web). --- core/src/processing/core/PShapeSVG.java | 342 ++++++++++++++++-------- 1 file changed, 227 insertions(+), 115 deletions(-) diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index d91e1b3f2..08f617b65 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -127,7 +127,7 @@ import java.util.HashMap; *
  • Now properly supports Processing 0118 *
  • Fixed a bunch of things for Casey's students and general buggity. *
  • Will now properly draw #FFFFFFFF colors (were being represented as -1) - *
  • SVGs without tags are now properly caught and loaded + *
  • SVGs without <g> tags are now properly caught and loaded *
  • Added a method customStyle() for overriding SVG colors/styles *
  • Added a method SVGStyle() to go back to using SVG colors/styles * @@ -154,6 +154,19 @@ public class PShapeSVG extends PShape { float strokeOpacity; float fillOpacity; + /** + * Used for percentages. Width of containing SVG. + */ + protected float svgWidth; + /** + * Used for percentages. Height of containing SVG. + */ + protected float svgHeight; + /** + * Used for percentages. √((w² + h²)/2) of containing SVG. + */ + protected float svgXYSize; + Gradient strokeGradient; Paint strokeGradientPaint; @@ -175,44 +188,17 @@ public class PShapeSVG extends PShape { /** - * Initializes a new SVG Object from the given PNode. + * Initializes a new SVG Object from the given XML. */ public PShapeSVG(XML svg) { this(null, svg, true); if (!svg.getName().equals("svg")) { - throw new RuntimeException("root is not , it's <" + svg.getName() + ">"); + throw new RuntimeException("The root node is not , it's <" + svg.getName() + ">." + + (svg.getName().toLowerCase().equals("html") ? + " That means it's just a webpage. Did you download it right?" : "")); } - // 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.getString("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.getString("width"); - String unitHeight = svg.getString("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 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); // ? @@ -244,6 +230,8 @@ public class PShapeSVG extends PShape { //hasTransform = false; //transformation = null; //new float[] { 1, 0, 0, 1, 0, 0 }; + // svgWidth, svgHeight, and svgXYSize done below. + strokeOpacity = 1; fillOpacity = 1; opacity = 1; @@ -267,9 +255,64 @@ public class PShapeSVG extends PShape { //hasTransform = parent.hasTransform; //transformation = parent.transformation; + svgWidth = parent.svgWidth; + svgHeight = parent.svgHeight; + svgXYSize = parent.svgXYSize; + opacity = parent.opacity; } + // 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; + svgXYSize = PApplet.sqrt((svgWidth*svgWidth + svgHeight*svgHeight)/2.0f); + } + element = properties; name = properties.getString("id"); // @#$(* adobe illustrator mangles names of objects when re-saving @@ -287,7 +330,11 @@ public class PShapeSVG extends PShape { String transformStr = properties.getString("transform"); if (transformStr != null) { - matrix = parseTransform(transformStr); + if (matrix == null) { + matrix = parseTransform(transformStr); + } else { + matrix.preApply(parseTransform(transformStr)); + } } if (parseKids) { @@ -304,12 +351,7 @@ public class PShapeSVG extends PShape { for (XML elem : elements) { PShape kid = parseChild(elem); - if (kid != null) { -// if (kid.name != null) { -// System.out.println("adding child " + kid.name); -// } - addChild(kid); - } + if (kid != null) addChild(kid); } children = (PShape[]) PApplet.subset(children, 0, childCount); } @@ -324,6 +366,7 @@ public class PShapeSVG extends PShape { String name = elem.getName(); PShapeSVG shape = null; + if (name == null) { // just some whitespace that can be ignored (hopefully) @@ -388,14 +431,9 @@ public class PShapeSVG extends PShape { // } else if (name.equals("glyph") || name.equals("missing-glyph")) { // return new FontGlyph(this, elem); - } else if (name.equals("metadata")) { - // fontforge just stuffs this in as a comment - return null; - } 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."); + 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."); @@ -412,8 +450,11 @@ public class PShapeSVG extends PShape { } else if (name.equals("sodipodi:namedview")) { // these are always in Inkscape files, the warnings get tedious - } else if (name.equals("title")) { - // harmless + } 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 <" + name + "> tag."); @@ -427,10 +468,10 @@ public class PShapeSVG extends PShape { kind = LINE; family = PRIMITIVE; params = new float[] { - getFloatWithUnit(element, "x1"), - getFloatWithUnit(element, "y1"), - getFloatWithUnit(element, "x2"), - getFloatWithUnit(element, "y2") + getFloatWithUnit(element, "x1", svgWidth), + getFloatWithUnit(element, "y1", svgHeight), + getFloatWithUnit(element, "x2", svgWidth), + getFloatWithUnit(element, "y2", svgHeight) }; } @@ -444,21 +485,22 @@ public class PShapeSVG extends PShape { family = PRIMITIVE; params = new float[4]; - params[0] = getFloatWithUnit(element, "cx"); - params[1] = getFloatWithUnit(element, "cy"); + params[0] = getFloatWithUnit(element, "cx", svgWidth); + params[1] = getFloatWithUnit(element, "cy", svgHeight); float rx, ry; if (circle) { - rx = ry = getFloatWithUnit(element, "r"); + rx = ry = getFloatWithUnit(element, "r", svgXYSize); } else { - rx = getFloatWithUnit(element, "rx"); - ry = getFloatWithUnit(element, "ry"); + rx = getFloatWithUnit(element, "rx", svgWidth); + ry = getFloatWithUnit(element, "ry", svgHeight); } params[0] -= rx; params[1] -= ry; params[2] = rx*2; params[3] = ry*2; + } @@ -466,16 +508,17 @@ public class PShapeSVG extends PShape { kind = RECT; family = PRIMITIVE; params = new float[] { - getFloatWithUnit(element, "x"), - getFloatWithUnit(element, "y"), - getFloatWithUnit(element, "width"), - getFloatWithUnit(element, "height") + 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) { @@ -488,9 +531,9 @@ public class PShapeSVG extends PShape { 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(); + String pb[] = PApplet.splitTokens(pointsBuffer[i], ", \t\r\n"); + vertices[i][X] = Float.parseFloat(pb[0]); + vertices[i][Y] = Float.parseFloat(pb[1]); } } } @@ -871,11 +914,6 @@ public class PShapeSVG extends PShape { PApplet.join(PApplet.subset(pathTokens, i), ","); System.err.println("parsed: " + parsed); System.err.println("unparsed: " + unparsed); - if (pathTokens[i].equals("a") || pathTokens[i].equals("A")) { - String msg = "Sorry, elliptical arc support for SVG files " + - "is not yet implemented (See issue 130 for updates)"; - throw new RuntimeException(msg); - } throw new RuntimeException("shape command not handled: " + pathTokens[i]); } // prevCommand = c; @@ -1089,13 +1127,11 @@ public class PShapeSVG extends PShape { } 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")) { @@ -1111,7 +1147,7 @@ public class PShapeSVG extends PShape { 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); + return mat; } } else if (pieces[1].equals("skewX")) { @@ -1219,7 +1255,7 @@ public class PShapeSVG extends PShape { void setStrokeWeight(String lineweight) { - strokeWeight = parseUnitSize(lineweight); + strokeWeight = parseUnitSize(lineweight, svgXYSize); } @@ -1268,41 +1304,29 @@ public class PShapeSVG extends PShape { void setColor(String colorText, boolean isFill) { + colorText = colorText.trim(); int opacityMask = fillColor & 0xFF000000; boolean visible = true; int color = 0; String name = ""; + String lColorText = colorText.toLowerCase(); Gradient gradient = null; Paint paint = null; if (colorText.equals("none")) { visible = false; - } else if (colorText.equals("black")) { - color = opacityMask; - } else if (colorText.equals("white")) { - color = opacityMask | 0xFFFFFF; - } else if (colorText.startsWith("#")) { - if (colorText.length() == 4) { - // Short form: #ABC, transform to long form #AABBCC - colorText = colorText.replaceAll("^#(.)(.)(.)$", "#$1$1$2$2$3$3"); - } - color = opacityMask | - (Integer.parseInt(colorText.substring(1), 16)) & 0xFFFFFF; - //System.out.println("hex for fill is " + PApplet.hex(fillColor)); - } else if (colorText.startsWith("rgb")) { - color = opacityMask | parseRGB(colorText); } else if (colorText.startsWith("url(#")) { name = colorText.substring(5, colorText.length() - 1); -// PApplet.println("looking for " + name); Object object = findChild(name); - //PApplet.println("found " + fillObject); if (object instanceof Gradient) { gradient = (Gradient) object; paint = calcGradientPaint(gradient); //, opacity); - //PApplet.println("got filla " + fillObject); } else { // visible = false; System.err.println("url " + name + " refers to unexpected data: " + object); } + } else { + // Prints errors itself. + color = opacityMask | parseSimpleColor(colorText); } if (isFill) { fill = visible; @@ -1320,17 +1344,85 @@ public class PShapeSVG extends PShape { } + /** + * Parses the "color" datatype only, and prints an error if it is not of this form. + * http://www.w3.org/TR/SVG/types.html#DataTypeColor + * @return 0xRRGGBB (no alpha). Zero on error. + */ + static protected int parseSimpleColor(String colorText) { + colorText = colorText.toLowerCase().trim(); + if (colorNames.containsKey(colorText)) { + return colorNames.get(colorText); + } else if (colorText.startsWith("#")) { + if (colorText.length() == 4) { + // Short form: #ABC, transform to long form #AABBCC + colorText = colorText.replaceAll("^#(.)(.)(.)$", "#$1$1$2$2$3$3"); + } + return (Integer.parseInt(colorText.substring(1), 16)) & 0xFFFFFF; + //System.out.println("hex for fill is " + PApplet.hex(fillColor)); + } else if (colorText.startsWith("rgb")) { + return parseRGB(colorText); + } else { + System.err.println("Cannot parse \"" + colorText + "\"."); + return 0; + } + } + + + /** + * Deliberately conforms to HTML 4.01 color spec + en-gb grey, + * not SVG's 147-color system. + */ + static protected HashMap colorNames; + + static { + colorNames = new HashMap(); + colorNames.put("aqua", 0x00ffff); + colorNames.put("black", 0x000000); + colorNames.put("blue", 0x0000ff); + colorNames.put("fuchsia", 0xff00ff); + colorNames.put("gray", 0x808080); + colorNames.put("grey", 0x808080); + colorNames.put("green", 0x008000); + colorNames.put("lime", 0x00ff00); + colorNames.put("maroon", 0x800000); + colorNames.put("navy", 0x000080); + colorNames.put("olive", 0x808000); + colorNames.put("purple", 0x800080); + colorNames.put("red", 0xff0000); + colorNames.put("silver", 0xc0c0c0); + colorNames.put("teal", 0x008080); + colorNames.put("white", 0xffffff); + colorNames.put("yellow", 0xffff00); + } + + 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]); + String[] values = PApplet.splitTokens(sub, ", "); + int rgbValue = 0; + if (values.length == 3) { + // Color spec allows for rgb values to be percentages. + for (int i = 0; i < 3; i++) { + rgbValue <<= 8; + if (values[i].endsWith("%")) { + rgbValue |= (int)(PApplet.constrain(255*parseFloatOrPercent(values[i]), 0, 255)); + } else { + rgbValue |= PApplet.constrain(PApplet.parseInt(values[i]), 0, 255); + } + } + } else System.err.println("Could not read color \"" + what + "\"."); + + return rgbValue; } static protected HashMap parseStyleAttributes(String style) { HashMap table = new HashMap(); + if (style == null) return table; + String[] pieces = style.split(";"); for (int i = 0; i < pieces.length; i++) { String[] parts = pieces[i].split(":"); @@ -1345,18 +1437,21 @@ public class PShapeSVG extends PShape { * have a unit suffix (length or coordinate). * @param element what to parse * @param attribute name of the attribute to get + * @param relativeTo (float) Used for %. When relative to viewbox, should + * be svgWidth for horizontal dimentions, svgHeight for vertical, and + * svgXYSize for anything else. * @return unit-parsed version of the data */ - static protected float getFloatWithUnit(XML element, String attribute) { + static protected float getFloatWithUnit(XML element, String attribute, float relativeTo) { String val = element.getString(attribute); - return (val == null) ? 0 : parseUnitSize(val); + return (val == null) ? 0 : parseUnitSize(val, relativeTo); } /** * Parse a size that may have a suffix for its units. - * Ignoring cases where this could also be a percentage. - * The units spec: + * This assumes 90dpi, which implies, as given in the + * units spec: *
      *
    • "1pt" equals "1.25px" (and therefore 1.25 user units) *
    • "1pc" equals "15px" (and therefore 15 user units) @@ -1364,8 +1459,11 @@ public class PShapeSVG extends PShape { *
    • "1cm" equals "35.43307px" (and therefore 35.43307 user units) *
    • "1in" equals "90px" (and therefore 90 user units) *
    + * @param relativeTo (float) Used for %. When relative to viewbox, should + * be svgWidth for horizontal dimentions, svgHeight for vertical, and + * svgXYSize for anything else. */ - static protected float parseUnitSize(String text) { + static protected float parseUnitSize(String text, float relativeTo) { int len = text.length() - 2; if (text.endsWith("pt")) { @@ -1380,12 +1478,24 @@ public class PShapeSVG extends PShape { return PApplet.parseFloat(text.substring(0, len)) * 90; } else if (text.endsWith("px")) { return PApplet.parseFloat(text.substring(0, len)); + } else if (text.endsWith("%")) { + return relativeTo * parseFloatOrPercent(text); } else { return PApplet.parseFloat(text); } } + static protected float parseFloatOrPercent(String text) { + text = text.trim(); + if (text.endsWith("%")) { + return Float.parseFloat(text.substring(0, text.length() - 1)) / 100.0f; + } else { + return Float.parseFloat(text); + } + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -1409,22 +1519,24 @@ public class PShapeSVG extends PShape { String name = elem.getName(); if (name.equals("stop")) { String offsetAttr = elem.getString("offset"); - float div = 1.0f; - if (offsetAttr.endsWith("%")) { - div = 100.0f; - offsetAttr = offsetAttr.substring(0, offsetAttr.length() - 1); - } - offset[count] = PApplet.parseFloat(offsetAttr) / div; + offset[count] = parseFloatOrPercent(offsetAttr); + String style = elem.getString("style"); HashMap styles = parseStyleAttributes(style); String colorStr = styles.get("stop-color"); - if (colorStr == null) colorStr = "#000000"; + if (colorStr == null) { + colorStr = elem.getString("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); + if (opacityStr == null) { + opacityStr = elem.getString("stop-opacity"); + if (opacityStr == null) opacityStr = "1"; + } + int tupacity = PApplet.constrain( + (int)(PApplet.parseFloat(opacityStr) * 255), 0, 255); + color[count] = (tupacity << 24) | parseSimpleColor(colorStr); count++; } } @@ -1440,10 +1552,10 @@ public class PShapeSVG extends PShape { public LinearGradient(PShapeSVG parent, XML properties) { super(parent, properties); - this.x1 = getFloatWithUnit(properties, "x1"); - this.y1 = getFloatWithUnit(properties, "y1"); - this.x2 = getFloatWithUnit(properties, "x2"); - this.y2 = getFloatWithUnit(properties, "y2"); + this.x1 = getFloatWithUnit(properties, "x1", svgWidth); + this.y1 = getFloatWithUnit(properties, "y1", svgHeight); + this.x2 = getFloatWithUnit(properties, "x2", svgWidth); + this.y2 = getFloatWithUnit(properties, "y2", svgHeight); String transformStr = properties.getString("gradientTransform"); @@ -1470,9 +1582,9 @@ public class PShapeSVG extends PShape { public RadialGradient(PShapeSVG parent, XML properties) { super(parent, properties); - this.cx = getFloatWithUnit(properties, "cx"); - this.cy = getFloatWithUnit(properties, "cy"); - this.r = getFloatWithUnit(properties, "r"); + this.cx = getFloatWithUnit(properties, "cx", svgWidth); + this.cy = getFloatWithUnit(properties, "cy", svgHeight); + this.r = getFloatWithUnit(properties, "r", svgXYSize); String transformStr = properties.getString("gradientTransform"); From 9753745ee4d47ab59bc1f05b7a6601090afb7af1 Mon Sep 17 00:00:00 2001 From: George Bateman Date: Thu, 23 Apr 2015 19:45:03 +0100 Subject: [PATCH 2/2] PApplet.java: disable a doc comment It's coming up in the wrong place on the Javadoc because its method is commented out. --- core/src/processing/core/PApplet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 9063ae086..ebf81d3e7 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -3887,7 +3887,7 @@ public class PApplet implements PConstants { } - /** + /* * Find the minimum value in an array. * Throws an ArrayIndexOutOfBoundsException if the array is length 0. * @param list the source array