WmfImageReader.java

package pro.verron.officestamper.imageio.wmf;

import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Collections;
import java.util.Iterator;

/// Minimal ImageIO reader for Placeable WMF that exposes only image dimensions.
///
/// Implementation parses the 22-byte Placeable WMF header to get the bounding box in metafile units
/// and the `Inch` units-per-inch factor. Dimensions are converted to pixels using an estimated
/// DPI (96 by default). Rasterization is not supported and [#read(int, ImageReadParam)] throws.
public final class WmfImageReader
        extends ImageReader {

    private static final int PLACEABLE_KEY = 0x9AC6CDD7; // little-endian in file: D7 CD C6 9A

    private Dimension cachedSize;

    WmfImageReader(WmfImageReaderSpi spi) {
        super(spi);
    }

    @Override
    public String getFormatName() {
        return "wmf";
    }

    @Override
    public int getNumImages(boolean allowSearch)
            throws IIOException {
        ensureInputSet();
        return 1;
    }

    private void ensureInputSet()
            throws IIOException {
        if (!(getInput() instanceof ImageInputStream)) {
            throw new IIOException("Input must be an ImageInputStream");
        }
    }

    @Override
    public int getWidth(int imageIndex)
            throws IIOException {
        checkImageIndex(imageIndex);
        return getOrParseSize().width;
    }

    private void checkImageIndex(int imageIndex)
            throws IIOException {
        if (imageIndex != 0) throw new IIOException("WMF reader supports a single image (index 0)");
        ensureInputSet();
    }

    private Dimension getOrParseSize()
            throws IIOException {
        if (cachedSize != null) return cachedSize;
        var iis = (ImageInputStream) getInput();
        long pos = 0L;
        try {
            pos = iis.getStreamPosition();
            var oldOrder = iis.getByteOrder();
            iis.setByteOrder(ByteOrder.LITTLE_ENDIAN);

            // Placeable WMF header (22 bytes)
            int key = iis.readInt();
            if (key != PLACEABLE_KEY) {
                throw new IIOException("Not a Placeable WMF (missing magic key)");
            }
            /* hmf */
            iis.readUnsignedShort();

            // Bounding box in metafile units (signed 16-bit shorts)
            short left = iis.readShort();
            short top = iis.readShort();
            short right = iis.readShort();
            short bottom = iis.readShort();

            int inch = iis.readUnsignedShort(); // units per inch
            /* reserved */
            iis.readInt();
            /* checksum */
            iis.readUnsignedShort();

            // Restore order ASAP
            iis.setByteOrder(oldOrder);
            iis.seek(pos); // rewind for other readers if needed

            int unitsW = (right - left);
            int unitsH = (bottom - top);
            if (unitsW <= 0 || unitsH <= 0) {
                throw new IIOException("Invalid WMF bounding box");
            }

            double dpiX = 96.0;
            double dpiY = 96.0;
            double unitsPerInch = (inch > 0) ? inch : 1440.0; // common default if missing

            int width = (int) Math.round((unitsW / unitsPerInch) * dpiX);
            int height = (int) Math.round((unitsH / unitsPerInch) * dpiY);

            if (width <= 0 || height <= 0) {
                throw new IIOException("Could not determine WMF image dimensions");
            }
            cachedSize = new Dimension(width, height);
        } catch (IOException e) {
            try {iis.seek(pos);} catch (IOException ignore) {}
            throw new IIOException("Failed to read WMF header", e);
        }
        return cachedSize;
    }

    @Override
    public int getHeight(int imageIndex)
            throws IIOException {
        checkImageIndex(imageIndex);
        return getOrParseSize().height;
    }

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex)
            throws IIOException {
        checkImageIndex(imageIndex);
        return Collections.emptyIterator();
    }

    @Override
    public ImageReadParam getDefaultReadParam() {
        return new ImageReadParam();
    }

    @Override
    public IIOMetadata getStreamMetadata() {
        return null;
    }

    @Override
    public IIOMetadata getImageMetadata(int imageIndex)
            throws IIOException {
        checkImageIndex(imageIndex);
        return null;
    }

    @Override
    public BufferedImage read(int imageIndex, ImageReadParam param) {
        throw new UnsupportedOperationException("WMF rasterization is not supported by this reader");
    }
}