PngExporter.java

package org.fresnel.optics;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * PNG export with embedded physical-pixel metadata (pHYs chunk) so prints come
 * out at the correct DPI.
 */
public final class PngExporter {

    private PngExporter() {}

    /** Write the rendered zone plate as PNG to the given stream, embedding DPI metadata. */
    public static void writePng(RenderResult result, double dpi, OutputStream out) throws IOException {
        ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
        try (ImageOutputStream ios = ImageIO.createImageOutputStream(out)) {
            writer.setOutput(ios);
            ImageWriteParam param = writer.getDefaultWriteParam();
            BufferedImage img = result.image();
            IIOMetadata metadata = writer.getDefaultImageMetadata(
                    new ImageTypeSpecifier(img), param);
            addDpiMetadata(metadata, dpi);
            writer.write(null, new IIOImage(img, null, metadata), param);
        } finally {
            writer.dispose();
        }
    }

    /** Convenience: render to PNG byte array. */
    public static byte[] toPngBytes(RenderResult result, double dpi) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        writePng(result, dpi, baos);
        return baos.toByteArray();
    }

    private static void addDpiMetadata(IIOMetadata metadata, double dpi) throws IOException {
        // pixels per millimeter
        double dotsPerMm = dpi / Units.INCH_MM;
        // PNG pHYs uses pixels-per-meter when unit specifier is 1 (meters)
        long pixelsPerMeter = Math.round(dotsPerMm * 1000.0);

        String formatName = "javax_imageio_png_1.0";
        IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(formatName);
        IIOMetadataNode phys = new IIOMetadataNode("pHYs");
        phys.setAttribute("pixelsPerUnitXAxis", Long.toString(pixelsPerMeter));
        phys.setAttribute("pixelsPerUnitYAxis", Long.toString(pixelsPerMeter));
        phys.setAttribute("unitSpecifier", "meter");
        root.appendChild(phys);
        metadata.mergeTree(formatName, root);
    }
}