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;
    }
}