#684 Fix some render size issues in SVGImageReader

Bonus: Minor code clean-up.
This commit is contained in:
Harald Kuhr 2022-06-10 17:24:47 +02:00
parent 00aec2c90e
commit be2d7d5f10
4 changed files with 190 additions and 110 deletions

View File

@ -27,9 +27,9 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<systemPropertyVariables> <systemPropertyVariables>
<com.twelvemonkeys.imageio.plugins.svg.allowexternalresources> <com.twelvemonkeys.imageio.plugins.svg.allowExternalResources>
true true
</com.twelvemonkeys.imageio.plugins.svg.allowexternalresources> </com.twelvemonkeys.imageio.plugins.svg.allowExternalResources>
</systemPropertyVariables> </systemPropertyVariables>
</configuration> </configuration>
</plugin> </plugin>

View File

@ -30,21 +30,10 @@
package com.twelvemonkeys.imageio.plugins.svg; package com.twelvemonkeys.imageio.plugins.svg;
import java.awt.*; import com.twelvemonkeys.image.ImageUtil;
import java.awt.geom.AffineTransform; import com.twelvemonkeys.imageio.ImageReaderBase;
import java.awt.geom.Rectangle2D; import com.twelvemonkeys.imageio.util.IIOUtil;
import java.awt.image.BufferedImage; import com.twelvemonkeys.lang.StringUtil;
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 org.apache.batik.anim.dom.SVGDOMImplementation; import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.anim.dom.SVGOMDocument; 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.Document;
import org.w3c.dom.svg.SVGSVGElement; import org.w3c.dom.svg.SVGSVGElement;
import com.twelvemonkeys.image.ImageUtil; import javax.imageio.IIOException;
import com.twelvemonkeys.imageio.ImageReaderBase; import javax.imageio.ImageReadParam;
import com.twelvemonkeys.imageio.util.IIOUtil; import javax.imageio.ImageTypeSpecifier;
import com.twelvemonkeys.lang.StringUtil; 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. * Image reader for SVG document fragments.
@ -79,12 +77,13 @@ import com.twelvemonkeys.lang.StringUtil;
* @author Harald Kuhr * @author Harald Kuhr
* @author Inpspired by code from the Batik Team * @author Inpspired by code from the Batik Team
* @version $Id: $ * @version $Id: $
* @see <A href="http://www.mail-archive.com/batik-dev@xml.apache.org/msg00992.html">batik-dev</A> * @see <a href="http://www.mail-archive.com/batik-dev@xml.apache.org/msg00992.html">batik-dev</a>
*/ */
public class SVGImageReader extends ImageReaderBase { public class SVGImageReader extends ImageReaderBase {
final static boolean DEFAULT_ALLOW_EXTERNAL_RESOURCES = 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 Rasterizer rasterizer;
private boolean allowExternalResources = DEFAULT_ALLOW_EXTERNAL_RESOURCES; private boolean allowExternalResources = DEFAULT_ALLOW_EXTERNAL_RESOURCES;
@ -150,7 +149,6 @@ public class SVGImageReader extends ImageReaderBase {
BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height); BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height);
// Read in the image, using the Batik Transcoder // Read in the image, using the Batik Transcoder
try {
processImageStarted(pIndex); processImageStarted(pIndex);
BufferedImage image = rasterizer.getImage(); BufferedImage image = rasterizer.getImage();
@ -169,11 +167,6 @@ public class SVGImageReader extends ImageReaderBase {
return destination; return destination;
} }
catch (TranscoderException e) {
Throwable cause = unwrapException(e);
throw new IIOException(cause.getMessage(), cause);
}
}
private static Throwable unwrapException(TranscoderException ex) { private static Throwable unwrapException(TranscoderException ex) {
// The TranscoderException is generally useless... // The TranscoderException is generally useless...
@ -187,11 +180,11 @@ public class SVGImageReader extends ImageReaderBase {
// Set dimensions // Set dimensions
Dimension size = pParam.getSourceRenderSize(); Dimension size = pParam.getSourceRenderSize();
Dimension origSize = new Dimension(getWidth(0), getHeight(0)); Rectangle viewBox = rasterizer.getViewBox();
if (size == null) { if (size == null) {
// SVG is not a pixel based format, but we'll scale it, according to // SVG is not a pixel based format, but we'll scale it, according to
// the subsampling for compatibility // the subsampling for compatibility
size = getSourceRenderSizeFromSubsamping(pParam, origSize); size = getSourceRenderSizeFromSubsamping(pParam, viewBox.getSize());
} }
if (size != null) { if (size != null) {
@ -211,8 +204,8 @@ public class SVGImageReader extends ImageReaderBase {
} }
else { else {
// Need to resize here... // Need to resize here...
double xScale = size.getWidth() / origSize.getWidth(); double xScale = size.getWidth() / viewBox.getWidth();
double yScale = size.getHeight() / origSize.getHeight(); double yScale = size.getHeight() / viewBox.getHeight();
hints.put(ImageTranscoder.KEY_WIDTH, (float) (region.getWidth() * xScale)); hints.put(ImageTranscoder.KEY_WIDTH, (float) (region.getWidth() * xScale));
hints.put(ImageTranscoder.KEY_HEIGHT, (float) (region.getHeight() * yScale)); hints.put(ImageTranscoder.KEY_HEIGHT, (float) (region.getHeight() * yScale));
@ -220,7 +213,7 @@ public class SVGImageReader extends ImageReaderBase {
} }
else if (size != null) { else if (size != null) {
// Allow non-uniform scaling // Allow non-uniform scaling
hints.put(ImageTranscoder.KEY_AOI, new Rectangle(origSize)); hints.put(ImageTranscoder.KEY_AOI, viewBox);
} }
// Background color // Background color
@ -246,23 +239,14 @@ public class SVGImageReader extends ImageReaderBase {
public int getWidth(int pIndex) throws IOException { public int getWidth(int pIndex) throws IOException {
checkBounds(pIndex); checkBounds(pIndex);
try {
return rasterizer.getDefaultWidth(); return rasterizer.getDefaultWidth();
} }
catch (TranscoderException e) {
throw new IIOException(e.getMessage(), e);
}
}
public int getHeight(int pIndex) throws IOException { public int getHeight(int pIndex) throws IOException {
checkBounds(pIndex); checkBounds(pIndex);
try {
return rasterizer.getDefaultHeight(); return rasterizer.getDefaultHeight();
} }
catch (TranscoderException e) {
throw new IIOException(e.getMessage(), e);
}
}
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) { public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) {
return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator(); return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator();
@ -275,12 +259,11 @@ public class SVGImageReader extends ImageReaderBase {
* and needs major refactoring! * and needs major refactoring!
* </p> * </p>
*/ */
private class Rasterizer extends SVGAbstractTranscoder /*ImageTranscoder*/ { private class Rasterizer extends SVGAbstractTranscoder {
private BufferedImage image; private BufferedImage image;
private TranscoderInput transcoderInput; private TranscoderInput transcoderInput;
private float defaultWidth; private final Rectangle2D viewBox = new Rectangle2D.Float();
private float defaultHeight; private final Dimension defaultSize = new Dimension();
private boolean initialized = false; private boolean initialized = false;
private SVGOMDocument document; private SVGOMDocument document;
private String uri; private String uri;
@ -341,54 +324,66 @@ public class SVGImageReader extends ImageReaderBase {
// ---- // ----
SVGSVGElement rootElement = svgDoc.getRootElement(); SVGSVGElement rootElement = svgDoc.getRootElement();
// get the 'width' and 'height' attributes of the SVG document // Get the viewBox
UnitProcessor.Context uctx String viewBoxStr = rootElement.getAttributeNS(null, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE);
= UnitProcessor.createContext(ctx, rootElement); 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 widthStr = rootElement.getAttributeNS(null, SVGConstants.SVG_WIDTH_ATTRIBUTE);
String heightStr = rootElement.getAttributeNS(null, SVGConstants.SVG_HEIGHT_ATTRIBUTE); String heightStr = rootElement.getAttributeNS(null, SVGConstants.SVG_HEIGHT_ATTRIBUTE);
if (!StringUtil.isEmpty(widthStr)) { 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)) { if (!StringUtil.isEmpty(heightStr)) {
defaultHeight = UnitProcessor.svgToUserSpace(heightStr, SVGConstants.SVG_HEIGHT_ATTRIBUTE, UnitProcessor.VERTICAL_LENGTH, uctx); height = UnitProcessor.svgToUserSpace(heightStr, SVGConstants.SVG_HEIGHT_ATTRIBUTE, UnitProcessor.VERTICAL_LENGTH, uctx);
} }
boolean hasWidth = defaultWidth > 0.0; boolean hasWidth = width > 0.0;
boolean hasHeight = defaultHeight > 0.0; boolean hasHeight = height > 0.0;
if (!hasWidth || !hasHeight) { if (!hasWidth || !hasHeight) {
String viewBoxStr = rootElement.getAttributeNS if (!viewBox.isEmpty()) {
(null, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE); // If one dimension is given, calculate other by aspect ratio in viewBox
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 (hasWidth) { if (hasWidth) {
defaultHeight = defaultWidth * rect[3] / rect[2]; height = width * viewBox.getHeight() / viewBox.getWidth();
} }
else if (hasHeight) { else if (hasHeight) {
defaultWidth = defaultHeight * rect[2] / rect[3]; width = height * viewBox.getWidth() / viewBox.getHeight();
} }
else { else {
defaultWidth = rect[2]; // ...or use viewBox if no dimension is given
defaultHeight = rect[3]; width = viewBox.getWidth();
height = viewBox.getHeight();
} }
} }
else { else {
// No viewBox, just assume square size
if (hasHeight) { if (hasHeight) {
defaultWidth = defaultHeight; width = height;
} }
else if (hasWidth) { else if (hasWidth) {
defaultHeight = defaultWidth; height = width;
} }
else { else {
// fallback to batik default sizes // ...or finally fall back to Batik default sizes
defaultWidth = 400; width = 400;
defaultHeight = 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 // Hack to work around exception above
if (root != null) { if (root != null) {
gvtRoot = root; gvtRoot = root;
@ -401,7 +396,7 @@ public class SVGImageReader extends ImageReaderBase {
ctx = null; ctx = null;
} }
private BufferedImage readImage() throws TranscoderException { private BufferedImage readImage() throws IOException {
init(); init();
if (abortRequested()) { if (abortRequested()) {
@ -426,7 +421,8 @@ public class SVGImageReader extends ImageReaderBase {
} }
if (gvtRoot == null) { if (gvtRoot == null) {
throw exception; Throwable cause = unwrapException(exception);
throw new IIOException(cause.getMessage(), cause);
} }
} }
ctx = context; ctx = context;
@ -444,7 +440,7 @@ public class SVGImageReader extends ImageReaderBase {
// ---- // ----
setImageSize(defaultWidth, defaultHeight); setImageSize(defaultSize.width, defaultSize.height);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
@ -458,18 +454,17 @@ public class SVGImageReader extends ImageReaderBase {
try { try {
Px = ViewBox.getViewTransform(ref, root, width, height, null); Px = ViewBox.getViewTransform(ref, root, width, height, null);
} }
catch (BridgeException ex) { 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. // The document has no viewBox, we need to resize it by hand.
// we want to keep the document size ratio // we want to keep the document size ratio
float xscale, yscale; float xscale, yscale;
xscale = width / defaultWidth; xscale = width / defaultSize.width;
yscale = height / defaultHeight; yscale = height / defaultSize.height;
float scale = Math.min(xscale, yscale); float scale = Math.min(xscale, yscale);
Px = AffineTransform.getScaleInstance(scale, scale); Px = AffineTransform.getScaleInstance(scale, scale);
} }
@ -519,7 +514,7 @@ public class SVGImageReader extends ImageReaderBase {
} }
} }
catch (BridgeException ex) { catch (BridgeException ex) {
throw new TranscoderException(ex); throw new IIOException(ex.getMessage(), ex);
} }
this.root = gvtRoot; this.root = gvtRoot;
@ -588,7 +583,7 @@ public class SVGImageReader extends ImageReaderBase {
return dest; return dest;
} }
catch (Exception ex) { catch (Exception ex) {
throw new TranscoderException(ex.getMessage(), ex); throw new IIOException(ex.getMessage(), ex);
} }
finally { finally {
if (context != null) { 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 (!initialized) {
if (transcoderInput == null) { if (transcoderInput == null) {
throw new IllegalStateException("input == null"); throw new IllegalStateException("input == null");
@ -605,11 +600,17 @@ public class SVGImageReader extends ImageReaderBase {
initialized = true; initialized = true;
try {
super.transcode(transcoderInput, null); 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) { if (image == null) {
image = readImage(); image = readImage();
} }
@ -617,14 +618,19 @@ public class SVGImageReader extends ImageReaderBase {
return image; return image;
} }
int getDefaultWidth() throws TranscoderException { int getDefaultWidth() throws IOException {
init(); init();
return (int) Math.ceil(defaultWidth); return defaultSize.width;
} }
int getDefaultHeight() throws TranscoderException { int getDefaultHeight() throws IOException {
init(); init();
return (int) Math.ceil(defaultHeight); return defaultSize.height;
}
Rectangle getViewBox() throws IOException {
init();
return viewBox.getBounds();
} }
void setInput(final TranscoderInput pInput) { void setInput(final TranscoderInput pInput) {

View File

@ -43,8 +43,7 @@ import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.*;
import java.awt.image.ImagingOpException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -53,7 +52,10 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.*; import static org.mockito.Mockito.*;
/** /**
@ -192,12 +194,12 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest<SVGImageReader>
TestData redSquare = new TestData(getClassLoaderResource("/svg/red-square.svg"), dim); TestData redSquare = new TestData(getClassLoaderResource("/svg/red-square.svg"), dim);
reader.setInput(redSquare.getInputStream()); reader.setInput(redSquare.getInputStream());
BufferedImage imageRed = reader.read(0, param); 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); TestData blueSquare = new TestData(getClassLoaderResource("/svg/blue-square.svg"), dim);
reader.setInput(blueSquare.getInputStream()); reader.setInput(blueSquare.getInputStream());
BufferedImage imageBlue = reader.read(0, param); 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 @Test
@ -337,4 +339,70 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest<SVGImageReader>
reader.dispose(); 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();
}
}
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
<circle cx="25" cy="25" r="25" fill="red"/></svg>

After

Width:  |  Height:  |  Size: 436 B