From c54c54c1459ac392c18df5df4fb0f596dbe5775c Mon Sep 17 00:00:00 2001 From: Jeremy Laviole Date: Tue, 24 Nov 2015 17:55:40 +0100 Subject: [PATCH] Support of embedded images in SVGs, linked images are supported but should not be avoided if possible. Support of Text in SVGs (partial). Multiline text are also displayed. Text limitations : Inline styles are not supported. Not all fonts are supported (depending on your OS). Tested only on Inkscape SVGs. --- core/src/processing/core/PShape.java | 111 +++++++++++-- core/src/processing/core/PShapeSVG.java | 199 +++++++++++++++++++++++- 2 files changed, 291 insertions(+), 19 deletions(-) diff --git a/core/src/processing/core/PShape.java b/core/src/processing/core/PShape.java index 44ff15115..03de66203 100644 --- a/core/src/processing/core/PShape.java +++ b/core/src/processing/core/PShape.java @@ -22,8 +22,23 @@ package processing.core; +import java.awt.Image; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; +import java.util.Base64.Decoder; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.nio.charset.StandardCharsets; +import javax.swing.ImageIcon; +import javax.xml.bind.DatatypeConverter; + + import processing.core.PApplet; @@ -106,6 +121,7 @@ public class PShape implements PConstants { /** Texture or image data associated with this shape. */ protected PImage image; + protected String imagePath = null; public static final String OUTSIDE_BEGIN_END_ERROR = "%1$s can only be called between beginShape() and endShape()"; @@ -1645,6 +1661,10 @@ public class PShape implements PConstants { params[6], params[7]); } else if (kind == RECT) { + + if (imagePath != null){ + loadImage(g); + } if (image != null) { int oldMode = g.imageMode; g.imageMode(CORNER); @@ -1879,6 +1899,63 @@ public class PShape implements PConstants { g.endShape(close ? CLOSE : OPEN); } + private void loadImage(PGraphics g){ + + if(this.imagePath.startsWith("data:image")){ + loadBase64Image(); + } + + if(this.imagePath.startsWith("file://")){ + loadFileSystemImage(g); + } + this.imagePath = null; + } + + private void loadFileSystemImage(PGraphics g){ + imagePath = imagePath.substring(7); + PImage loadedImage = g.parent.loadImage(imagePath); + if(loadedImage == null){ + System.err.println("Error loading image file: " + imagePath); + }else{ + setTexture(loadedImage); + } + } + + private void loadBase64Image(){ + String[] parts = this.imagePath.split(";base64,"); + String extension = parts[0].substring(11); + String encodedData = parts[1]; + + byte[] decodedBytes = DatatypeConverter.parseBase64Binary(encodedData); + + if(decodedBytes == null){ + System.err.println("Decode Error on image: " + imagePath.substring(0, 20)); + return; + } + + Image awtImage = new ImageIcon(decodedBytes).getImage(); + + if (awtImage instanceof BufferedImage) { + BufferedImage buffImage = (BufferedImage) awtImage; + int space = buffImage.getColorModel().getColorSpace().getType(); + if (space == ColorSpace.TYPE_CMYK) { + return; + } + } + + PImage loadedImage = new PImage(awtImage); + if (loadedImage.width == -1) { + // error... + } + + // if it's a .gif image, test to see if it has transparency + if (extension.equals("gif") || extension.equals("png") || + extension.equals("unknown")) { + loadedImage.checkAlpha(); + } + + setTexture(loadedImage); + } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -2387,14 +2464,14 @@ public class PShape implements PConstants { /** * ( begin auto-generated from PShape_setFill.xml ) * - * The setFill() method defines the fill color of a PShape. - * This method is used after shapes are created or when a shape is defined explicitly - * (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in the above example. - * When a shape is created with beginShape() and endShape(), its - * attributes may be changed with fill() and stroke() within - * beginShape() and endShape(). However, after the shape is - * created, only the setFill() method can define a new fill value for - * the PShape. + * The setFill() method defines the fill color of a PShape. + * This method is used after shapes are created or when a shape is defined explicitly + * (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in the above example. + * When a shape is created with beginShape() and endShape(), its + * attributes may be changed with fill() and stroke() within + * beginShape() and endShape(). However, after the shape is + * created, only the setFill() method can define a new fill value for + * the PShape. * * ( end auto-generated ) * @@ -2543,14 +2620,14 @@ public class PShape implements PConstants { /** * ( begin auto-generated from PShape_setStroke.xml ) * - * The setStroke() method defines the outline color of a PShape. - * This method is used after shapes are created or when a shape is defined - * explicitly (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in - * the above example. When a shape is created with beginShape() and - * endShape(), its attributes may be changed with fill() and - * stroke() within beginShape() and endShape(). - * However, after the shape is created, only the setStroke() method - * can define a new stroke value for the PShape. + * The setStroke() method defines the outline color of a PShape. + * This method is used after shapes are created or when a shape is defined + * explicitly (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in + * the above example. When a shape is created with beginShape() and + * endShape(), its attributes may be changed with fill() and + * stroke() within beginShape() and endShape(). + * However, after the shape is created, only the setStroke() method + * can define a new stroke value for the PShape. * * ( end auto-generated ) * @@ -3442,4 +3519,4 @@ public class PShape implements PConstants { calcAlpha = (calcAi != 255); } -} \ No newline at end of file +} diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index bf5cdc2f1..adc1763bb 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -24,6 +24,9 @@ package processing.core; +import static java.awt.Font.BOLD; +import static java.awt.Font.ITALIC; +import static java.awt.Font.PLAIN; import processing.data.*; // TODO replace these with PMatrix2D @@ -329,6 +332,10 @@ public class PShapeSVG extends PShape { } else if (name.equals("rect")) { shape = createShape(this, elem, true); shape.parseRect(); + + } else if (name.equals("image")) { + shape = createShape(this, elem, true); + shape.parseImage(); } else if (name.equals("polygon")) { shape = createShape(this, elem, true); @@ -358,8 +365,10 @@ public class PShapeSVG extends PShape { // return new FontGlyph(this, elem); } else if (name.equals("text")) { // || name.equals("font")) { - PGraphics.showWarning("Text and fonts in SVG files are " + - "not currently supported, convert text to outlines instead."); + return new Text(this, elem); + + } else if (name.equals("tspan")) { + return new LineOfText(this, elem); } else if (name.equals("filter")) { PGraphics.showWarning("Filters are not supported."); @@ -439,8 +448,23 @@ public class PShapeSVG extends PShape { getFloatWithUnit(element, "height", svgHeight) }; } + + + protected void parseImage() { + kind = RECT; + textureMode = NORMAL; + family = PRIMITIVE; + params = new float[] { + getFloatWithUnit(element, "x", svgWidth), + getFloatWithUnit(element, "y", svgHeight), + getFloatWithUnit(element, "width", svgWidth), + getFloatWithUnit(element, "height", svgHeight) + }; + this.imagePath = element.getString("xlink:href"); + } + /** * Parse a polyline or polygon from an SVG file. * Syntax defined at http://www.w3.org/TR/SVG/shapes.html#PointsBNF @@ -1557,8 +1581,179 @@ public class PShapeSVG extends PShape { // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + public static float TEXT_QUALITY = 1; + protected PFont parseFont(XML properties) { +// FontFace fontFace = new FontFace(this, properties); + String fontFamily = null; + float size = 10; + int weight = PLAIN; // 0 + int italic = 0; + + if (properties.hasAttribute("style")) { + String styleText = properties.getString("style"); + String[] styleTokens = PApplet.splitTokens(styleText, ";"); + //PApplet.println(styleTokens); + for (int i = 0; i < styleTokens.length; i++) { + + String[] tokens = PApplet.splitTokens(styleTokens[i], ":"); + //PApplet.println(tokens); + + tokens[0] = PApplet.trim(tokens[0]); + + if (tokens[0].equals("font-style")) { + // PApplet.println("font-style: " + tokens[1]); + if (tokens[1].contains("italic")) { + italic = ITALIC; + } + } else if (tokens[0].equals("font-variant")) { + // PApplet.println("font-variant: " + tokens[1]); + // setFillOpacity(tokens[1]); + + } else if (tokens[0].equals("font-weight")) { + // PApplet.println("font-weight: " + tokens[1]); + + if (tokens[1].contains("bold")) { + weight = BOLD; + // PApplet.println("Bold weight ! "); + } + + + } else if (tokens[0].equals("font-stretch")) { + // not supported. + + } else if (tokens[0].equals("font-size")) { + // PApplet.println("font-size: " + tokens[1]); + size = Float.parseFloat(tokens[1].split("px")[0]); + // PApplet.println("font-size-parsed: " + size); + } else if (tokens[0].equals("line-height")) { + // not supported + + } else if (tokens[0].equals("font-family")) { + // PApplet.println("Font-family: " + tokens[1]); + fontFamily = tokens[1]; + + } else if (tokens[0].equals("text-align")) { + // not supported + + } else if (tokens[0].equals("letter-spacing")) { + // not supported + + } else if (tokens[0].equals("word-spacing")) { + // not supported + + } else if (tokens[0].equals("writing-mode")) { + // not supported + + } else if (tokens[0].equals("text-anchor")) { + // not supported + + } else { + // Other attributes are not yet implemented + } + } + } + if (fontFamily == null) { + return null; + } + size = size * TEXT_QUALITY; + + return createFont(fontFamily, weight | italic, size, true); + } + + protected PFont createFont(String name, int weight, float size, boolean smooth) { + +// System.out.println("Try to create a font of " + name + " family, " + weight); + java.awt.Font baseFont = new java.awt.Font(name, weight, (int) size); // PFont.findFont(name);รง + +// System.out.println("Resulting family : " + baseFont.getFamily() + " " + baseFont.getStyle()); + PFont outputPFont = new PFont(baseFont.deriveFont(size), smooth, null); + +// System.out.println("Resulting PFont family : " + outputPFont.getName()); + return outputPFont; + } + + public static class Text extends PShapeSVG { + + protected PFont font; + + public Text(PShapeSVG parent, XML properties) { + super(parent, properties, true); + + // get location + float x = Float.parseFloat(properties.getString("x")); + float y = Float.parseFloat(properties.getString("y")); + + if (matrix == null) { + matrix = new PMatrix2D(); + } + matrix.translate(x, y); + + family = GROUP; + + font = parseFont(properties); + + } + +// @Override +// public void drawImpl(PGraphics g){ +// } + } + + public static class LineOfText extends PShapeSVG { + + String textToDisplay; + PFont font; + + public LineOfText(PShapeSVG parent, XML properties) { + + // TODO: child should ideally be parsed too... + // for inline content. + super(parent, properties, false); + +// // get location + float x = Float.parseFloat(properties.getString("x")); + float y = Float.parseFloat(properties.getString("y")); + + float parentX = Float.parseFloat(parent.element.getString("x")); + float parentY = Float.parseFloat(parent.element.getString("y")); + + if (matrix == null) matrix = new PMatrix2D(); + matrix.translate(x - parentX, (y - parentY) / 2f); + + // get the first properties + parseColors(properties); + font = parseFont(properties); + + // It is a line.. + boolean isALine = properties.getString("role") == "line"; + // NO inline content yet. + if (this.childCount > 0) { + } + + String text = properties.getContent(); + textToDisplay = text; + } + + @Override + public void drawImpl(PGraphics g) { + if (font == null) { + font = ((Text) parent).font; + if (font == null) { + return; + } + } + + pre(g); + g.textFont(font, font.size / TEXT_QUALITY); + g.text(textToDisplay, 0, 0); + post(g); + } + + } + public static class Font extends PShapeSVG { public FontFace face;