ImageHandler.java
package io.brunoborges.jairosvg.draw;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.MemoryCacheImageInputStream;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.surface.Surface;
import io.brunoborges.jairosvg.util.UrlHelper;
import static io.brunoborges.jairosvg.util.Helpers.*;
/**
* SVG image element handler.
*/
public final class ImageHandler {
private static final System.Logger LOG = System.getLogger(ImageHandler.class.getName());
private ImageHandler() {
}
/** Draw an image node. */
public static void image(Surface surface, Node node) {
String href = node.getHref();
if (href == null || href.isEmpty())
return;
// Fast path for data: URIs — skip URL parsing overhead
byte[] imageBytes;
String cacheKey;
String resolvedUrl;
if (href.startsWith("data:")) {
cacheKey = href;
resolvedUrl = href;
imageBytes = UrlHelper.decodeDataUrl(href);
} else {
UrlHelper.ParsedUrl url = UrlHelper.parseUrl(href, UrlHelper.resolveBaseUrl(node));
cacheKey = url.getUrl();
resolvedUrl = cacheKey;
try {
imageBytes = node.fetchUrl(url, "image/*");
} catch (IOException e) {
LOG.log(System.Logger.Level.DEBUG, "Failed to fetch image: {0}", e.getMessage());
return;
}
}
if (imageBytes == null || imageBytes.length < 5)
return;
double x = size(surface, node.get("x"), "x");
double y = size(surface, node.get("y"), "y");
double width = size(surface, node.get("width"), "x");
double height = size(surface, node.get("height"), "y");
double opacity = 1;
String opacityStr = node.get("opacity");
if (opacityStr != null)
opacity = Double.parseDouble(opacityStr);
// Check if it's an SVG image
if (isSvgContent(imageBytes)) {
try {
Node tree = Node.parseTree(imageBytes, resolvedUrl, node.urlFetcher, node.unsafe);
double[] nf = nodeFormat(surface, tree, false);
double treeWidth = nf[0], treeHeight = nf[1];
if (treeWidth == 0)
treeWidth = width;
if (treeHeight == 0)
treeHeight = height;
node.imageWidth = treeWidth;
node.imageHeight = treeHeight;
double[] ratio = preserveRatio(surface, node, width, height);
var savedTransform = surface.context.getTransform();
var savedComposite = surface.context.getComposite();
surface.context.translate(x, y);
surface.context.scale(ratio[0], ratio[1]);
surface.context.translate(ratio[2], ratio[3]);
if (opacity < 1) {
surface.context.setComposite(
java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, (float) opacity));
}
surface.draw(tree);
surface.context.setComposite(savedComposite);
surface.context.setTransform(savedTransform);
} catch (Exception e) { // parseTree may throw RuntimeException or IOException
// Skip invalid SVG images
}
return;
}
// Raster image — use cache to avoid repeated ImageIO.read() for same href
try {
BufferedImage img = surface.rasterImageCache.get(cacheKey);
if (img == null) {
// Use MemoryCacheImageInputStream to avoid temp file I/O
// ImageIO.read() will automatically close the stream after reading from it.
var iis = new MemoryCacheImageInputStream(new ByteArrayInputStream(imageBytes));
img = ImageIO.read(iis);
if (img == null)
return;
surface.rasterImageCache.put(cacheKey, img);
}
node.imageWidth = img.getWidth();
node.imageHeight = img.getHeight();
if (width == 0)
width = node.imageWidth;
if (height == 0)
height = node.imageHeight;
double[] ratio = preserveRatio(surface, node, width, height);
var savedTransform = surface.context.getTransform();
var savedComposite = surface.context.getComposite();
var savedInterpolation = surface.context.getRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION);
surface.context.translate(x, y);
surface.context.scale(ratio[0], ratio[1]);
surface.context.translate(ratio[2], ratio[3]);
if (opacity < 1) {
surface.context.setComposite(
java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, (float) opacity));
}
surface.context.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, imageInterpolation(node));
BufferedImage imageToDraw = img;
int drawX = 0;
int drawY = 0;
if (opacity < 1) {
imageToDraw = new BufferedImage(img.getWidth() + 2, img.getHeight() + 2, BufferedImage.TYPE_INT_ARGB);
var g = imageToDraw.createGraphics();
g.setRenderingHints(surface.context.getRenderingHints());
g.drawImage(img, 1, 1, null);
g.dispose();
drawX = -1;
drawY = -1;
}
surface.context.drawImage(imageToDraw, drawX, drawY, null);
surface.context.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION,
savedInterpolation != null
? savedInterpolation
: java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
surface.context.setComposite(savedComposite);
surface.context.setTransform(savedTransform);
} catch (IOException e) {
// Skip unreadable images
}
}
private static boolean isSvgContent(byte[] data) {
if (data.length < 5)
return false;
String start = new String(data, 0, Math.min(data.length, 256));
return start.contains("<svg") || start.startsWith("<?xml") || start.startsWith("<!DOC")
|| (data[0] == 0x1f && data[1] == (byte) 0x8b); // gzip
}
/** Resolve interpolation hint from the image-rendering CSS property. */
private static Object imageInterpolation(Node node) {
String rendering = node.get("image-rendering");
if (rendering != null) {
return switch (rendering) {
case "pixelated", "crisp-edges", "optimizeSpeed" ->
java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
default -> java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
};
}
return java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
}
}