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