blob: 1d5b227c5c75a325cd7015cc07f6c03e485e6626 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.util.svg;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.ExternalResourceSecurity;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.NoLoadExternalResourceSecurity;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.XMLResourceDescriptor;
import org.openide.util.CachedHiDPIIcon;
import org.openide.util.Parameters;
import org.w3c.dom.Document;
/**
* An icon loaded from an SVG file resource. Renders in high resolution on HiDPI displays.
* Thread-safe.
*/
final class SVGIcon extends CachedHiDPIIcon {
private static final Logger LOG = Logger.getLogger(SVGIcon.class.getName());
/* A limit of 8192 pixels on each side means the resulting maximum image buffer would take 268
megabytes. It's also twice twice as long as the longest side of a 4K display. This is small
enough to avoid an OutOfMemoryError but large enough to cater for most SVG loading scenarios.
Photoshop had 10000 pixels as a maximum limit for many years. */
private static final int MAX_DIMENSION_PIXELS = 8192;
// XML document factories are expensive to initialize, so do it once per thread only.
private static final ThreadLocal<SAXSVGDocumentFactory> DOCUMENT_FACTORY =
new ThreadLocal<SAXSVGDocumentFactory>()
{
@Override
protected SAXSVGDocumentFactory initialValue() {
return new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
}
};
private final URL url;
/**
* Cache of the parsed SVG document. Just painting the GraphicsNode is much faster than also
* re-parsing the underlying SVG file, yet we want to avoid keeping potentially complex object
* trees in memory for the lifetime of the Icon instance. Thus we allow the GraphicsNode to be
* garbage collected after the first paint. The rasterized bitmap will be cached separately by
* the superclass.
*/
private WeakReference<GraphicsNode> graphicsNodeWeakRef;
/**
* A strong reference version of {@link #graphicsNodeWeakRef}, which can be set to ensure that
* the latter is not yet garbage collected. Used to ensure that the initially loaded
* GraphicsNode is cached at least until the first time the icon is painted. May be null.
*/
private GraphicsNode graphicsNodeStrongRef;
private SVGIcon(URL url, GraphicsNode initialGraphicsNode, int width, int height) {
super(width, height);
Parameters.notNull("url", url);
Parameters.notNull("initialGraphicsNode", initialGraphicsNode);
this.url = url;
this.graphicsNodeStrongRef = initialGraphicsNode;
this.graphicsNodeWeakRef = new WeakReference<GraphicsNode>(initialGraphicsNode);
}
public static Icon load(URL url) throws IOException {
Parameters.notNull("url", url);
Dimension size = new Dimension();
GraphicsNode initialGraphicsNode = loadGraphicsNode(url, size);
return new SVGIcon(url, initialGraphicsNode, size.width, size.height);
}
/**
* Get the {@code GraphicsNode}, re-loading it from the original resource if a cached instance
* is no longer available. Once this method has been called at least once, garbage collection
* may cause the cache to be cleared.
*/
private synchronized GraphicsNode getGraphicsNode() throws IOException {
GraphicsNode ret = graphicsNodeWeakRef.get();
if (ret != null) {
// Allow the GraphicsNode to be garbage collected after the initial paint.
graphicsNodeStrongRef = null;
return ret;
}
ret = loadGraphicsNode(url, null);
graphicsNodeWeakRef = new WeakReference<GraphicsNode>(ret);
return ret;
}
/**
* Load the original SVG resource.
*
* @param toSize if not null, will be set to the image's size
*/
private static GraphicsNode loadGraphicsNode(URL url, Dimension toSize)
throws IOException
{
Parameters.notNull("url", url);
final GraphicsNode graphicsNode;
final Dimension2D documentSize;
final Document doc;
InputStream is = url.openStream();
try {
// See http://batik.2283329.n4.nabble.com/rendering-directly-to-java-awt-Graphics2D-td3716202.html
SAXSVGDocumentFactory factory = DOCUMENT_FACTORY.get();
/* Don't provide an URI here; we shouldn't commit to supporting relative links from
loaded SVG documents. */
doc = factory.createDocument(null, is);
// Disallow external resource dereferences
UserAgent userAgent = new UserAgentAdapter() {
@Override
public ExternalResourceSecurity getExternalResourceSecurity(
ParsedURL resourceURL, ParsedURL docURL) {
return new NoLoadExternalResourceSecurity();
}
};
DocumentLoader loader = new DocumentLoader(userAgent);
BridgeContext bctx = new BridgeContext(userAgent, loader);
try {
bctx.setDynamicState(BridgeContext.STATIC);
graphicsNode = new GVTBuilder().build(bctx, doc);
documentSize = bctx.getDocumentSize();
} finally {
bctx.dispose();
}
} catch (RuntimeException e) {
/* Rethrow the many different exceptions that can occur when parsing invalid SVG files;
DOMException, BridgeException etc. */
throw new IOException("Error parsing SVG file", e);
} finally {
is.close();
}
if (toSize != null) {
int width = (int) Math.ceil(documentSize.getWidth());
int height = (int) Math.ceil(documentSize.getHeight());
final int widthLimited = Math.min(MAX_DIMENSION_PIXELS, width);
final int heightLimited = Math.min(MAX_DIMENSION_PIXELS, height);
if (width != widthLimited || height != heightLimited) {
LOG.log(Level.WARNING,
"SVG image {0} too large (dimensions were {1}x{2}, each side can be at most {3}px)",
new Object[]{url, width, height, MAX_DIMENSION_PIXELS});
} else if (width <= 1 && height <= 1) {
LOG.log(Level.WARNING,
"SVG image {0} did not specify a width/height, or is incorrectly sized", url);
}
toSize.width = widthLimited;
toSize.height = heightLimited;
}
return graphicsNode;
}
private static RenderingHints createHints() {
Map<RenderingHints.Key, Object> hints = new LinkedHashMap<RenderingHints.Key, Object>();
hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
/* Ensure that outlined strokes (strokes converted to solid shapes) appear the same as
regular strokes, as they do during editing in Adobe Illustrator. */
hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
return new RenderingHints(hints);
}
@Override
protected Image createAndPaintImage(
Component c, ColorModel colorModel, int deviceWidth, int deviceHeight, double scale)
{
BufferedImage img = createBufferedImage(colorModel, deviceWidth, deviceHeight);
/* Use Batik's createGraphics method to improve performance and avoid the
"Graphics2D from BufferedImage lacks BUFFERED_IMAGE hint" warning. */
final Graphics2D g = GraphicsUtil.createGraphics(img);
try {
g.scale(scale, scale);
try {
GraphicsNode graphicsNode = getGraphicsNode();
g.addRenderingHints(createHints());
graphicsNode.paint(g);
} catch (IOException e) {
LOG.log(Level.WARNING,
"Unexpected exception while re-loading an SVG file that previously loaded successfully", e);
}
} finally {
g.dispose();
}
return img;
}
}