From be2d7d5f101a803d9a636785d3432b2509562d80 Mon Sep 17 00:00:00 2001
From: Harald Kuhr
Date: Fri, 10 Jun 2022 17:24:47 +0200
Subject: [PATCH] #684 Fix some render size issues in SVGImageReader Bonus:
Minor code clean-up.
---
imageio/imageio-batik/pom.xml | 4 +-
.../imageio/plugins/svg/SVGImageReader.java | 212 +++++++++---------
.../plugins/svg/SVGImageReaderTest.java | 78 ++++++-
.../src/test/resources/svg/circle.svg | 6 +
4 files changed, 190 insertions(+), 110 deletions(-)
create mode 100644 imageio/imageio-batik/src/test/resources/svg/circle.svg
diff --git a/imageio/imageio-batik/pom.xml b/imageio/imageio-batik/pom.xml
index dcc23436..047c8c5e 100644
--- a/imageio/imageio-batik/pom.xml
+++ b/imageio/imageio-batik/pom.xml
@@ -27,9 +27,9 @@
maven-surefire-plugin
-
+
true
-
+
diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java
index 39ec64f0..b0568197 100755
--- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java
+++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReader.java
@@ -30,21 +30,10 @@
package com.twelvemonkeys.imageio.plugins.svg;
-import java.awt.*;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.Map;
-
-import javax.imageio.IIOException;
-import javax.imageio.ImageReadParam;
-import javax.imageio.ImageTypeSpecifier;
-import javax.imageio.spi.ImageReaderSpi;
+import com.twelvemonkeys.image.ImageUtil;
+import com.twelvemonkeys.imageio.ImageReaderBase;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+import com.twelvemonkeys.lang.StringUtil;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.anim.dom.SVGOMDocument;
@@ -68,10 +57,19 @@ import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGSVGElement;
-import com.twelvemonkeys.image.ImageUtil;
-import com.twelvemonkeys.imageio.ImageReaderBase;
-import com.twelvemonkeys.imageio.util.IIOUtil;
-import com.twelvemonkeys.lang.StringUtil;
+import javax.imageio.IIOException;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.spi.ImageReaderSpi;
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.image.*;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
/**
* Image reader for SVG document fragments.
@@ -79,12 +77,13 @@ import com.twelvemonkeys.lang.StringUtil;
* @author Harald Kuhr
* @author Inpspired by code from the Batik Team
* @version $Id: $
- * @see batik-dev
+ * @see batik-dev
*/
public class SVGImageReader extends ImageReaderBase {
final static boolean DEFAULT_ALLOW_EXTERNAL_RESOURCES =
- "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.svg.allowexternalresources"));
+ "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.svg.allowExternalResources",
+ System.getProperty("com.twelvemonkeys.imageio.plugins.svg.allowexternalresources")));
private Rasterizer rasterizer;
private boolean allowExternalResources = DEFAULT_ALLOW_EXTERNAL_RESOURCES;
@@ -150,29 +149,23 @@ public class SVGImageReader extends ImageReaderBase {
BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height);
// Read in the image, using the Batik Transcoder
+ processImageStarted(pIndex);
+
+ BufferedImage image = rasterizer.getImage();
+
+ Graphics2D g = destination.createGraphics();
try {
- processImageStarted(pIndex);
-
- BufferedImage image = rasterizer.getImage();
-
- Graphics2D g = destination.createGraphics();
- try {
- g.setComposite(AlphaComposite.Src);
- g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
- g.drawImage(image, 0, 0, null); // TODO: Dest offset?
- }
- finally {
- g.dispose();
- }
-
- processImageComplete();
-
- return destination;
+ g.setComposite(AlphaComposite.Src);
+ g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
+ g.drawImage(image, 0, 0, null); // TODO: Dest offset?
}
- catch (TranscoderException e) {
- Throwable cause = unwrapException(e);
- throw new IIOException(cause.getMessage(), cause);
+ finally {
+ g.dispose();
}
+
+ processImageComplete();
+
+ return destination;
}
private static Throwable unwrapException(TranscoderException ex) {
@@ -187,11 +180,11 @@ public class SVGImageReader extends ImageReaderBase {
// Set dimensions
Dimension size = pParam.getSourceRenderSize();
- Dimension origSize = new Dimension(getWidth(0), getHeight(0));
+ Rectangle viewBox = rasterizer.getViewBox();
if (size == null) {
// SVG is not a pixel based format, but we'll scale it, according to
// the subsampling for compatibility
- size = getSourceRenderSizeFromSubsamping(pParam, origSize);
+ size = getSourceRenderSizeFromSubsamping(pParam, viewBox.getSize());
}
if (size != null) {
@@ -211,8 +204,8 @@ public class SVGImageReader extends ImageReaderBase {
}
else {
// Need to resize here...
- double xScale = size.getWidth() / origSize.getWidth();
- double yScale = size.getHeight() / origSize.getHeight();
+ double xScale = size.getWidth() / viewBox.getWidth();
+ double yScale = size.getHeight() / viewBox.getHeight();
hints.put(ImageTranscoder.KEY_WIDTH, (float) (region.getWidth() * xScale));
hints.put(ImageTranscoder.KEY_HEIGHT, (float) (region.getHeight() * yScale));
@@ -220,7 +213,7 @@ public class SVGImageReader extends ImageReaderBase {
}
else if (size != null) {
// Allow non-uniform scaling
- hints.put(ImageTranscoder.KEY_AOI, new Rectangle(origSize));
+ hints.put(ImageTranscoder.KEY_AOI, viewBox);
}
// Background color
@@ -235,7 +228,7 @@ public class SVGImageReader extends ImageReaderBase {
private Dimension getSourceRenderSizeFromSubsamping(ImageReadParam pParam, Dimension pOrigSize) {
if (pParam.getSourceXSubsampling() > 1 || pParam.getSourceYSubsampling() > 1) {
return new Dimension((int) (pOrigSize.width / (float) pParam.getSourceXSubsampling()),
- (int) (pOrigSize.height / (float) pParam.getSourceYSubsampling()));
+ (int) (pOrigSize.height / (float) pParam.getSourceYSubsampling()));
}
return null;
}
@@ -246,22 +239,13 @@ public class SVGImageReader extends ImageReaderBase {
public int getWidth(int pIndex) throws IOException {
checkBounds(pIndex);
- try {
- return rasterizer.getDefaultWidth();
- }
- catch (TranscoderException e) {
- throw new IIOException(e.getMessage(), e);
- }
+
+ return rasterizer.getDefaultWidth();
}
public int getHeight(int pIndex) throws IOException {
checkBounds(pIndex);
- try {
- return rasterizer.getDefaultHeight();
- }
- catch (TranscoderException e) {
- throw new IIOException(e.getMessage(), e);
- }
+ return rasterizer.getDefaultHeight();
}
public Iterator getImageTypes(int imageIndex) {
@@ -275,12 +259,11 @@ public class SVGImageReader extends ImageReaderBase {
* and needs major refactoring!
*
*/
- private class Rasterizer extends SVGAbstractTranscoder /*ImageTranscoder*/ {
-
+ private class Rasterizer extends SVGAbstractTranscoder {
private BufferedImage image;
private TranscoderInput transcoderInput;
- private float defaultWidth;
- private float defaultHeight;
+ private final Rectangle2D viewBox = new Rectangle2D.Float();
+ private final Dimension defaultSize = new Dimension();
private boolean initialized = false;
private SVGOMDocument document;
private String uri;
@@ -341,54 +324,66 @@ public class SVGImageReader extends ImageReaderBase {
// ----
SVGSVGElement rootElement = svgDoc.getRootElement();
- // get the 'width' and 'height' attributes of the SVG document
- UnitProcessor.Context uctx
- = UnitProcessor.createContext(ctx, rootElement);
+ // Get the viewBox
+ String viewBoxStr = rootElement.getAttributeNS(null, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE);
+ if (viewBoxStr.length() != 0) {
+ float[] rect = ViewBox.parseViewBoxAttribute(rootElement, viewBoxStr, null);
+ viewBox.setFrame(rect[0], rect[1], rect[2], rect[3]);
+ }
+
+ // Get the 'width' and 'height' attributes of the SVG document
+ double width = 0;
+ double height = 0;
+ UnitProcessor.Context uctx = UnitProcessor.createContext(ctx, rootElement);
String widthStr = rootElement.getAttributeNS(null, SVGConstants.SVG_WIDTH_ATTRIBUTE);
String heightStr = rootElement.getAttributeNS(null, SVGConstants.SVG_HEIGHT_ATTRIBUTE);
if (!StringUtil.isEmpty(widthStr)) {
- defaultWidth = UnitProcessor.svgToUserSpace(widthStr, SVGConstants.SVG_WIDTH_ATTRIBUTE, UnitProcessor.HORIZONTAL_LENGTH, uctx);
+ width = UnitProcessor.svgToUserSpace(widthStr, SVGConstants.SVG_WIDTH_ATTRIBUTE, UnitProcessor.HORIZONTAL_LENGTH, uctx);
}
- if(!StringUtil.isEmpty(heightStr)){
- defaultHeight = UnitProcessor.svgToUserSpace(heightStr, SVGConstants.SVG_HEIGHT_ATTRIBUTE, UnitProcessor.VERTICAL_LENGTH, uctx);
+ if (!StringUtil.isEmpty(heightStr)) {
+ height = UnitProcessor.svgToUserSpace(heightStr, SVGConstants.SVG_HEIGHT_ATTRIBUTE, UnitProcessor.VERTICAL_LENGTH, uctx);
}
- boolean hasWidth = defaultWidth > 0.0;
- boolean hasHeight = defaultHeight > 0.0;
+ boolean hasWidth = width > 0.0;
+ boolean hasHeight = height > 0.0;
if (!hasWidth || !hasHeight) {
- String viewBoxStr = rootElement.getAttributeNS
- (null, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE);
- if (viewBoxStr.length() != 0) {
- float[] rect = ViewBox.parseViewBoxAttribute(rootElement, viewBoxStr, null);
- // if one dimension is given, calculate other by aspect ratio in viewBox
- // or use viewBox if no dimension is given
+ if (!viewBox.isEmpty()) {
+ // If one dimension is given, calculate other by aspect ratio in viewBox
if (hasWidth) {
- defaultHeight = defaultWidth * rect[3] / rect[2];
+ height = width * viewBox.getHeight() / viewBox.getWidth();
}
else if (hasHeight) {
- defaultWidth = defaultHeight * rect[2] / rect[3];
+ width = height * viewBox.getWidth() / viewBox.getHeight();
}
else {
- defaultWidth = rect[2];
- defaultHeight = rect[3];
+ // ...or use viewBox if no dimension is given
+ width = viewBox.getWidth();
+ height = viewBox.getHeight();
}
}
else {
+ // No viewBox, just assume square size
if (hasHeight) {
- defaultWidth = defaultHeight;
+ width = height;
}
else if (hasWidth) {
- defaultHeight = defaultWidth;
+ height = width;
}
else {
- // fallback to batik default sizes
- defaultWidth = 400;
- defaultHeight = 400;
+ // ...or finally fall back to Batik default sizes
+ width = 400;
+ height = 400;
}
}
}
+ // We now have a size, in the rare case we don't have a viewBox; set it to this size
+ defaultSize.setSize(width, height);
+ if (viewBox.isEmpty()) {
+ viewBox.setRect(0, 0, width, height);
+ }
+
// Hack to work around exception above
if (root != null) {
gvtRoot = root;
@@ -401,7 +396,7 @@ public class SVGImageReader extends ImageReaderBase {
ctx = null;
}
- private BufferedImage readImage() throws TranscoderException {
+ private BufferedImage readImage() throws IOException {
init();
if (abortRequested()) {
@@ -426,7 +421,8 @@ public class SVGImageReader extends ImageReaderBase {
}
if (gvtRoot == null) {
- throw exception;
+ Throwable cause = unwrapException(exception);
+ throw new IIOException(cause.getMessage(), cause);
}
}
ctx = context;
@@ -444,7 +440,7 @@ public class SVGImageReader extends ImageReaderBase {
// ----
- setImageSize(defaultWidth, defaultHeight);
+ setImageSize(defaultSize.width, defaultSize.height);
if (abortRequested()) {
processReadAborted();
@@ -458,18 +454,17 @@ public class SVGImageReader extends ImageReaderBase {
try {
Px = ViewBox.getViewTransform(ref, root, width, height, null);
-
}
catch (BridgeException ex) {
- throw new TranscoderException(ex);
+ throw new IIOException(ex.getMessage(), ex);
}
- if (Px.isIdentity() && (width != defaultWidth || height != defaultHeight)) {
+ if (Px.isIdentity() && (width != defaultSize.width || height != defaultSize.height)) {
// The document has no viewBox, we need to resize it by hand.
// we want to keep the document size ratio
float xscale, yscale;
- xscale = width / defaultWidth;
- yscale = height / defaultHeight;
+ xscale = width / defaultSize.width;
+ yscale = height / defaultSize.height;
float scale = Math.min(xscale, yscale);
Px = AffineTransform.getScaleInstance(scale, scale);
}
@@ -519,7 +514,7 @@ public class SVGImageReader extends ImageReaderBase {
}
}
catch (BridgeException ex) {
- throw new TranscoderException(ex);
+ throw new IIOException(ex.getMessage(), ex);
}
this.root = gvtRoot;
@@ -588,7 +583,7 @@ public class SVGImageReader extends ImageReaderBase {
return dest;
}
catch (Exception ex) {
- throw new TranscoderException(ex.getMessage(), ex);
+ throw new IIOException(ex.getMessage(), ex);
}
finally {
if (context != null) {
@@ -597,7 +592,7 @@ public class SVGImageReader extends ImageReaderBase {
}
}
- private synchronized void init() throws TranscoderException {
+ private synchronized void init() throws IIOException {
if (!initialized) {
if (transcoderInput == null) {
throw new IllegalStateException("input == null");
@@ -605,11 +600,17 @@ public class SVGImageReader extends ImageReaderBase {
initialized = true;
- super.transcode(transcoderInput, null);
+ try {
+ super.transcode(transcoderInput, null);
+ }
+ catch (TranscoderException e) {
+ Throwable cause = unwrapException(e);
+ throw new IIOException(cause.getMessage(), cause);
+ }
}
}
- private BufferedImage getImage() throws TranscoderException {
+ private BufferedImage getImage() throws IOException {
if (image == null) {
image = readImage();
}
@@ -617,14 +618,19 @@ public class SVGImageReader extends ImageReaderBase {
return image;
}
- int getDefaultWidth() throws TranscoderException {
+ int getDefaultWidth() throws IOException {
init();
- return (int) Math.ceil(defaultWidth);
+ return defaultSize.width;
}
- int getDefaultHeight() throws TranscoderException {
+ int getDefaultHeight() throws IOException {
init();
- return (int) Math.ceil(defaultHeight);
+ return defaultSize.height;
+ }
+
+ Rectangle getViewBox() throws IOException {
+ init();
+ return viewBox.getBounds();
}
void setInput(final TranscoderInput pInput) {
diff --git a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java
index 550f0cdb..ca0fdd6c 100755
--- a/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java
+++ b/imageio/imageio-batik/src/test/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderTest.java
@@ -43,8 +43,7 @@ import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.awt.image.ImagingOpException;
+import java.awt.image.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
@@ -53,7 +52,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
/**
@@ -192,12 +194,12 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest
TestData redSquare = new TestData(getClassLoaderResource("/svg/red-square.svg"), dim);
reader.setInput(redSquare.getInputStream());
BufferedImage imageRed = reader.read(0, param);
- assertEquals(0xFF0000, imageRed.getRGB(50, 50) & 0xFFFFFF);
+ assertRGBEquals("Expected all red", 0xFF0000, imageRed.getRGB(50, 50) & 0xFFFFFF, 0);
TestData blueSquare = new TestData(getClassLoaderResource("/svg/blue-square.svg"), dim);
reader.setInput(blueSquare.getInputStream());
BufferedImage imageBlue = reader.read(0, param);
- assertEquals(0x0000FF, imageBlue.getRGB(50, 50) & 0xFFFFFF);
+ assertRGBEquals("Expected all blue", 0x0000FF, imageBlue.getRGB(50, 50) & 0xFFFFFF, 0);
}
@Test
@@ -337,4 +339,70 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest
reader.dispose();
}
}
+
+ @Test
+ public void testReadWitSourceRenderSize() throws IOException {
+ URL resource = getClassLoaderResource("/svg/circle.svg");
+
+ SVGImageReader reader = createReader();
+
+ TestData data = new TestData(resource, (Dimension) null);
+ try (ImageInputStream stream = data.getInputStream()) {
+ reader.setInput(stream);
+
+ SVGReadParam param = reader.getDefaultReadParam();
+ param.setSourceRenderSize(new Dimension(100, 100));
+ BufferedImage image = reader.read(0, param);
+
+ assertNotNull(image);
+ assertEquals(100, image.getWidth());
+ assertEquals(100, image.getHeight());
+
+ // Some quick samples
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(0, 0), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(99, 0), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(0, 99), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(99, 99), 0);
+ assertRGBEquals("Expected red center", 0xffff0000, image.getRGB(50, 50), 0);
+ }
+ finally {
+ reader.dispose();
+ }
+ }
+
+ @Test
+ public void testReadWitSourceRenderSizeViewBoxNegativeXY() throws IOException {
+ // TODO: Fix the reader so that this also works with /svg/Android_robot.svg, probably needs some translation to compensate for the viewBox
+ URL resource = getClassLoaderResource("/svg/Android_robot.svg");
+
+ SVGImageReader reader = createReader();
+
+ TestData data = new TestData(resource, (Dimension) null);
+ try (ImageInputStream stream = data.getInputStream()) {
+ reader.setInput(stream);
+
+ SVGReadParam param = reader.getDefaultReadParam();
+ param.setSourceRenderSize(new Dimension(219, 256)); // Aspect scaled to 256 boxed
+ BufferedImage image = reader.read(0, param);
+
+ assertNotNull(image);
+ assertEquals(219, image.getWidth());
+ assertEquals(256, image.getHeight());
+
+ // Some quick samples
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(0, 0), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(218, 0), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(0, 255), 0);
+ assertRGBEquals("Expected transparent corner", 0, image.getRGB(218, 255), 0);
+ assertRGBEquals("Expected green head", 0xffa4c639, image.getRGB(109, 20), 0);
+ assertRGBEquals("Expected green center", 0xffa4c639, image.getRGB(109, 128), 0);
+ assertRGBEquals("Expected green feet", 0xffa4c639, image.getRGB(80, 246), 0);
+ assertRGBEquals("Expected green feet", 0xffa4c639, image.getRGB(130, 246), 0);
+ assertRGBEquals("Expected white edge", 0xffffffff, image.getRGB(0, 128), 0);
+ assertRGBEquals("Expected white edge", 0xffffffff, image.getRGB(218, 128), 0);
+ }
+ finally {
+ reader.dispose();
+ }
+ }
}
diff --git a/imageio/imageio-batik/src/test/resources/svg/circle.svg b/imageio/imageio-batik/src/test/resources/svg/circle.svg
new file mode 100644
index 00000000..77e2e041
--- /dev/null
+++ b/imageio/imageio-batik/src/test/resources/svg/circle.svg
@@ -0,0 +1,6 @@
+
+