/* Candy 2 - SVG Importer for Processing - http://processing.org Copyright (c) 2006 Michael Chang (Flux) http://www.ghost-hack.com/ Revised and expanded by Ben Fry for inclusion as a core library Copyright (c) 2006-08 Ben Fry and Casey Reas 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. */ package processing.candy; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.Hashtable; import processing.core.*; import processing.xml.*; /** * Candy is a minimal SVG import library for Processing. * Candy was written by Michael Chang, and later revised and * expanded for use as a Processing core library by Ben Fry. *

* SVG stands for Scalable Vector Graphics, a portable graphics * format. It is a vector format so it allows for infinite resolution * and relatively minute file sizes. Most modern media software * can view SVG files, including Firefox, Adobe products, etc. * You can use something like Illustrator to edit SVG files. *

* 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. Because of this size, it is not made part * of processing.core, because it would increase the download size of any * applet by 20%, and it's not a feature that will be used by the majority * of our audience. For more sophisticated import/export, consider the * Batik 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 * SVG Tiny or Basic, * although we still would not support the interactivity options. *

* This library was specifically tested under SVG files created with Adobe * Illustrator. We can't guarantee that it will work for any SVGs created with * other software. In the future we would like to improve compatibility with * Open Source software such as InkScape, however initial tests show its * base implementation produces more complicated files, and this will require * more time. *

* An SVG created under Illustrator must be created in one of two ways: *

* Saving it any other way will most likely break Candy. * *


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

 * import processing.candy.*;
 * import processing.xml.*;
 *
 * SVG moo;
 * void setup() {
 *   size(400,400);
 *   moo = new SVG("moo.svg",this);
 * }
 * void draw() {
 *   moo.draw();
 * }
 * 
* * Note that processing.xml needs to be imported as well. * This may not be required when running code within the Processing * environment, but when exported it may cause a NoClassDefError. * This will be fixed in later releases of Processing * (Bug 518). * *


* * February 2008 revisions by fry (Processing 0136) *

* * Revisions for "Candy 2" November 2006 by fry * * * Revision 10/31/06 by flux * * * Some SVG objects and features may not yet be supported. * Here is a partial list of non-included features * * * For those interested, the SVG specification can be found * here. */ public class SVG { protected PApplet parent; public float width; public float height; protected Hashtable table = new Hashtable(); protected XMLElement svg; protected BaseObject root; protected boolean ignoreStyles = false; int drawMode = PConstants.CORNER; /** * Initializes a new SVG Object with the given filename. */ public SVG(PApplet parent, String filename) { // this will grab the root document, starting // the xml version and initial comments are ignored this(parent, new XMLElement(parent, filename)); } /** * Initializes a new SVG Object with the given filename. */ public SVG(PApplet parent, XMLElement svg) { this.parent = parent; this.svg = svg; if (!svg.getName().equals("svg")) { throw new RuntimeException("root is not , 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"); System.err.println("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; } } /* PApplet.println("document has " + document.getChildCount() + " children"); //Get the xml child node we need XMLElement doc = document.getChild(1); PApplet.println(doc); if (true) return; */ /* //XMLElement entSVG = doc.getChild(0); //XMLElement svg = entSVG.getChild(1); //While we're doing that, save the width and height too //svgWidth = svg.getIntAttribute("width"); //svgHeight = svg.getIntAttribute("height"); //Catch exception when SVG doesn't have a tag XMLElement graphics; String nameOfFirstChild = svg.getChild(1).toString(); if(nameOfFirstChild.equals("")) graphics = svg.getChild(1); else graphics = svg; this.svgData = svg; */ //parseChildren(document); root = new Group(null, svg); /* XMLElement graphics = null; //Print SVG on construction //Use this for debugging //svg.printElementTree(" ."); */ } /** * Internal method used to clone an object and return the subtree. */ protected SVG(PApplet parent, float width, float height, Hashtable table, BaseObject obj, boolean styleOverride) { this.parent = parent; this.width = width; this.height = height; this.table = table; this.root = obj; this.svg = obj.element; this.ignoreStyles = styleOverride; } /** * Parse a size that may have a suffix for its units. * Ignoring cases where this could also be a percentage. * The units spec: * */ public 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); } } /** * 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. *
     * // This code grabs "Layer 3" and the shapes beneath it.
     * SVG layer3 = svg.get("Layer 3");
     * 
*/ public SVG get(String name) { BaseObject obj = (BaseObject) table.get(name); if (obj == null) { // try with underscores instead of spaces obj = (BaseObject) table.get(name.replace(' ', '_')); } if (obj != null) { return new SVG(parent, width, height, table, obj, ignoreStyles); } return null; } // grab the (fill) gradient from a particular object by name // and apply it to either the stroke or fill // based on protected Paint getGradient(String name, float cx, float cy, float r) { BaseObject obj = (BaseObject) table.get(name); if (obj == null) { // try with underscores instead of spaces obj = (BaseObject) table.get(name.replace(' ', '_')); } if (obj != null) { if (obj.fillGradient != null) { return obj.calcGradientPaint(obj.fillGradient, cx, cy, r); } } throw new RuntimeException("No gradient found for shape " + name); } protected Paint getGradient(String name, float x1, float y1, float x2, float y2) { BaseObject obj = (BaseObject) table.get(name); if (obj == null) { // try with underscores instead of spaces obj = (BaseObject) table.get(name.replace(' ', '_')); } if (obj != null) { if (obj.fillGradient != null) { return obj.calcGradientPaint(obj.fillGradient, x1, y1, x2, y2); } } throw new RuntimeException("No gradient found for shape " + name); } public void strokeGradient(String name, float x, float y, float r) { Paint paint = getGradient(name, x, y, r); if (parent.g instanceof PGraphicsJava2D) { PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g); p2d.strokeGradient = true; p2d.strokeGradientObject = paint; } } public void strokeGradient(String name, float x1, float y1, float x2, float y2) { Paint paint = getGradient(name, x1, y1, x2, y2); if (parent.g instanceof PGraphicsJava2D) { PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g); p2d.strokeGradient = true; p2d.strokeGradientObject = paint; } } public void fillGradient(String name, float x, float y, float r) { Paint paint = getGradient(name, x, y, r); if (parent.g instanceof PGraphicsJava2D) { PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g); p2d.fillGradient = true; p2d.fillGradientObject = paint; } } public void fillGradient(String name, float x1, float y1, float x2, float y2) { Paint paint = getGradient(name, x1, y1, x2, y2); if (parent.g instanceof PGraphicsJava2D) { PGraphicsJava2D p2d = ((PGraphicsJava2D) parent.g); p2d.fillGradient = true; p2d.fillGradientObject = paint; } } /** * Temporary hack for gradient handling. This is not supported * and will be removed from future releases. */ /* public void drawStyles() { root.drawStyles(); //PApplet.println(root); if (root instanceof VectorObject) { ((VectorObject)root).drawStyles(); } else { PApplet.println("Only use drawStyles() on an object, not a group."); } } */ public void draw() { if (drawMode == PConstants.CENTER) { parent.pushMatrix(); parent.translate(-width/2, -height/2); drawImpl(); parent.popMatrix(); } else if ((drawMode == PConstants.CORNER) || (drawMode == PConstants.CORNERS)) { drawImpl(); } } /** * Convenience method to draw at a particular location. */ public void draw(float x, float y) { parent.pushMatrix(); if (drawMode == PConstants.CENTER) { parent.translate(x - width/2, y - height/2); } else if ((drawMode == PConstants.CORNER) || (drawMode == PConstants.CORNERS)) { parent.translate(x, y); } drawImpl(); parent.popMatrix(); } public void draw(float x, float y, float c, float d) { parent.pushMatrix(); if (drawMode == PConstants.CENTER) { // x and y are center, c and d refer to a diameter parent.translate(x - c/2f, y - d/2f); parent.scale(c / width, d / height); } else if (drawMode == PConstants.CORNER) { parent.translate(x, y); parent.scale(c / width, d / height); } else if (drawMode == PConstants.CORNERS) { // c and d are x2/y2, make them into width/height c -= x; d -= y; // then same as above parent.translate(x, y); parent.scale(c / width, d / height); } drawImpl(); parent.popMatrix(); } /** * Draws the SVG document. */ public void drawImpl() { boolean stroke = parent.g.stroke; int strokeColor = parent.g.strokeColor; float strokeWeight = parent.g.strokeWeight; int strokeCap = parent.g.strokeCap; int strokeJoin= parent.g.strokeJoin; boolean fill = parent.g.fill; int fillColor = parent.g.fillColor; int ellipseMode = parent.g.ellipseMode; root.draw(); parent.g.stroke = stroke; parent.g.strokeColor = strokeColor; parent.g.strokeWeight = strokeWeight; parent.g.strokeCap = strokeCap; parent.g.strokeJoin = strokeJoin; parent.g.fill = fill; parent.g.fillColor = fillColor; parent.g.ellipseMode = ellipseMode; } /** * Set the orientation for drawn objects, similar to PImage.imageMode(). * @param which Either CORNER, CORNERS, or CENTER. */ public void drawMode(int which) { drawMode = which; } /** * Overrides SVG-set styles and uses PGraphics styles and colors. * Identical to ignoreStyles(true). */ public void ignoreStyles() { ignoreStyles(true); } /** * Enables or disables style information (fill and stroke) set in the file. * @param state true to use user-specified stroke/fill, false for svg version */ public void ignoreStyles(boolean state) { ignoreStyles = state; } /** * Prints out the SVG document useful for parsing */ public void print() { PApplet.println(svg.toString()); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . protected abstract class BaseObject { String id; XMLElement element; // set to false if the object is hidden in the layers palette boolean display; boolean stroke; int strokeColor; float strokeWeight; // default is 1 int strokeCap; int strokeJoin; Gradient strokeGradient; Paint strokeGradientPaint; String strokeName; // id of another object, gradients only? boolean fill; int fillColor; Gradient fillGradient; Paint fillGradientPaint; String fillName; // id of another object boolean hasTransform; PMatrix transformation; float strokeOpacity; float fillOpacity; float opacity; public BaseObject(BaseObject parent, XMLElement properties) { 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 = 255; fillOpacity = 255; 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; strokeOpacity = parent.strokeOpacity; fillOpacity = parent.fillOpacity; opacity = parent.opacity; } element = properties; id = properties.getStringAttribute("id"); if (id != null) { table.put(id, this); //System.out.println("now parsing " + id); } String displayStr = properties.getStringAttribute("display", "inline"); display = !displayStr.equals("none"); getColors(properties); getTransformation(properties); } private void getTransformation(XMLElement properties) { if (properties.hasAttribute("transform")) { String transform = properties.getStringAttribute("transform"); setTransformation(transform); } } protected void getColors(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 setTransformation(String transfText){ String[] transfTokens = PApplet.splitTokens(transfText, ")"); //if(transformation == null){ transformation = new PMatrix(); //} hasTransform = true; // Loop through all transformations for(int i=0; i tag."); } } } public void drawShape() { if (display) { for (int i = 0; i < objectCount; i++) { objects[i].draw(); } } } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . abstract private class Gradient extends BaseObject { AffineTransform transform; float[] offset; int[] color; int count; public Gradient(BaseObject parent, XMLElement properties) { super(parent, properties); XMLElement elements[] = properties.getChildren(); offset = new float[elements.length]; color = new int[elements.length]; // 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"); Hashtable styles = parseStyleAttributes(style); String colorStr = (String) styles.get("stop-color"); if (colorStr == null) colorStr = "#000000"; String opacityStr = (String) 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++; //System.out.println("this color is " + PApplet.hex(color[count])); /* int idx = farbe.indexOf("#"); if (idx != -1) { color[count] = Integer.parseInt(farbe.substring(idx+1), 16); count++; } else { System.err.println("problem with gradient stop " + properties); } */ } } } abstract protected void drawShape(); } static protected Hashtable parseStyleAttributes(String style) { Hashtable table = new Hashtable(); 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; } private class LinearGradient extends Gradient { float x1, y1, x2, y2; public LinearGradient(BaseObject 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) { this.transform = parseTransform(transformStr); 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(); } } protected void drawShape() { } } // complete version is here // http://www.w3.org/TR/SVG/coords.html#TransformAttribute AffineTransform parseTransform(String what) { if (what != null) { if (what.startsWith("matrix(") && what.endsWith(")")) { // columns go first with AT constructor what = what.substring(7, what.length() - 1); return new AffineTransform(PApplet.parseFloat(PApplet.split(what, ' '))); } } return null; } private class RadialGradient extends Gradient { float cx, cy, r; public RadialGradient(BaseObject 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) { this.transform = parseTransform(transformStr); 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()); } } protected void drawShape() { } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private class Line extends BaseObject { float x1, y1, x2, y2; public Line(BaseObject 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"); } protected void drawShape() { parent.line(x1, y1, x2, y2); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private class Circle extends BaseObject { float x, y, radius; public Circle(BaseObject parent, XMLElement properties) { super(parent, properties); this.x = properties.getFloatAttribute("cx"); this.y = properties.getFloatAttribute("cy"); this.radius = properties.getFloatAttribute("r") * 2; } protected void drawShape() { parent.ellipseMode(PConstants.CENTER); parent.ellipse(x, y, radius, radius); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private class Ellipse extends BaseObject{ float x, y, rx, ry; public Ellipse(BaseObject parent, XMLElement properties) { super(parent, properties); this.x = properties.getFloatAttribute("cx"); this.y = properties.getFloatAttribute("cy"); this.rx = properties.getFloatAttribute("rx") * 2; this.ry = properties.getFloatAttribute("ry") * 2; } protected void drawShape() { parent.ellipseMode(PConstants.CENTER); parent.ellipse(x, y, rx, ry); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private class Rect extends BaseObject{ float x, y, w, h; public Rect(BaseObject parent, XMLElement properties) { super(parent, properties); this.x = properties.getFloatAttribute("x"); this.y = properties.getFloatAttribute("y"); this.w = properties.getFloatAttribute("width"); this.h = properties.getFloatAttribute("height"); } protected void drawShape() { parent.rectMode(PConstants.CORNER); parent.rect(x, y, w, h); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . private class Poly extends BaseObject { float points[][] = null; /** true if polygon, false if polyline */ boolean closed; public Poly(BaseObject parent, XMLElement properties, boolean closed) { super(parent, properties); String pointsBuffer[] = null; this.closed = closed; if (properties.hasAttribute("points")) { pointsBuffer = PApplet.splitTokens(properties.getStringAttribute("points")); } points = new float[pointsBuffer.length][2]; for (int i = 0; i < points.length; i++) { String pb[] = PApplet.split(pointsBuffer[i], ','); points[i][0] = Float.valueOf(pb[0]).floatValue(); points[i][1] = Float.valueOf(pb[1]).floatValue(); } } protected void drawShape() { if (points != null) if (points.length > 0) { parent.beginShape(); for (int i = 0; i < points.length; i++) { parent.vertex(points[i][0], points[i][1]); } parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN); } } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . public class Path extends BaseObject { public int count = 0; public float[] x = new float[4]; public float[] y = new float[4]; static public final int MOVETO = 0; static public final int LINETO = 1; static public final int CURVETO = 2; static public final int QCURVETO = 3; public int[] kind = new int[4]; public boolean closed = false; public Path(BaseObject parent, XMLElement properties) { super(parent, properties); String pathDataBuffer = ""; if (!properties.hasAttribute("d")) return; pathDataBuffer = properties.getStringAttribute("d"); StringBuffer pathChars = new StringBuffer(); boolean lastSeparate = false; for (int i = 0; i < pathDataBuffer.length(); i++) { char c = pathDataBuffer.charAt(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) { pathChars.append("|"); } } if (c == 'Z' || c == 'z') { separate = false; } if (c == '-' && !lastSeparate) { pathChars.append("|"); } if (c != ',') { pathChars.append("" + pathDataBuffer.charAt(i)); } if (separate && c != ',' && c != '-') { pathChars.append("|"); } lastSeparate = separate; } pathDataBuffer = pathChars.toString(); //String pathDataKeys[] = PApplet.split(pathDataBuffer, '|'); // use whitespace constant to get rid of extra spaces and CR or LF String pathDataKeys[] = PApplet.splitTokens(pathDataBuffer, "|" + PConstants.WHITESPACE); //for (int j = 0; j < pathDataKeys.length; j++) { // PApplet.println(j + "\t" + pathDataKeys[j]); //} //PApplet.println(pathDataKeys); //PApplet.println(); //float cp[] = {0, 0}; float cx = 0; float cy = 0; int i = 0; //for (int i = 0; i < pathDataKeys.length; i++) { while (i < pathDataKeys.length) { char c = pathDataKeys[i].charAt(0); switch (c) { //M - move to (absolute) case 'M': /* cp[0] = PApplet.parseFloat(pathDataKeys[i + 1]); cp[1] = PApplet.parseFloat(pathDataKeys[i + 2]); float s[] = {cp[0], cp[1]}; i += 2; points.add(s); */ cx = PApplet.parseFloat(pathDataKeys[i + 1]); cy = PApplet.parseFloat(pathDataKeys[i + 2]); moveto(cx, cy); i += 3; break; //m - move to (relative) case 'm': /* cp[0] = cp[0] + PApplet.parseFloat(pathDataKeys[i + 1]); cp[1] = cp[1] + PApplet.parseFloat(pathDataKeys[i + 2]); float s[] = {cp[0], cp[1]}; i += 2; points.add(s); */ cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]); moveto(cx, cy); i += 3; break; case 'L': cx = PApplet.parseFloat(pathDataKeys[i + 1]); cy = PApplet.parseFloat(pathDataKeys[i + 2]); lineto(cx, cy); i += 3; break; case 'l': cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]); lineto(cx, cy); i += 3; break; // horizontal lineto absolute case 'H': cx = PApplet.parseFloat(pathDataKeys[i + 1]); lineto(cx, cy); i += 2; break; // horizontal lineto relative case 'h': cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]); lineto(cx, cy); i += 2; break; case 'V': cy = PApplet.parseFloat(pathDataKeys[i + 1]); lineto(cx, cy); i += 2; break; case 'v': cy = cy + PApplet.parseFloat(pathDataKeys[i + 1]); lineto(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]); curveto(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]); curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 7; } break; // S - curve to shorthand (absolute) case 'S': { float ppx = x[count-2]; float ppy = y[count-2]; float px = x[count-1]; float py = y[count-1]; 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]); curveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); cx = endX; cy = endY; i += 5; } break; // s - curve to shorthand (relative) case 's': { float ppx = x[count-2]; float ppy = y[count-2]; float px = x[count-1]; float py = y[count-1]; 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]); curveto(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]); curveto(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]); curveto(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 = x[count-2]; float ppy = y[count-2]; float px = x[count-1]; float py = y[count-1]; float ctrlX = px + (px - ppx); float ctrlY = py + (py - ppy); float endX = PApplet.parseFloat(pathDataKeys[i + 1]); float endY = PApplet.parseFloat(pathDataKeys[i + 2]); curveto(ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 3; } break; // t - quadratic curve to shorthand (relative) case 't': { float ppx = x[count-2]; float ppy = y[count-2]; float px = x[count-1]; float py = y[count-1]; 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]); curveto(ctrlX, ctrlY, endX, endY); cx = endX; cy = endY; i += 3; } break; case 'Z': case 'z': closed = 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); throw new RuntimeException("shape command not handled: " + pathDataKeys[i]); } } } protected void moveto(float px, float py) { if (count == x.length) { x = PApplet.expand(x); y = PApplet.expand(y); kind = PApplet.expand(kind); } kind[count] = MOVETO; x[count] = px; y[count] = py; count++; } protected void lineto(float px, float py) { if (count == x.length) { x = PApplet.expand(x); y = PApplet.expand(y); kind = PApplet.expand(kind); } kind[count] = LINETO; x[count] = px; y[count] = py; count++; } /** Quadratic curveto command. */ protected void curveto(float x1, float y1, float x2, float y2) { if (count + 2 >= x.length) { x = PApplet.expand(x); y = PApplet.expand(y); kind = PApplet.expand(kind); } kind[count] = QCURVETO; x[count] = x1; y[count] = y1; count++; x[count] = x2; y[count] = y2; count++; } /** Cubic curveto command. */ protected void curveto(float x1, float y1, float x2, float y2, float x3, float y3) { if (count + 2 >= x.length) { x = PApplet.expand(x); y = PApplet.expand(y); kind = PApplet.expand(kind); } kind[count] = CURVETO; x[count] = x1; y[count] = y1; count++; x[count] = x2; y[count] = y2; count++; x[count] = x3; y[count] = y3; count++; } protected void drawShape() { parent.beginShape(); parent.vertex(x[0], y[0]); int i = 1; // moveto has the first point while (i < count) { switch (kind[i]) { case MOVETO: parent.breakShape(); parent.vertex(x[i], y[i]); i++; break; case LINETO: parent.vertex(x[i], y[i]); i++; break; case QCURVETO: // doubles the control point parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+1], y[i+1]); i += 2; break; case CURVETO: parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+2], y[i+2]); i += 3; break; } } parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN); } } /* public void draw() { drawShape(); } public void draw(PApplet tp) { PApplet p = parent; parent = tp; drawShape(); parent = p; } public void draw(PGraphics parent) { parent.beginShape(); parent.vertex(x[0], y[0]); int i = 1; // moveto has the first point while (i < count) { switch (kind[i]) { case MOVETO: parent.breakShape(); parent.vertex(x[i], y[i]); i++; break; case LINETO: parent.vertex(x[i], y[i]); i++; break; case QCURVETO: // doubles the control point parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+1], y[i+1]); i += 2; break; case CURVETO: parent.bezierVertex(x[i], y[i], x[i+1], y[i+1], x[i+2], y[i+2]); i += 3; break; } } parent.endShape(closed ? PConstants.CLOSE : PConstants.OPEN); } */ // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /* // http://www.w3.org/TR/SVG11/fonts.html public class Font extends BaseObject { // String id; // XMLElement element; public FontFace face; public HashMap namedGlyphs; public HashMap unicodeGlyphs; public int glyphCount; public FontGlyph[] glyphs; public FontGlyph missingGlyph; public Font(BaseObject parent, XMLElement properties) { super(parent, properties); XMLElement[] elements = properties.getChildren(); namedGlyphs = new HashMap(); unicodeGlyphs = new HashMap(); glyphCount = 0; glyphs = new FontGlyph[elements.length]; for (int i = 0; i < elements.length; i++) { String name = elements[i].getName(); XMLElement elem = elements[i]; if (name.equals("glyph")) { FontGlyph fg = new FontGlyph(this, elem); if (fg.count != 0) { if (fg.name != null) { namedGlyphs.put(fg.name, fg); } if (fg.unicode != 0) { unicodeGlyphs.put(new Character(fg.unicode), fg); } } glyphs[glyphCount++] = fg; } else if (name.equals("missing-glyph")) { missingGlyph = new FontGlyph(this, elem); } else if (name.equals("font-face")) { face = new FontFace(this, elem); } else { System.err.println("Ignoring " + name + " inside "); } } } protected void drawShape() { // does nothing for fonts } public void drawString(String str, float x, float y, float size) { // 1) scale by the 1.0/unitsPerEm // 2) scale up by a font size parent.pushMatrix(); float s = size / (float) face.unitsPerEm; //System.out.println("scale is " + s); // swap y coord at the same time, since fonts have y=0 at baseline parent.translate(x, y); parent.scale(s, -s); char[] c = str.toCharArray(); for (int i = 0; i < c.length; i++) { // call draw on each char (pulling it w/ the unicode table) FontGlyph fg = (FontGlyph) unicodeGlyphs.get(new Character(c[i])); if (fg != null) { fg.draw(); // add horizAdvX/unitsPerEm to the x coordinate along the way parent.translate(fg.horizAdvX, 0); } else { System.err.println("'" + c[i] + "' not available."); } } parent.popMatrix(); } public void drawChar(char c, float x, float y, float size) { parent.pushMatrix(); float s = size / (float) face.unitsPerEm; parent.translate(x, y); parent.scale(s, -s); FontGlyph fg = (FontGlyph) unicodeGlyphs.get(new Character(c)); if (fg != null) fg.draw(); parent.popMatrix(); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . public class FontFace extends BaseObject { int horizOriginX; // dflt 0 int horizOriginY; // dflt 0 int horizAdvX; // no dflt? int vertOriginX; // dflt horizAdvX/2 int vertOriginY; // dflt ascent int vertAdvY; // dflt 1em (unitsPerEm value) String fontFamily; int fontWeight; // can also be normal or bold (also comma separated) String fontStretch; int unitsPerEm; // dflt 1000 int[] panose1; // dflt "0 0 0 0 0 0 0 0 0 0" int ascent; int descent; int[] bbox; // spec says comma separated, tho not w/ forge int underlineThickness; int underlinePosition; //String unicodeRange; // gonna ignore for now public FontFace(BaseObject parent, XMLElement properties) { super(parent, properties); unitsPerEm = properties.getIntAttribute("units-per-em", 1000); } protected void drawShape() { // nothing to draw in the font face attribute } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . public class FontGlyph extends Path { String name; //String unicode; char unicode; //c; int horizAdvX; public FontGlyph(BaseObject parent, XMLElement properties) { super(parent, properties); name = properties.getStringAttribute("glyph-name"); String u = properties.getStringAttribute("unicode"); unicode = 0; if (u != null) { if (u.length() == 1) { unicode = u.charAt(0); //System.out.println("unicode for " + name + " is " + u); } else { System.err.println("unicode for " + name + " is more than one char: " + u); } } horizAdvX = properties.getIntAttribute("horiz-adv-x", 0); } } */ }