BmpImageParser.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.imaging.formats.bmp;

import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.imaging.AbstractImageParser;
import org.apache.commons.imaging.FormatCompliance;
import org.apache.commons.imaging.ImageFormat;
import org.apache.commons.imaging.ImageFormats;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.PixelDensity;
import org.apache.commons.imaging.bytesource.ByteSource;
import org.apache.commons.imaging.common.BinaryOutputStream;
import org.apache.commons.imaging.common.ImageBuilder;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.palette.PaletteFactory;
import org.apache.commons.imaging.palette.SimplePalette;

public class BmpImageParser extends AbstractImageParser<BmpImagingParameters> {

    private static final Logger LOGGER = Logger.getLogger(BmpImageParser.class.getName());

    private static final String DEFAULT_EXTENSION = ImageFormats.BMP.getDefaultExtension();
    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.BMP.getExtensions();
    private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, };
    private static final int BI_RGB = 0;
    private static final int BI_RLE4 = 2;
    private static final int BI_RLE8 = 1;
    private static final int BI_BITFIELDS = 3;
    private static final int BITMAP_FILE_HEADER_SIZE = 14;
    private static final int BITMAP_INFO_HEADER_SIZE = 40;

    public BmpImageParser() {
        super(ByteOrder.LITTLE_ENDIAN);
    }

    @Override
    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
        pw.println("bmp.dumpImageFile");

        final ImageInfo imageData = getImageInfo(byteSource, null);

        imageData.toString(pw, "");

        pw.println("");

        return true;
    }

    @Override
    protected String[] getAcceptedExtensions() {
        return ACCEPTED_EXTENSIONS;
    }

    @Override
    protected ImageFormat[] getAcceptedTypes() {
        return new ImageFormat[] { ImageFormats.BMP };
    }

    private String getBmpTypeDescription(final int identifier1, final int identifier2) {
        if (identifier1 == 'B' && identifier2 == 'M') {
            return "Windows 3.1x, 95, NT,";
        }
        if (identifier1 == 'B' && identifier2 == 'A') {
            return "OS/2 Bitmap Array";
        }
        if (identifier1 == 'C' && identifier2 == 'I') {
            return "OS/2 Color Icon";
        }
        if (identifier1 == 'C' && identifier2 == 'P') {
            return "OS/2 Color Pointer";
        }
        if (identifier1 == 'I' && identifier2 == 'C') {
            return "OS/2 Icon";
        }
        if (identifier1 == 'P' && identifier2 == 'T') {
            return "OS/2 Pointer";
        }

        return "Unknown";
    }

    @Override
    public BufferedImage getBufferedImage(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException {
        try (InputStream is = byteSource.getInputStream()) {
            return getBufferedImage(is, params);
        }
    }

    public BufferedImage getBufferedImage(final InputStream inputStream, final BmpImagingParameters params) throws ImagingException, IOException {
        final BmpImageContents ic = readImageContents(inputStream, FormatCompliance.getDefault());

        final BmpHeaderInfo bhi = ic.bhi;
        // byte[] colorTable = ic.colorTable;
        // byte[] imageData = ic.imageData;

        final int width = bhi.width;
        final int height = bhi.height;

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("width: " + width);
            LOGGER.fine("height: " + height);
            LOGGER.fine("width*height: " + width * height);
            LOGGER.fine("width*height*4: " + width * height * 4);
        }

        final AbstractPixelParser abstractPixelParser = ic.abstractPixelParser;
        final ImageBuilder imageBuilder = new ImageBuilder(width, height, true);
        abstractPixelParser.processImage(imageBuilder);

        return imageBuilder.getBufferedImage();

    }

    @Override
    public String getDefaultExtension() {
        return DEFAULT_EXTENSION;
    }

    @Override
    public BmpImagingParameters getDefaultParameters() {
        return new BmpImagingParameters();
    }

    @Override
    public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImagingException, IOException {
        final FormatCompliance result = new FormatCompliance(byteSource.toString());

        try (InputStream is = byteSource.getInputStream()) {
            readImageContents(is, result);
        }

        return result;
    }

    @Override
    public byte[] getIccProfileBytes(final ByteSource byteSource, final BmpImagingParameters params) {
        return null;
    }

    @Override
    public ImageInfo getImageInfo(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException {
        BmpImageContents ic = null;
        try (InputStream is = byteSource.getInputStream()) {
            ic = readImageContents(is, FormatCompliance.getDefault());
        }

        final BmpHeaderInfo bhi = ic.bhi;
        final byte[] colorTable = ic.colorTable;

        if (bhi == null) {
            throw new ImagingException("BMP: couldn't read header");
        }

        final int height = bhi.height;
        final int width = bhi.width;

        final List<String> comments = new ArrayList<>();
        // TODO: comments...

        final int bitsPerPixel = bhi.bitsPerPixel;
        final ImageFormat format = ImageFormats.BMP;
        final String name = "BMP Windows Bitmap";
        final String mimeType = "image/x-ms-bmp";
        // we ought to count images, but don't yet.
        final int numberOfImages = -1;
        // not accurate ... only reflects first
        final boolean progressive = false;
        // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0);
        //
        // pixels per meter
        final int physicalWidthDpi = (int) Math.round(bhi.hResolution * .0254);
        final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
        // int physicalHeightDpi = 72;
        final int physicalHeightDpi = (int) Math.round(bhi.vResolution * .0254);
        final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);

        final String formatDetails = "Bmp (" + (char) bhi.identifier1 + (char) bhi.identifier2 + ": " + getBmpTypeDescription(bhi.identifier1, bhi.identifier2)
                + ")";

        final boolean transparent = false;

        final boolean usesPalette = colorTable != null;
        final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.RLE;

        return new ImageInfo(formatDetails, bitsPerPixel, comments, format, name, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
                physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
    }

    @Override
    public Dimension getImageSize(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException {
        final BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource);

        return new Dimension(bhi.width, bhi.height);

    }

    @Override
    public ImageMetadata getMetadata(final ByteSource byteSource, final BmpImagingParameters params) {
        // TODO this should throw UnsupportedOperationException, but RoundtripTest has to be refactored completely before this can be changed
        return null;
    }

    @Override
    public String getName() {
        return "Bmp-Custom";
    }

    private byte[] getRleBytes(final InputStream is, final int rleSamplesPerByte) throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // this.setDebug(true);

        boolean done = false;
        while (!done) {
            final int a = 0xff & readByte("RLE a", is, "BMP: Bad RLE");
            baos.write(a);
            final int b = 0xff & readByte("RLE b", is, "BMP: Bad RLE");
            baos.write(b);

            if (a == 0) {
                switch (b) {
                case 0: // EOL
                    break;
                case 1: // EOF
                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
                    // );
                    done = true;
                    break;
                case 2: {
                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
                    // );
                    final int c = 0xff & readByte("RLE c", is, "BMP: Bad RLE");
                    baos.write(c);
                    final int d = 0xff & readByte("RLE d", is, "BMP: Bad RLE");
                    baos.write(d);

                }
                    break;
                default: {
                    int size = b / rleSamplesPerByte;
                    if (b % rleSamplesPerByte > 0) {
                        size++;
                    }
                    if (size % 2 != 0) {
                        size++;
                    }

                    // System.out.println("b: " + b);
                    // System.out.println("size: " + size);
                    // System.out.println("RLESamplesPerByte: " +
                    // RLESamplesPerByte);
                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
                    // );
                    final byte[] bytes = readBytes("bytes", is, size, "RLE: Absolute Mode");
                    baos.write(bytes);
                }
                    break;
                }
            }
        }

        return baos.toByteArray();
    }

    private BmpHeaderInfo readBmpHeaderInfo(final ByteSource byteSource) throws ImagingException, IOException {
        try (InputStream is = byteSource.getInputStream()) {
            // readSignature(is);
            return readBmpHeaderInfo(is, null);
        }
    }

    private BmpHeaderInfo readBmpHeaderInfo(final InputStream is, final FormatCompliance formatCompliance) throws ImagingException, IOException {
        final byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File");
        final byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File");

        if (formatCompliance != null) {
            formatCompliance.compareBytes("Signature", BMP_HEADER_SIGNATURE, new byte[] { identifier1, identifier2 });
        }

        final int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File", getByteOrder());
        final int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
        final int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, "Not a Valid BMP File", getByteOrder());

        final int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, "Not a Valid BMP File", getByteOrder());
        int width = 0;
        int height = 0;
        int planes = 0;
        int bitsPerPixel = 0;
        int compression = 0;
        int bitmapDataSize = 0;
        int hResolution = 0;
        int vResolution = 0;
        int colorsUsed = 0;
        int colorsImportant = 0;
        int redMask = 0;
        int greenMask = 0;
        int blueMask = 0;
        int alphaMask = 0;
        int colorSpaceType = 0;
        final BmpHeaderInfo.ColorSpace colorSpace = new BmpHeaderInfo.ColorSpace();
        colorSpace.red = new BmpHeaderInfo.ColorSpaceCoordinate();
        colorSpace.green = new BmpHeaderInfo.ColorSpaceCoordinate();
        colorSpace.blue = new BmpHeaderInfo.ColorSpaceCoordinate();
        int gammaRed = 0;
        int gammaGreen = 0;
        int gammaBlue = 0;
        int intent = 0;
        int profileData = 0;
        int profileSize = 0;
        int reservedV5 = 0;

        if (bitmapHeaderSize < 40) {
            throw new ImagingException("Invalid/unsupported BMP file");
        }
        // BITMAPINFOHEADER
        width = read4Bytes("Width", is, "Not a Valid BMP File", getByteOrder());
        height = read4Bytes("Height", is, "Not a Valid BMP File", getByteOrder());
        planes = read2Bytes("Planes", is, "Not a Valid BMP File", getByteOrder());
        bitsPerPixel = read2Bytes("Bits Per Pixel", is, "Not a Valid BMP File", getByteOrder());
        compression = read4Bytes("Compression", is, "Not a Valid BMP File", getByteOrder());
        bitmapDataSize = read4Bytes("Bitmap Data Size", is, "Not a Valid BMP File", getByteOrder());
        hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File", getByteOrder());
        vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File", getByteOrder());
        colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File", getByteOrder());
        colorsImportant = read4Bytes("ColorsImportant", is, "Not a Valid BMP File", getByteOrder());
        if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
            // 52 = BITMAPV2INFOHEADER, now undocumented
            // see https://en.wikipedia.org/wiki/BMP_file_format
            redMask = read4Bytes("RedMask", is, "Not a Valid BMP File", getByteOrder());
            greenMask = read4Bytes("GreenMask", is, "Not a Valid BMP File", getByteOrder());
            blueMask = read4Bytes("BlueMask", is, "Not a Valid BMP File", getByteOrder());
        }
        if (bitmapHeaderSize >= 56) {
            // 56 = the now undocumented BITMAPV3HEADER sometimes used by
            // Photoshop
            // see [BROKEN URL] http://forums.adobe.com/thread/751592?tstart=1
            alphaMask = read4Bytes("AlphaMask", is, "Not a Valid BMP File", getByteOrder());
        }
        if (bitmapHeaderSize >= 108) {
            // BITMAPV4HEADER
            colorSpaceType = read4Bytes("ColorSpaceType", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.red.x = read4Bytes("ColorSpaceRedX", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.red.y = read4Bytes("ColorSpaceRedY", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.red.z = read4Bytes("ColorSpaceRedZ", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.green.x = read4Bytes("ColorSpaceGreenX", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.green.y = read4Bytes("ColorSpaceGreenY", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.green.z = read4Bytes("ColorSpaceGreenZ", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.blue.x = read4Bytes("ColorSpaceBlueX", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.blue.y = read4Bytes("ColorSpaceBlueY", is, "Not a Valid BMP File", getByteOrder());
            colorSpace.blue.z = read4Bytes("ColorSpaceBlueZ", is, "Not a Valid BMP File", getByteOrder());
            gammaRed = read4Bytes("GammaRed", is, "Not a Valid BMP File", getByteOrder());
            gammaGreen = read4Bytes("GammaGreen", is, "Not a Valid BMP File", getByteOrder());
            gammaBlue = read4Bytes("GammaBlue", is, "Not a Valid BMP File", getByteOrder());
        }
        if (bitmapHeaderSize >= 124) {
            // BITMAPV5HEADER
            intent = read4Bytes("Intent", is, "Not a Valid BMP File", getByteOrder());
            profileData = read4Bytes("ProfileData", is, "Not a Valid BMP File", getByteOrder());
            profileSize = read4Bytes("ProfileSize", is, "Not a Valid BMP File", getByteOrder());
            reservedV5 = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("identifier1", identifier1, 1);
            debugNumber("identifier2", identifier2, 1);
            debugNumber("fileSize", fileSize, 4);
            debugNumber("reserved", reserved, 4);
            debugNumber("bitmapDataOffset", bitmapDataOffset, 4);
            debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4);
            debugNumber("width", width, 4);
            debugNumber("height", height, 4);
            debugNumber("planes", planes, 2);
            debugNumber("bitsPerPixel", bitsPerPixel, 2);
            debugNumber("compression", compression, 4);
            debugNumber("bitmapDataSize", bitmapDataSize, 4);
            debugNumber("hResolution", hResolution, 4);
            debugNumber("vResolution", vResolution, 4);
            debugNumber("colorsUsed", colorsUsed, 4);
            debugNumber("colorsImportant", colorsImportant, 4);
            if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
                debugNumber("redMask", redMask, 4);
                debugNumber("greenMask", greenMask, 4);
                debugNumber("blueMask", blueMask, 4);
            }
            if (bitmapHeaderSize >= 56) {
                debugNumber("alphaMask", alphaMask, 4);
            }
            if (bitmapHeaderSize >= 108) {
                debugNumber("colorSpaceType", colorSpaceType, 4);
                debugNumber("colorSpace.red.x", colorSpace.red.x, 1);
                debugNumber("colorSpace.red.y", colorSpace.red.y, 1);
                debugNumber("colorSpace.red.z", colorSpace.red.z, 1);
                debugNumber("colorSpace.green.x", colorSpace.green.x, 1);
                debugNumber("colorSpace.green.y", colorSpace.green.y, 1);
                debugNumber("colorSpace.green.z", colorSpace.green.z, 1);
                debugNumber("colorSpace.blue.x", colorSpace.blue.x, 1);
                debugNumber("colorSpace.blue.y", colorSpace.blue.y, 1);
                debugNumber("colorSpace.blue.z", colorSpace.blue.z, 1);
                debugNumber("gammaRed", gammaRed, 4);
                debugNumber("gammaGreen", gammaGreen, 4);
                debugNumber("gammaBlue", gammaBlue, 4);
            }
            if (bitmapHeaderSize >= 124) {
                debugNumber("intent", intent, 4);
                debugNumber("profileData", profileData, 4);
                debugNumber("profileSize", profileSize, 4);
                debugNumber("reservedV5", reservedV5, 4);
            }
        }

        return new BmpHeaderInfo(identifier1, identifier2, fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width, height, planes, bitsPerPixel,
                compression, bitmapDataSize, hResolution, vResolution, colorsUsed, colorsImportant, redMask, greenMask, blueMask, alphaMask, colorSpaceType,
                colorSpace, gammaRed, gammaGreen, gammaBlue, intent, profileData, profileSize, reservedV5);
    }

    private BmpImageContents readImageContents(final InputStream is, final FormatCompliance formatCompliance) throws ImagingException, IOException {
        final BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance);

        int colorTableSize = bhi.colorsUsed;
        if (colorTableSize == 0) {
            colorTableSize = 1 << bhi.bitsPerPixel;
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("ColorsUsed", bhi.colorsUsed, 4);
            debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4);
            debugNumber("ColorTableSize", colorTableSize, 4);
            debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4);
            debugNumber("Compression", bhi.compression, 4);
        }

        // A palette is always valid, even for images that don't need it
        // (like 32 bpp), it specifies the "optimal color palette" for
        // when the image is displayed on a <= 256 color graphics card.
        int paletteLength;
        int rleSamplesPerByte = 0;
        boolean rle = false;

        switch (bhi.compression) {
        case BI_RGB:
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Compression: BI_RGB");
            }
            if (bhi.bitsPerPixel <= 8) {
                paletteLength = 4 * colorTableSize;
            } else {
                paletteLength = 0;
            }
            // BytesPerPaletteEntry = 0;
            // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel);
            // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel
            // <= 16));
            break;

        case BI_RLE4:
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Compression: BI_RLE4");
            }
            paletteLength = 4 * colorTableSize;
            rleSamplesPerByte = 2;
            // ExtraBitsPerPixel = 4;
            rle = true;
            // // BytesPerPixel = 2;
            // // BytesPerPaletteEntry = 0;
            break;
        //
        case BI_RLE8:
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Compression: BI_RLE8");
            }
            paletteLength = 4 * colorTableSize;
            rleSamplesPerByte = 1;
            // ExtraBitsPerPixel = 8;
            rle = true;
            // BytesPerPixel = 2;
            // BytesPerPaletteEntry = 0;
            break;
        //
        case BI_BITFIELDS:
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.fine("Compression: BI_BITFIELDS");
            }
            if (bhi.bitsPerPixel <= 8) {
                paletteLength = 4 * colorTableSize;
            } else {
                paletteLength = 0;
            }
            // BytesPerPixel = 2;
            // BytesPerPaletteEntry = 4;
            break;

        default:
            throw new ImagingException("BMP: Unknown Compression: " + bhi.compression);
        }

        if (paletteLength < 0) {
            throw new ImagingException("BMP: Invalid negative palette length: " + paletteLength);
        }

        byte[] colorTable = null;
        if (paletteLength > 0) {
            colorTable = readBytes("ColorTable", is, paletteLength, "Not a Valid BMP File");
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("paletteLength", paletteLength, 4);
            LOGGER.fine("ColorTable: " + (colorTable == null ? "null" : Integer.toString(colorTable.length)));
        }

        int imageLineLength = (bhi.bitsPerPixel * bhi.width + 7) / 8;

        if (LOGGER.isLoggable(Level.FINE)) {
            final int pixelCount = bhi.width * bhi.height;
            // this.debugNumber("Total BitsPerPixel",
            // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4);
            // this.debugNumber("Total Bit Per Line",
            // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4);
            // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4);
            debugNumber("bhi.Width", bhi.width, 4);
            debugNumber("bhi.Height", bhi.height, 4);
            debugNumber("ImageLineLength", imageLineLength, 4);
            // this.debugNumber("imageDataSize", imageDataSize, 4);
            debugNumber("PixelCount", pixelCount, 4);
        }
        // int ImageLineLength = BytesPerPixel * bhi.Width;
        while (imageLineLength % 4 != 0) {
            imageLineLength++;
        }

        final int headerSize = BITMAP_FILE_HEADER_SIZE + bhi.bitmapHeaderSize + (bhi.bitmapHeaderSize == 40 && bhi.compression == BI_BITFIELDS ? 3 * 4 : 0);
        final int expectedDataOffset = headerSize + paletteLength;

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4);
            debugNumber("expectedDataOffset", expectedDataOffset, 4);
        }
        final int extraBytes = bhi.bitmapDataOffset - expectedDataOffset;
        if (extraBytes < 0 || extraBytes > bhi.fileSize) {
            throw new ImagingException("BMP has invalid image data offset: " + bhi.bitmapDataOffset + " (expected: " + expectedDataOffset + ", paletteLength: "
                    + paletteLength + ", headerSize: " + headerSize + ")");
        }
        if (extraBytes > 0) {
            readBytes("BitmapDataOffset", is, extraBytes, "Not a Valid BMP File");
        }

        final int imageDataSize = bhi.height * imageLineLength;

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("imageDataSize", imageDataSize, 4);
        }

        byte[] imageData;
        if (rle) {
            imageData = getRleBytes(is, rleSamplesPerByte);
        } else {
            imageData = readBytes("ImageData", is, imageDataSize, "Not a Valid BMP File");
        }

        if (LOGGER.isLoggable(Level.FINE)) {
            debugNumber("ImageData.length", imageData.length, 4);
        }

        AbstractPixelParser abstractPixelParser;

        switch (bhi.compression) {
        case BI_RLE4:
        case BI_RLE8:
            abstractPixelParser = new PixelParserRle(bhi, colorTable, imageData);
            break;
        case BI_RGB:
            abstractPixelParser = new PixelParserRgb(bhi, colorTable, imageData);
            break;
        case BI_BITFIELDS:
            abstractPixelParser = new PixelParserBitFields(bhi, colorTable, imageData);
            break;
        default:
            throw new ImagingException("BMP: Unknown Compression: " + bhi.compression);
        }

        return new BmpImageContents(bhi, colorTable, imageData, abstractPixelParser);
    }

    @Override
    public void writeImage(final BufferedImage src, final OutputStream os, BmpImagingParameters params) throws ImagingException, IOException {
        if (params == null) {
            params = new BmpImagingParameters();
        }
        final PixelDensity pixelDensity = params.getPixelDensity();

        final SimplePalette palette = new PaletteFactory().makeExactRgbPaletteSimple(src, 256);

        BmpWriter writer;
        if (palette == null) {
            writer = new BmpWriterRgb();
        } else {
            writer = new BmpWriterPalette(palette);
        }

        final byte[] imageData = writer.getImageData(src);
        final BinaryOutputStream bos = BinaryOutputStream.littleEndian(os);

        // write BitmapFileHeader
        os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap
        os.write(0x4d); // M

        final int fileSize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header size
                4 * writer.getPaletteSize() + // palette size in bytes
                imageData.length;
        bos.write4Bytes(fileSize);

        bos.write4Bytes(0); // reserved
        bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + 4 * writer.getPaletteSize()); // Bitmap Data Offset

        final int width = src.getWidth();
        final int height = src.getHeight();

        // write BitmapInfoHeader
        bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size
        bos.write4Bytes(width); // width
        bos.write4Bytes(height); // height
        bos.write2Bytes(1); // Number of Planes
        bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel

        bos.write4Bytes(BI_RGB); // Compression
        bos.write4Bytes(imageData.length); // Bitmap Data Size
        bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.horizontalDensityMetres()) : 0); // HResolution
        bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.verticalDensityMetres()) : 0); // VResolution
        if (palette == null) {
            bos.write4Bytes(0); // Colors
        } else {
            bos.write4Bytes(palette.length()); // Colors
        }
        bos.write4Bytes(0); // Important Colors
        // bos.write_4_bytes(0); // Compression

        // write Palette
        writer.writePalette(bos);
        // write Image Data
        bos.write(imageData);
    }
}