com.twelvemonkeys.imageio.plugins.svg.SVGImageReader Maven / Gradle / Ivy
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.svg;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import org.apache.batik.bridge.*;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.dom.svg.SVGOMDocument;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.CanvasGraphicsNode;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.gvt.renderer.ImageRendererFactory;
import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.util.ParsedURL;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGSVGElement;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
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;
/**
* Image reader for SVG document fragments.
*
*
* @author Harald Kuhr
* @author Inpspired by code from the Batik Team
* @version $Id: $
* @see batik-dev
*/
public class SVGImageReader extends ImageReaderBase {
private Rasterizer rasterizer;
/**
* Creates an {@code SVGImageReader}.
*
* @param pProvider the provider
*/
public SVGImageReader(final ImageReaderSpi pProvider) {
super(pProvider);
}
protected void resetMembers() {
rasterizer = new Rasterizer();
}
@Override
public void dispose() {
super.dispose();
rasterizer = null;
}
@Override
public void setInput(Object pInput, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(pInput, seekForwardOnly, ignoreMetadata);
if (imageInput != null) {
TranscoderInput input = new TranscoderInput(IIOUtil.createStreamAdapter(imageInput));
rasterizer.setInput(input);
}
}
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException {
checkBounds(pIndex);
String baseURI = null;
if (pParam instanceof SVGReadParam) {
SVGReadParam svgParam = (SVGReadParam) pParam;
// Set IIOParams as hints
// Note: The cast to Map invokes a different method that preserves
// unset defaults, DO NOT REMOVE!
rasterizer.setTranscodingHints((Map) paramsToHints(svgParam));
// Get the base URI (not a hint)
baseURI = svgParam.getBaseURI();
}
Dimension size;
if (pParam != null && (size = pParam.getSourceRenderSize()) != null) {
// Use size...
}
else {
size = new Dimension(getWidth(pIndex), getHeight(pIndex));
}
BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height);
// Read in the image, using the Batik Transcoder
try {
processImageStarted(pIndex);
rasterizer.transcoderInput.setURI(baseURI);
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;
}
catch (TranscoderException e) {
throw new IIOException(e.getMessage(), e);
}
}
private TranscodingHints paramsToHints(SVGReadParam pParam) throws IOException {
TranscodingHints hints = new TranscodingHints();
// Note: We must allow generic ImageReadParams, so converting to
// TanscodingHints should be done outside the SVGReadParam class.
// Set dimensions
Dimension size = pParam.getSourceRenderSize();
Dimension origSize = new Dimension(getWidth(0), getHeight(0));
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);
}
if (size != null) {
hints.put(ImageTranscoder.KEY_WIDTH, new Float(size.getWidth()));
hints.put(ImageTranscoder.KEY_HEIGHT, new Float(size.getHeight()));
}
// Set area of interest
Rectangle region = pParam.getSourceRegion();
if (region != null) {
hints.put(ImageTranscoder.KEY_AOI, region);
// Avoid that the batik transcoder scales the AOI up to original image size
if (size == null) {
hints.put(ImageTranscoder.KEY_WIDTH, new Float(region.getWidth()));
hints.put(ImageTranscoder.KEY_HEIGHT, new Float(region.getHeight()));
}
else {
// Need to resize here...
double xScale = size.getWidth() / origSize.getWidth();
double yScale = size.getHeight() / origSize.getHeight();
hints.put(ImageTranscoder.KEY_WIDTH, new Float(region.getWidth() * xScale));
hints.put(ImageTranscoder.KEY_HEIGHT, new Float(region.getHeight() * yScale));
}
}
else if (size != null) {
// Allow non-uniform scaling
hints.put(ImageTranscoder.KEY_AOI, new Rectangle(origSize));
}
// Background color
Paint bg = pParam.getBackgroundColor();
if (bg != null) {
hints.put(ImageTranscoder.KEY_BACKGROUND_COLOR, bg);
}
return hints;
}
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()));
}
return null;
}
public ImageReadParam getDefaultReadParam() {
return new SVGReadParam();
}
public int getWidth(int pIndex) throws IOException {
checkBounds(pIndex);
try {
return rasterizer.getDefaultWidth();
}
catch (TranscoderException e) {
throw new IIOException(e.getMessage(), e);
}
}
public int getHeight(int pIndex) throws IOException {
checkBounds(pIndex);
try {
return rasterizer.getDefaultHeight();
}
catch (TranscoderException e) {
throw new IIOException(e.getMessage(), e);
}
}
public Iterator getImageTypes(int imageIndex) throws IOException {
return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator();
}
/**
* An image transcoder that stores the resulting image.
*
* NOTE: This class includes a lot of copy and paste code from the Batik classes
* and needs major refactoring!
*/
private class Rasterizer extends SVGAbstractTranscoder /*ImageTranscoder*/ {
BufferedImage image = null;
private TranscoderInput transcoderInput;
private float defaultWidth;
private float defaultHeight;
private boolean initialized = false;
private SVGOMDocument document;
private String uri;
private GraphicsNode gvtRoot;
private TranscoderException exception;
private BridgeContext context;
public BufferedImage createImage(final int width, final int height) {
return ImageUtil.createTransparent(width, height);//, BufferedImage.TYPE_INT_ARGB);
}
// This is cheating... We don't fully transcode after all
protected void transcode(Document document, final String uri, final TranscoderOutput output) throws TranscoderException {
// Sets up root, curTxf & curAoi
// ----
if ((document != null) && !(document.getImplementation() instanceof SVGDOMImplementation)) {
DOMImplementation impl = (DOMImplementation) hints.get(KEY_DOM_IMPLEMENTATION);
document = DOMUtilities.deepCloneDocument(document, impl);
if (uri != null) {
try {
URL url = new URL(uri);
((SVGOMDocument) document).setURLObject(url);
}
catch (MalformedURLException ignore) {
}
}
}
ctx = createBridgeContext();
SVGOMDocument svgDoc = (SVGOMDocument) document;
// build the GVT tree
builder = new GVTBuilder();
// flag that indicates if the document is dynamic
boolean isDynamic =
(hints.containsKey(KEY_EXECUTE_ONLOAD) &&
(Boolean) hints.get(KEY_EXECUTE_ONLOAD) &&
BaseScriptingEnvironment.isDynamicDocument(ctx, svgDoc));
if (isDynamic) {
ctx.setDynamicState(BridgeContext.DYNAMIC);
}
// Modified code below:
GraphicsNode root = null;
try {
root = builder.build(ctx, svgDoc);
}
catch (BridgeException ex) {
// Note: This might fail, but we STILL have the dimensions we need
// However, we need to reparse later...
exception = new TranscoderException(ex);
}
// ----
// get the 'width' and 'height' attributes of the SVG document
Dimension2D docSize = ctx.getDocumentSize();
if (docSize != null) {
defaultWidth = (float) docSize.getWidth();
defaultHeight = (float) docSize.getHeight();
}
else {
defaultWidth = 200;
defaultHeight = 200;
}
// Hack to work around exception above
if (root != null) {
gvtRoot = root;
}
this.document = svgDoc;
this.uri = uri;
// Hack to avoid the transcode method wacking my context...
context = ctx;
ctx = null;
}
private BufferedImage readImage() throws TranscoderException {
init();
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(10f);
// Hacky workaround below...
if (gvtRoot == null) {
// Try to reparse, if we had no URI last time...
if (uri != transcoderInput.getURI()) {
try {
context.dispose();
document.setURLObject(new URL(transcoderInput.getURI()));
transcode(document, transcoderInput.getURI(), null);
}
catch (MalformedURLException ignore) {
// Ignored
}
}
if (gvtRoot == null) {
throw exception;
}
}
ctx = context;
// /Hacky
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(20f);
// ----
SVGSVGElement root = document.getRootElement();
// ----
// ----
setImageSize(defaultWidth, defaultHeight);
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(40f);
// compute the preserveAspectRatio matrix
AffineTransform Px;
String ref = new ParsedURL(uri).getRef();
try {
Px = ViewBox.getViewTransform(ref, root, width, height);
}
catch (BridgeException ex) {
throw new TranscoderException(ex);
}
if (Px.isIdentity() && (width != defaultWidth || height != defaultHeight)) {
// 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;
float scale = Math.min(xscale, yscale);
Px = AffineTransform.getScaleInstance(scale, scale);
}
// take the AOI into account if any
if (hints.containsKey(KEY_AOI)) {
Rectangle2D aoi = (Rectangle2D) hints.get(KEY_AOI);
// transform the AOI into the image's coordinate system
aoi = Px.createTransformedShape(aoi).getBounds2D();
AffineTransform Mx = new AffineTransform();
double sx = width / aoi.getWidth();
double sy = height / aoi.getHeight();
Mx.scale(sx, sy);
double tx = -aoi.getX();
double ty = -aoi.getY();
Mx.translate(tx, ty);
// take the AOI transformation matrix into account
// we apply first the preserveAspectRatio matrix
Px.preConcatenate(Mx);
curAOI = aoi;
}
else {
curAOI = new Rectangle2D.Float(0, 0, width, height);
}
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(50f);
CanvasGraphicsNode cgn = getCanvasGraphicsNode(gvtRoot);
if (cgn != null) {
cgn.setViewingTransform(Px);
curTxf = new AffineTransform();
}
else {
curTxf = Px;
}
try {
// dispatch an 'onload' event if needed
if (ctx.isDynamic()) {
BaseScriptingEnvironment se;
se = new BaseScriptingEnvironment(ctx);
se.loadScripts();
se.dispatchSVGLoadEvent();
}
}
catch (BridgeException ex) {
throw new TranscoderException(ex);
}
this.root = gvtRoot;
// ----
// NOTE: The code below is copied and pasted from the Batik
// ImageTranscoder class' transcode() method:
// prepare the image to be painted
int w = (int) (width + 0.5);
int h = (int) (height + 0.5);
// paint the SVG document using the bridge package
// create the appropriate renderer
ImageRendererFactory rendFactory = new ConcreteImageRendererFactory();
// ImageRenderer renderer = rendFactory.createDynamicImageRenderer();
ImageRenderer renderer = rendFactory.createStaticImageRenderer();
renderer.updateOffScreen(w, h);
renderer.setTransform(curTxf);
renderer.setTree(this.root);
this.root = null; // We're done with it...
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(75f);
try {
// now we are sure that the aoi is the image size
Shape raoi = new Rectangle2D.Float(0, 0, width, height);
// Warning: the renderer's AOI must be in user space
renderer.repaint(curTxf.createInverse().
createTransformedShape(raoi));
// NOTE: repaint above cause nullpointer exception with fonts..???
BufferedImage rend = renderer.getOffScreen();
renderer = null; // We're done with it...
BufferedImage dest = createImage(w, h);
Graphics2D g2d = GraphicsUtil.createGraphics(dest);
try {
if (hints.containsKey(ImageTranscoder.KEY_BACKGROUND_COLOR)) {
Paint bgcolor = (Paint) hints.get(ImageTranscoder.KEY_BACKGROUND_COLOR);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setPaint(bgcolor);
g2d.fillRect(0, 0, w, h);
}
if (rend != null) { // might be null if the svg document is empty
g2d.drawRenderedImage(rend, new AffineTransform());
}
}
finally {
if (g2d != null) {
g2d.dispose();
}
}
if (abortRequested()) {
processReadAborted();
return null;
}
processImageProgress(99f);
return dest;
}
catch (Exception ex) {
TranscoderException exception = new TranscoderException(ex.getMessage());
exception.initCause(ex);
throw exception;
}
finally {
if (context != null) {
context.dispose();
}
}
}
private synchronized void init() throws TranscoderException {
if (!initialized) {
if (transcoderInput == null) {
throw new IllegalStateException("input == null");
}
initialized = true;
super.transcode(transcoderInput, null);
}
}
private BufferedImage getImage() throws TranscoderException {
if (image == null) {
image = readImage();
}
return image;
}
protected int getDefaultWidth() throws TranscoderException {
init();
return (int) (defaultWidth + 0.5);
}
protected int getDefaultHeight() throws TranscoderException {
init();
return (int) (defaultHeight + 0.5);
}
public void setInput(final TranscoderInput pInput) {
transcoderInput = pInput;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy