EmfImageReader.java

package pro.verron.officestamper.imageio.emf;

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.io.IOException;
import java.nio.ByteOrder;
import java.util.Collections;
import java.util.Iterator;

/// Minimal ImageIO reader for EMF (Enhanced Metafile) that exposes only image metadata
/// (width, height, bounds). No rasterization is performed and [#read(int, ImageReadParam)]
/// throws [UnsupportedOperationException].
///
/// Implementation notes:
///
///   - Only the EMF `EMR_HEADER` record is parsed from the stream. Parsing stops immediately
///     after the header; no additional records are read.
///   - Bounds selection: prefer `rclBounds` (device units/pixels). If not available or zero-sized,
///     compute from `rclFrame` (0.01 millimeters) using device DPI estimated from
///     `szlDevice` (pixels) and `szlMillimeters` (millimeters). If unavailable, fall back to 96 DPI.
///   - Stream safety: the reader records and restores the original stream position when probing/parsing.
public final class EmfImageReader
        extends ImageReader {

    private static final int EMR_HEADER = 1; // Record type for EMR_HEADER
    private static final int EMF_SIGNATURE = 0x464D4520; // ' EMF' in little-endian

    private Dimension cachedSize; // lazily computed

    EmfImageReader(EmfImageReaderSpi spi) {
        super(spi);
    }

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

    @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("EMF reader supports a single image (index 0)");
        ensureInputSet();
    }

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

            // --- EMR header ---
            int type = iis.readInt();
            int nSize = iis.readInt(); // total size of EMR_HEADER in bytes
            if (type != EMR_HEADER || nSize < 88) { // 88 bytes up to szlMillimeters in the base header
                throw new IIOException("Not an EMF header (type=" + type + ", size=" + nSize + ")");
            }

            // rclBounds (pixels)
            int boundsLeft = iis.readInt();
            int boundsTop = iis.readInt();
            int boundsRight = iis.readInt();
            int boundsBottom = iis.readInt();

            // rclFrame (0.01 millimeters)
            int frameLeft = iis.readInt();
            int frameTop = iis.readInt();
            int frameRight = iis.readInt();
            int frameBottom = iis.readInt();

            int signature = iis.readInt();
            if (signature != EMF_SIGNATURE) {
                throw new IIOException("Invalid EMF signature");
            }

            // Skip: nVersion, nBytes, nRecords, nHandles, sReserved
            /* nVersion */
            iis.readInt();
            /* nBytes   */
            iis.readInt();
            /* nRecords */
            iis.readInt();
            /* nHandles */
            iis.readUnsignedShort();
            /* sReserved*/
            iis.readUnsignedShort();

            // Description count + offset
            /* nDescription  */
            iis.readInt();
            /* offDescription */
            iis.readInt();

            // Palette entries
            /* nPalEntries */
            iis.readInt();

            // szlDevice (pixels) and szlMillimeters (millimeters)
            int deviceCx = iis.readInt();
            int deviceCy = iis.readInt();
            int mmCx = iis.readInt();
            int mmCy = iis.readInt();

            // Restore order ASAP
            iis.setByteOrder(oldOrder);

            // Compute size
            int width = Math.max(0, boundsRight - boundsLeft);
            int height = Math.max(0, boundsBottom - boundsTop);

            if (width == 0 || height == 0) {
                // Convert from frame (.01 mm) to pixels
                double frameWmm = (frameRight - frameLeft) / 100.0; // .01 mm -> mm
                double frameHmm = (frameBottom - frameTop) / 100.0;

                double dpiX = estimateDpi(deviceCx, mmCx);
                double dpiY = estimateDpi(deviceCy, mmCy);

                width = (int) Math.round((frameWmm / 25.4) * dpiX);
                height = (int) Math.round((frameHmm / 25.4) * dpiY);
            }

            if (width <= 0 || height <= 0) {
                throw new IIOException("Could not determine EMF image dimensions");
            }

            cachedSize = new Dimension(width, height);
        } catch (IOException e) {
            throw new IIOException("Failed to read EMF header", e);
        }
        return cachedSize;
    }

    private static double estimateDpi(int devicePixels, int deviceMillimeters) {
        if (devicePixels > 0 && deviceMillimeters > 0) {
            return devicePixels / (deviceMillimeters / 25.4);
        }
        // Assumption when no device info is available
        return 96.0; // common default DPI
    }

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

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex)
            throws IIOException {
        // No rasterization support – return an empty iterator.
        checkImageIndex(imageIndex);
        return Collections.emptyIterator();
    }

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

    @Override
    public IIOMetadata getStreamMetadata()
            throws IIOException {
        // Minimal implementation – no stream metadata yet.
        // TODO: Provide basic metadata with bounds and EMF version if needed.
        return null;
    }

    @Override
    public IIOMetadata getImageMetadata(int imageIndex)
            throws IIOException {
        // Minimal implementation – no per-image metadata yet.
        // TODO: Return a minimal IIOMetadata with width/height/bounds if consumers expect it.
        checkImageIndex(imageIndex);
        return null;
    }

    @Override
    public java.awt.image.BufferedImage read(int imageIndex, ImageReadParam param)
            throws IIOException {
        throw new UnsupportedOperationException("EMF rasterization is not supported by this reader");
    }

    @Override
    public boolean canReadRaster() {
        return false;
    }
}