DIBHeader.java

/*
 * Copyright (c) 2009, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.imageio.plugins.bmp;

import javax.imageio.IIOException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * Represents the DIB (Device Independent Bitmap) Information header structure.
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: DIBHeader.java,v 1.0 May 5, 2009 10:45:31 AM haraldk Exp$
 * @see <a href="http://en.wikipedia.org/wiki/BMP_file_format">BMP file format (Wikipedia)</a>
 */
abstract class DIBHeader {
    // Roughly 72 DPI
    private final int DEFAULT_PIXELS_PER_METER = 2835;

    protected int size;
    protected int width;
    // NOTE: If a bitmask is present, this value includes the height of the mask
    // (so often header.height = entry.height * 2)
    protected int height;
    protected boolean topDown = false;

    protected int planes;
    protected int bitCount;

    /**
     * 0 = BI_RGB: No compression
     * 1 = BI_RLE8: 8 bit RLE Compression (8 bit only)
     * 2 = BI_RLE4: 4 bit RLE Compression (4 bit only)
     * 3 = BI_BITFIELDS: No compression (16 & 32 bit only)
     */
    protected int compression;

    // May be 0 if not known
    protected int imageSize;

    protected int xPixelsPerMeter;
    protected int yPixelsPerMeter;

    protected int colorsUsed;

    // 0 means all colors are important
    protected int colorsImportant;

    // V4+ members below
    protected int[] masks;
    protected int colorSpaceType;
    protected double[] cieXYZEndpoints;
    protected int[] gamma;

    // V5+ members below
    protected int intent;
    protected long profileData;
    protected long profileSize;

    protected DIBHeader() {
    }

    public static DIBHeader read(final DataInput stream) throws IOException {
        int size = stream.readInt();

        DIBHeader header = createHeader(size);
        header.read(size, stream);

        return header;
    }

    private static DIBHeader createHeader(final int size) throws IOException {
        switch (size) {
            case DIB.BITMAP_CORE_HEADER_SIZE:
                return new BitmapCoreHeader();
            case DIB.OS2_V2_HEADER_16_SIZE:
            case DIB.OS2_V2_HEADER_SIZE:
                return new BitmapCoreHeaderV2();
            case DIB.BITMAP_INFO_HEADER_SIZE:
            case DIB.BITMAP_V2_INFO_HEADER_SIZE:
            case DIB.BITMAP_V3_INFO_HEADER_SIZE:
                // ICO and CUR always uses the Microsoft Windows 3.0 DIB header, which is 40 bytes.
                // This is also the most common format for persistent BMPs.
                return new BitmapInfoHeader();
            case DIB.BITMAP_V4_INFO_HEADER_SIZE:
                return new BitmapV4InfoHeader();
            case DIB.BITMAP_V5_INFO_HEADER_SIZE:
                return new BitmapV5InfoHeader();
            default:
                throw new IIOException(String.format("Unknown Bitmap Information Header (size: %s)", size));
        }
    }

    protected abstract void read(int size, DataInput stream) throws IOException;
    protected abstract void write(final DataOutput stream) throws IOException;

    public final int getSize() {
        return size;
    }

    public final int getWidth() {
        return width;
    }

    public final int getHeight() {
        return height;
    }

    public final int getPlanes() {
        return planes;
    }

    public final int getBitCount() {
        return bitCount;
    }

    public int getCompression() {
        return compression;
    }

    public int getImageSize() {
        return imageSize != 0 ? imageSize : ((bitCount * width + 31) / 32) * 4 * height;
    }

    public int getXPixelsPerMeter() {
        return xPixelsPerMeter != 0 ? xPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
    }

    public int getYPixelsPerMeter() {
        return yPixelsPerMeter != 0 ? yPixelsPerMeter : DEFAULT_PIXELS_PER_METER;
    }

    public int getColorsUsed() {
        return colorsUsed != 0 ? colorsUsed : 1 << Math.min(24, bitCount);
    }

    public int getColorsImportant() {
        return colorsImportant != 0 ? colorsImportant : getColorsUsed();
    }

    public boolean hasMasks() {
        return masks != null || compression == DIB.COMPRESSION_BITFIELDS || compression == DIB.COMPRESSION_ALPHA_BITFIELDS;
    }

    public String toString() {
        return String.format(
                "%s: size: %d bytes, " +
                        "width: %d, height: %d, planes: %d, bit count: %d, compression: %d, " +
                        "image size: %d%s, " +
                        "X pixels per m: %d, Y pixels per m: %d, " +
                        "colors used: %d%s, colors important: %d%s",
                getClass().getSimpleName(),
                getSize(), getWidth(), getHeight(), getPlanes(), getBitCount(), getCompression(),
                getImageSize(), (imageSize == 0 ? " (calculated)" : ""),
                getXPixelsPerMeter(),
                getYPixelsPerMeter(),
                getColorsUsed(), (colorsUsed == 0 ? " (unknown)" : ""),
                getColorsImportant(), (colorsImportant == 0 ? " (all)" : "")
        );
    }

    private static int[] readMasks(final DataInput stream, final boolean hasAlphaMask) throws IOException {
        int maskCount = hasAlphaMask ? 4 : 3;
        int[] masks = new int[4];

        for (int i = 0; i < maskCount; i++) {
            masks[i] = stream.readInt();
        }

        return masks;
    }

    protected abstract String getBMPVersion();

    // TODO: Get rid of code duplication below...

    static final class BitmapCoreHeader extends DIBHeader {
        @Override
        protected void read(final int size, final DataInput stream) throws IOException {
            if (size != DIB.BITMAP_CORE_HEADER_SIZE) {
                throw new IIOException(String.format("Size: %s !=: %s", size, DIB.BITMAP_CORE_HEADER_SIZE));
            }

            this.size = size;

            // NOTE: Unlike all other headers, width and height are unsigned SHORT values (16 bit)!
            width = stream.readUnsignedShort();
            height = stream.readShort();

            if (height < 0) {
                height = -height;
                topDown = true;
            }

            planes = stream.readUnsignedShort();
            bitCount = stream.readUnsignedShort();
        }

        @Override
        protected void write(DataOutput stream) {
            throw new UnsupportedOperationException();
        }

        public String getBMPVersion() {
            return "BMP v. 2.x";
        }
    }

    /**
     * OS/2 BitmapCoreHeader Version 2.
     * <p>
     * NOTE: According to the docs this header is <em>variable size</em>.
     * However, it seems that the size is either 16, 40 or 64, which is covered
     * (40 is the size of the normal {@link BitmapInfoHeader}, and has the same layout).
     * </p>
     *
     * @see <a href="http://www.fileformat.info/format/os2bmp/egff.htm">OS/2 Bitmap File Format Summary</a>
     */
    static final class BitmapCoreHeaderV2 extends DIBHeader {
        @SuppressWarnings("unused")
        @Override
        protected void read(final int size, final DataInput stream) throws IOException {
            if (size != DIB.OS2_V2_HEADER_SIZE && size != DIB.OS2_V2_HEADER_16_SIZE) {
                throw new IIOException(String.format("Size: %s !=: %s", size, DIB.OS2_V2_HEADER_SIZE));
            }

            this.size = size;

            width = stream.readInt();
            height = stream.readInt();

            if (height < 0) {
                height = -height;
                topDown = true;
            }

            planes = stream.readUnsignedShort();
            bitCount = stream.readUnsignedShort();

            if (size != DIB.OS2_V2_HEADER_16_SIZE) {
                compression = stream.readInt();

                imageSize = stream.readInt();

                xPixelsPerMeter = stream.readInt();
                yPixelsPerMeter = stream.readInt();

                colorsUsed = stream.readInt();
                colorsImportant = stream.readInt();
            }

            // TODO: Use? These fields are not reflected in metadata as per now...
            int units = stream.readShort();
            int reserved = stream.readShort();
            int recording = stream.readShort(); // Recording algorithm
            int rendering = stream.readShort(); // Halftoning algorithm
            int size1 = stream.readInt(); // Reserved for halftoning use
            int size2 = stream.readInt(); // Reserved for halftoning use
            int colorEncoding = stream.readInt(); // Color model used in bitmap
            int identifier = stream.readInt(); // Reserved for application use
        }

        @Override
        protected void write(DataOutput stream) {
            throw new UnsupportedOperationException();
        }

        public String getBMPVersion() {
            return "BMP v. 2.2";
        }
    }

    /**
     * Represents the DIB (Device Independent Bitmap) Windows 3.0 Bitmap Information header structure.
     * This is the common format for persistent DIB structures, even if Windows
     * may use the later versions at run-time.
     * <p>
     * Note: Some variations that includes the mask fields into the header size exists,
     * but is no longer part of the documented spec.
     * </p>
     *
     * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
     * @version $Id: DIBHeader.java,v 1.0 25.feb.2006 00:29:44 haku Exp$
     * @see <a href="http://en.wikipedia.org/wiki/BMP_file_format">BMP file format (Wikipedia)</a>
     * @see <a href="https://forums.adobe.com/message/3272950#3272950">BITMAPV3INFOHEADER</a>.
     */
    static final class BitmapInfoHeader extends DIBHeader {
        @Override
        protected void read(final int size, final DataInput stream) throws IOException {
            if (!(size == DIB.BITMAP_INFO_HEADER_SIZE || size == DIB.BITMAP_V2_INFO_HEADER_SIZE || size == DIB.BITMAP_V3_INFO_HEADER_SIZE)) {
                throw new IIOException(String.format("Size: %s !=: %s", size, DIB.BITMAP_INFO_HEADER_SIZE));
            }

            this.size = size;

            width = stream.readInt();
            height = stream.readInt();

            if (height < 0) {
                height = -height;
                topDown = true;
            }

            planes = stream.readUnsignedShort();
            bitCount = stream.readUnsignedShort();
            compression = stream.readInt();

            imageSize = stream.readInt();

            xPixelsPerMeter = stream.readInt();
            yPixelsPerMeter = stream.readInt();

            colorsUsed = stream.readInt();
            colorsImportant = stream.readInt();

            // Read masks if we have V2 or V3
            // or if we have compression BITFIELDS or ALPHA_BITFIELDS
            if (this.size == DIB.BITMAP_V2_INFO_HEADER_SIZE || compression == DIB.COMPRESSION_BITFIELDS) {
                masks = readMasks(stream, false);
            }
            else if (this.size == DIB.BITMAP_V3_INFO_HEADER_SIZE || compression == DIB.COMPRESSION_ALPHA_BITFIELDS) {
                masks = readMasks(stream, true);
            }
        }

        @Override
        protected void write(final DataOutput stream) throws IOException {
            stream.writeInt(DIB.BITMAP_INFO_HEADER_SIZE);

            stream.writeInt(width);
            stream.writeInt(topDown ? -height : height);

            stream.writeShort(planes);
            stream.writeShort(bitCount);
            stream.writeInt(compression);

            stream.writeInt(imageSize);

            stream.writeInt(xPixelsPerMeter);
            stream.writeInt(yPixelsPerMeter);

            stream.writeInt(colorsUsed);
            stream.writeInt(colorsImportant);

            // TODO: Write masks, if COMPRESSION_BITFIELDS/COMPRESSION_ALPHA_BITFIELDS
        }

        public String getBMPVersion() {
            // This is to be compatible with the native metadata of the original com.sun....BMPMetadata
            return size > DIB.BITMAP_INFO_HEADER_SIZE
                   ? "BMP V2/V3 INFO"
                   : compression == DIB.COMPRESSION_BITFIELDS || compression == DIB.COMPRESSION_ALPHA_BITFIELDS
                     ? "BMP v. 3.x NT"
                     : "BMP v. 3.x";
        }
    }

    /**
     * Represents the BITMAPV4INFOHEADER structure.
     */
    static final class BitmapV4InfoHeader extends DIBHeader {
        @Override
        protected void read(final int size, final DataInput stream) throws IOException {
            if (size != DIB.BITMAP_V4_INFO_HEADER_SIZE) {
                throw new IIOException(String.format("Size: %s !=: %s", size, DIB.BITMAP_V4_INFO_HEADER_SIZE));
            }

            this.size = size;

            width = stream.readInt();
            height = stream.readInt();

            if (height < 0) {
                height = -height;
                topDown = true;
            }

            planes = stream.readUnsignedShort();
            bitCount = stream.readUnsignedShort();
            compression = stream.readInt();

            imageSize = stream.readInt();

            xPixelsPerMeter = stream.readInt();
            yPixelsPerMeter = stream.readInt();

            colorsUsed = stream.readInt();
            colorsImportant = stream.readInt();

            masks = readMasks(stream, true);

            colorSpaceType = stream.readInt(); // Should be 0 for V4
            cieXYZEndpoints = new double[9];

            for (int i = 0; i < cieXYZEndpoints.length; i++) {
                cieXYZEndpoints[i] = stream.readInt(); // TODO: Hmmm...?
            }

            gamma = new int[3];

            for (int i = 0; i < gamma.length; i++) {
                gamma[i] = stream.readInt();
            }
        }

        public String getBMPVersion() {
            return "BMP v. 4.x";
        }

        @Override
        protected void write(DataOutput stream) throws IOException {
            stream.writeInt(DIB.BITMAP_V4_INFO_HEADER_SIZE);

            stream.writeInt(width);
            stream.writeInt(topDown ? -height : height);

            stream.writeShort(planes);
            stream.writeShort(bitCount);
            stream.writeInt(compression);

            stream.writeInt(imageSize);

            stream.writeInt(xPixelsPerMeter);
            stream.writeInt(yPixelsPerMeter);

            stream.writeInt(colorsUsed);
            stream.writeInt(colorsImportant);

            // Red, Green, Blue, Alpha masks
            stream.writeInt(masks[0]);
            stream.writeInt(masks[1]);
            stream.writeInt(masks[2]);
            stream.writeInt(masks[3]);

            // color space ("sRGB" LITTLE endian)
            stream.writeInt(DIB.LCS_sRGB);

            // 36 bytes CIE XYZ triples, unused for sRGB
            stream.writeInt(0);
            stream.writeInt(0);
            stream.writeInt(0);

            stream.writeInt(0);
            stream.writeInt(0);
            stream.writeInt(0);

            stream.writeInt(0);
            stream.writeInt(0);
            stream.writeInt(0);

            // Red gamma, unused for sRGB
            // Green gamma, unused for sRGB
            // Blue gamma, unused for sRGB
            stream.writeInt(0);
            stream.writeInt(0);
            stream.writeInt(0);
        }
    }

    /**
     * Represents the BITMAPV5INFOHEADER structure.
     */
    static final class BitmapV5InfoHeader extends DIBHeader {
        protected void read(final int size, final DataInput stream) throws IOException {
            if (size != DIB.BITMAP_V5_INFO_HEADER_SIZE) {
                throw new IIOException(String.format("Size: %s !=: %s", size, DIB.BITMAP_V5_INFO_HEADER_SIZE));
            }

            this.size = size;

            width = stream.readInt();
            height = stream.readInt();

            if (height < 0) {
                height = -height;
                topDown = true;
            }

            planes = stream.readUnsignedShort();
            bitCount = stream.readUnsignedShort();
            compression = stream.readInt();

            imageSize = stream.readInt();

            xPixelsPerMeter = stream.readInt();
            yPixelsPerMeter = stream.readInt();

            colorsUsed = stream.readInt();
            colorsImportant = stream.readInt();

            masks = readMasks(stream, true);

            colorSpaceType = stream.readInt();

            cieXYZEndpoints = new double[9];

            for (int i = 0; i < cieXYZEndpoints.length; i++) {
                cieXYZEndpoints[i] = stream.readInt(); // TODO: Hmmm...?
            }

            gamma = new int[3];

            for (int i = 0; i < gamma.length; i++) {
                gamma[i] = stream.readInt();
            }

            intent = stream.readInt(); // TODO: Verify if this is same as ICC intent
            profileData = stream.readInt() & 0xffffffffL;
            profileSize = stream.readInt() & 0xffffffffL;
            stream.readInt(); // Reserved
        }

        @Override
        protected void write(DataOutput stream) {
            throw new UnsupportedOperationException();
        }

        public String getBMPVersion() {
            return "BMP v. 5.x";
        }
    }
}