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 @@ + + +