    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.imaging.formats.bmp;
    018
    019import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
    020import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
    021import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
    022import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
    023
    024import java.awt.Dimension;
    025import java.awt.image.BufferedImage;
    026import java.io.ByteArrayOutputStream;
    027import java.io.IOException;
    028import java.io.InputStream;
    029import java.io.OutputStream;
    030import java.io.PrintWriter;
    031import java.nio.ByteOrder;
    032import java.util.ArrayList;
    033import java.util.List;
    034import java.util.logging.Level;
    035import java.util.logging.Logger;
    036
    037import org.apache.commons.imaging.FormatCompliance;
    038import org.apache.commons.imaging.ImageFormat;
    039import org.apache.commons.imaging.ImageFormats;
    040import org.apache.commons.imaging.ImageInfo;
    041import org.apache.commons.imaging.ImageParser;
    042import org.apache.commons.imaging.ImageReadException;
    043import org.apache.commons.imaging.ImageWriteException;
    044import org.apache.commons.imaging.PixelDensity;
    045import org.apache.commons.imaging.common.BinaryOutputStream;
    046import org.apache.commons.imaging.common.ImageBuilder;
    047import org.apache.commons.imaging.common.ImageMetadata;
    048import org.apache.commons.imaging.common.bytesource.ByteSource;
    049import org.apache.commons.imaging.palette.PaletteFactory;
    050import org.apache.commons.imaging.palette.SimplePalette;
    051
    052public class BmpImageParser extends ImageParser<BmpImagingParameters> {
    053
    054    private static final Logger LOGGER = Logger.getLogger(BmpImageParser.class.getName());
    055
    056    private static final String DEFAULT_EXTENSION = ImageFormats.BMP.getDefaultExtension();
    057    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.BMP.getExtensions();
    058    private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, };
    059    private static final int BI_RGB = 0;
    060    private static final int BI_RLE4 = 2;
    061    private static final int BI_RLE8 = 1;
    062    private static final int BI_BITFIELDS = 3;
    063    private static final int BITMAP_FILE_HEADER_SIZE = 14;
    064    private static final int BITMAP_INFO_HEADER_SIZE = 40;
    065
    066    public BmpImageParser() {
    067        super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
    068    }
    069
    070    @Override
    071    public BmpImagingParameters getDefaultParameters() {
    072        return new BmpImagingParameters();
    073    }
    074
    075    @Override
    076    public String getName() {
    077        return "Bmp-Custom";
    078    }
    079
    080    @Override
    081    public String getDefaultExtension() {
    082        return DEFAULT_EXTENSION;
    083    }
    084
    085    @Override
    086    protected String[] getAcceptedExtensions() {
    087        return ACCEPTED_EXTENSIONS;
    088    }
    089
    090    @Override
    091    protected ImageFormat[] getAcceptedTypes() {
    092        return new ImageFormat[] { ImageFormats.BMP, //
    093        };
    094    }
    095
    096    private BmpHeaderInfo readBmpHeaderInfo(final InputStream is,
    097            final FormatCompliance formatCompliance)
    098            throws ImageReadException, IOException {
    099        final byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File");
    100        final byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File");
    101
    102        if (formatCompliance != null) {
    103            formatCompliance.compareBytes("Signature", BMP_HEADER_SIGNATURE,
    104                    new byte[]{identifier1, identifier2,});
    105        }
    106
    107        final int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File", getByteOrder());
    108        final int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
    109        final int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, "Not a Valid BMP File", getByteOrder());
    110
    111        final int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, "Not a Valid BMP File", getByteOrder());
    112        int width = 0;
    113        int height = 0;
    114        int planes = 0;
    115        int bitsPerPixel = 0;
    116        int compression = 0;
    117        int bitmapDataSize = 0;
    118        int hResolution = 0;
    119        int vResolution = 0;
    120        int colorsUsed = 0;
    121        int colorsImportant = 0;
    122        int redMask = 0;
    123        int greenMask = 0;
    124        int blueMask = 0;
    125        int alphaMask = 0;
    126        int colorSpaceType = 0;
    127        final BmpHeaderInfo.ColorSpace colorSpace = new BmpHeaderInfo.ColorSpace();
    128        colorSpace.red = new BmpHeaderInfo.ColorSpaceCoordinate();
    129        colorSpace.green = new BmpHeaderInfo.ColorSpaceCoordinate();
    130        colorSpace.blue = new BmpHeaderInfo.ColorSpaceCoordinate();
    131        int gammaRed = 0;
    132        int gammaGreen = 0;
    133        int gammaBlue = 0;
    134        int intent = 0;
    135        int profileData = 0;
    136        int profileSize = 0;
    137        int reservedV5 = 0;
    138
    139        if (bitmapHeaderSize < 40) {
    140            throw new ImageReadException("Invalid/unsupported BMP file");
    141        }
    142        // BITMAPINFOHEADER
    143        width = read4Bytes("Width", is, "Not a Valid BMP File", getByteOrder());
    144        height = read4Bytes("Height", is, "Not a Valid BMP File", getByteOrder());
    145        planes = read2Bytes("Planes", is, "Not a Valid BMP File", getByteOrder());
    146        bitsPerPixel = read2Bytes("Bits Per Pixel", is, "Not a Valid BMP File", getByteOrder());
    147        compression = read4Bytes("Compression", is, "Not a Valid BMP File", getByteOrder());
    148        bitmapDataSize = read4Bytes("Bitmap Data Size", is, "Not a Valid BMP File", getByteOrder());
    149        hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File", getByteOrder());
    150        vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File", getByteOrder());
    151        colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File", getByteOrder());
    152        colorsImportant = read4Bytes("ColorsImportant", is, "Not a Valid BMP File", getByteOrder());
    153        if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
    154            // 52 = BITMAPV2INFOHEADER, now undocumented
    155            // see http://en.wikipedia.org/wiki/BMP_file_format
    156            redMask = read4Bytes("RedMask", is, "Not a Valid BMP File", getByteOrder());
    157            greenMask = read4Bytes("GreenMask", is, "Not a Valid BMP File", getByteOrder());
    158            blueMask = read4Bytes("BlueMask", is, "Not a Valid BMP File", getByteOrder());
    159        }
    160        if (bitmapHeaderSize >= 56) {
    161            // 56 = the now undocumented BITMAPV3HEADER sometimes used by
    162            // Photoshop
    163            // see http://forums.adobe.com/thread/751592?tstart=1
    164            alphaMask = read4Bytes("AlphaMask", is, "Not a Valid BMP File", getByteOrder());
    165        }
    166        if (bitmapHeaderSize >= 108) {
    167            // BITMAPV4HEADER
    168            colorSpaceType = read4Bytes("ColorSpaceType", is, "Not a Valid BMP File", getByteOrder());
    169            colorSpace.red.x = read4Bytes("ColorSpaceRedX", is, "Not a Valid BMP File", getByteOrder());
    170            colorSpace.red.y = read4Bytes("ColorSpaceRedY", is, "Not a Valid BMP File", getByteOrder());
    171            colorSpace.red.z = read4Bytes("ColorSpaceRedZ", is, "Not a Valid BMP File", getByteOrder());
    172            colorSpace.green.x = read4Bytes("ColorSpaceGreenX", is, "Not a Valid BMP File", getByteOrder());
    173            colorSpace.green.y = read4Bytes("ColorSpaceGreenY", is, "Not a Valid BMP File", getByteOrder());
    174            colorSpace.green.z = read4Bytes("ColorSpaceGreenZ", is, "Not a Valid BMP File", getByteOrder());
    175            colorSpace.blue.x = read4Bytes("ColorSpaceBlueX", is, "Not a Valid BMP File", getByteOrder());
    176            colorSpace.blue.y = read4Bytes("ColorSpaceBlueY", is, "Not a Valid BMP File", getByteOrder());
    177            colorSpace.blue.z = read4Bytes("ColorSpaceBlueZ", is, "Not a Valid BMP File", getByteOrder());
    178            gammaRed = read4Bytes("GammaRed", is, "Not a Valid BMP File", getByteOrder());
    179            gammaGreen = read4Bytes("GammaGreen", is, "Not a Valid BMP File", getByteOrder());
    180            gammaBlue = read4Bytes("GammaBlue", is, "Not a Valid BMP File", getByteOrder());
    181        }
    182        if (bitmapHeaderSize >= 124) {
    183            // BITMAPV5HEADER
    184            intent = read4Bytes("Intent", is, "Not a Valid BMP File", getByteOrder());
    185            profileData = read4Bytes("ProfileData", is, "Not a Valid BMP File", getByteOrder());
    186            profileSize = read4Bytes("ProfileSize", is, "Not a Valid BMP File", getByteOrder());
    187            reservedV5 = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
    188        }
    189
    190        if (LOGGER.isLoggable(Level.FINE)) {
    191            debugNumber("identifier1", identifier1, 1);
    192            debugNumber("identifier2", identifier2, 1);
    193            debugNumber("fileSize", fileSize, 4);
    194            debugNumber("reserved", reserved, 4);
    195            debugNumber("bitmapDataOffset", bitmapDataOffset, 4);
    196            debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4);
    197            debugNumber("width", width, 4);
    198            debugNumber("height", height, 4);
    199            debugNumber("planes", planes, 2);
    200            debugNumber("bitsPerPixel", bitsPerPixel, 2);
    201            debugNumber("compression", compression, 4);
    202            debugNumber("bitmapDataSize", bitmapDataSize, 4);
    203            debugNumber("hResolution", hResolution, 4);
    204            debugNumber("vResolution", vResolution, 4);
    205            debugNumber("colorsUsed", colorsUsed, 4);
    206            debugNumber("colorsImportant", colorsImportant, 4);
    207            if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
    208                debugNumber("redMask", redMask, 4);
    209                debugNumber("greenMask", greenMask, 4);
    210                debugNumber("blueMask", blueMask, 4);
    211            }
    212            if (bitmapHeaderSize >= 56) {
    213                debugNumber("alphaMask", alphaMask, 4);
    214            }
    215            if (bitmapHeaderSize >= 108) {
    216                debugNumber("colorSpaceType", colorSpaceType, 4);
    217                debugNumber("colorSpace.red.x", colorSpace.red.x, 1);
    218                debugNumber("colorSpace.red.y", colorSpace.red.y, 1);
    219                debugNumber("colorSpace.red.z", colorSpace.red.z, 1);
    220                debugNumber("colorSpace.green.x", colorSpace.green.x, 1);
    221                debugNumber("colorSpace.green.y", colorSpace.green.y, 1);
    222                debugNumber("colorSpace.green.z", colorSpace.green.z, 1);
    223                debugNumber("colorSpace.blue.x", colorSpace.blue.x, 1);
    224                debugNumber("colorSpace.blue.y", colorSpace.blue.y, 1);
    225                debugNumber("colorSpace.blue.z", colorSpace.blue.z, 1);
    226                debugNumber("gammaRed", gammaRed, 4);
    227                debugNumber("gammaGreen", gammaGreen, 4);
    228                debugNumber("gammaBlue", gammaBlue, 4);
    229            }
    230            if (bitmapHeaderSize >= 124) {
    231                debugNumber("intent", intent, 4);
    232                debugNumber("profileData", profileData, 4);
    233                debugNumber("profileSize", profileSize, 4);
    234                debugNumber("reservedV5", reservedV5, 4);
    235            }
    236        }
    237
    238        return new BmpHeaderInfo(identifier1, identifier2,
    239                fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width,
    240                height, planes, bitsPerPixel, compression, bitmapDataSize,
    241                hResolution, vResolution, colorsUsed, colorsImportant, redMask,
    242                greenMask, blueMask, alphaMask, colorSpaceType, colorSpace,
    243                gammaRed, gammaGreen, gammaBlue, intent, profileData,
    244                profileSize, reservedV5);
    245    }
    246
    247    private byte[] getRLEBytes(final InputStream is, final int rleSamplesPerByte) throws IOException {
    248        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    249
    250        // this.setDebug(true);
    251
    252        boolean done = false;
    253        while (!done) {
    254            final int a = 0xff & readByte("RLE a", is, "BMP: Bad RLE");
    255            baos.write(a);
    256            final int b = 0xff & readByte("RLE b", is, "BMP: Bad RLE");
    257            baos.write(b);
    258
    259            if (a == 0) {
    260                switch (b) {
    261                case 0: // EOL
    262                    break;
    263                case 1: // EOF
    264                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    265                    // );
    266                    done = true;
    267                    break;
    268                case 2: {
    269                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    270                    // );
    271                    final int c = 0xff & readByte("RLE c", is, "BMP: Bad RLE");
    272                    baos.write(c);
    273                    final int d = 0xff & readByte("RLE d", is, "BMP: Bad RLE");
    274                    baos.write(d);
    275
    276                }
    277                    break;
    278                default: {
    279                    int size = b / rleSamplesPerByte;
    280                    if ((b % rleSamplesPerByte) > 0) {
    281                        size++;
    282                    }
    283                    if ((size % 2) != 0) {
    284                        size++;
    285                    }
    286
    287                    // System.out.println("b: " + b);
    288                    // System.out.println("size: " + size);
    289                    // System.out.println("RLESamplesPerByte: " +
    290                    // RLESamplesPerByte);
    291                    // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    292                    // );
    293                    final byte[] bytes = readBytes("bytes", is, size,
    294                            "RLE: Absolute Mode");
    295                    baos.write(bytes);
    296                }
    297                    break;
    298                }
    299            }
    300        }
    301
    302        return baos.toByteArray();
    303    }
    304
    305    private BmpImageContents readImageContents(final InputStream is,
    306            final FormatCompliance formatCompliance)
    307            throws ImageReadException, IOException {
    308        final BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance);
    309
    310        int colorTableSize = bhi.colorsUsed;
    311        if (colorTableSize == 0) {
    312            colorTableSize = (1 << bhi.bitsPerPixel);
    313        }
    314
    315        if (LOGGER.isLoggable(Level.FINE)) {
    316            debugNumber("ColorsUsed", bhi.colorsUsed, 4);
    317            debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4);
    318            debugNumber("ColorTableSize", colorTableSize, 4);
    319            debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4);
    320            debugNumber("Compression", bhi.compression, 4);
    321        }
    322
    323        // A palette is always valid, even for images that don't need it
    324        // (like 32 bpp), it specifies the "optimal color palette" for
    325        // when the image is displayed on a <= 256 color graphics card.
    326        int paletteLength;
    327        int rleSamplesPerByte = 0;
    328        boolean rle = false;
    329
    330        switch (bhi.compression) {
    331        case BI_RGB:
    332            if (LOGGER.isLoggable(Level.FINE)) {
    333                LOGGER.fine("Compression: BI_RGB");
    334            }
    335            if (bhi.bitsPerPixel <= 8) {
    336                paletteLength = 4 * colorTableSize;
    337            } else {
    338                paletteLength = 0;
    339            }
    340            // BytesPerPaletteEntry = 0;
    341            // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel);
    342            // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel
    343            // <= 16));
    344            break;
    345
    346        case BI_RLE4:
    347            if (LOGGER.isLoggable(Level.FINE)) {
    348                LOGGER.fine("Compression: BI_RLE4");
    349            }
    350            paletteLength = 4 * colorTableSize;
    351            rleSamplesPerByte = 2;
    352            // ExtraBitsPerPixel = 4;
    353            rle = true;
    354            // // BytesPerPixel = 2;
    355            // // BytesPerPaletteEntry = 0;
    356            break;
    357        //
    358        case BI_RLE8:
    359            if (LOGGER.isLoggable(Level.FINE)) {
    360                LOGGER.fine("Compression: BI_RLE8");
    361            }
    362            paletteLength = 4 * colorTableSize;
    363            rleSamplesPerByte = 1;
    364            // ExtraBitsPerPixel = 8;
    365            rle = true;
    366            // BytesPerPixel = 2;
    367            // BytesPerPaletteEntry = 0;
    368            break;
    369        //
    370        case BI_BITFIELDS:
    371            if (LOGGER.isLoggable(Level.FINE)) {
    372                LOGGER.fine("Compression: BI_BITFIELDS");
    373            }
    374            if (bhi.bitsPerPixel <= 8) {
    375                paletteLength = 4 * colorTableSize;
    376            } else {
    377                paletteLength = 0;
    378            }
    379            // BytesPerPixel = 2;
    380            // BytesPerPaletteEntry = 4;
    381            break;
    382
    383        default:
    384            throw new ImageReadException("BMP: Unknown Compression: "
    385                    + bhi.compression);
    386        }
    387
    388        if (paletteLength < 0) {
    389            throw new ImageReadException("BMP: Invalid negative palette length: " + paletteLength);
    390        }
    391
    392        byte[] colorTable = null;
    393        if (paletteLength > 0) {
    394            colorTable = readBytes("ColorTable", is, paletteLength,
    395                    "Not a Valid BMP File");
    396        }
    397
    398        if (LOGGER.isLoggable(Level.FINE)) {
    399            debugNumber("paletteLength", paletteLength, 4);
    400            LOGGER.fine("ColorTable: "
    401                    + ((colorTable == null) ? "null" : Integer.toString(colorTable.length)));
    402        }
    403
    404        int imageLineLength = (((bhi.bitsPerPixel) * bhi.width) + 7) / 8;
    405
    406        if (LOGGER.isLoggable(Level.FINE)) {
    407            final int pixelCount = bhi.width * bhi.height;
    408            // this.debugNumber("Total BitsPerPixel",
    409            // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4);
    410            // this.debugNumber("Total Bit Per Line",
    411            // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4);
    412            // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4);
    413            debugNumber("bhi.Width", bhi.width, 4);
    414            debugNumber("bhi.Height", bhi.height, 4);
    415            debugNumber("ImageLineLength", imageLineLength, 4);
    416            // this.debugNumber("imageDataSize", imageDataSize, 4);
    417            debugNumber("PixelCount", pixelCount, 4);
    418        }
    419        // int ImageLineLength = BytesPerPixel * bhi.Width;
    420        while ((imageLineLength % 4) != 0) {
    421            imageLineLength++;
    422        }
    423
    424        final int headerSize = BITMAP_FILE_HEADER_SIZE
    425                + bhi.bitmapHeaderSize
    426                + (bhi.bitmapHeaderSize == 40
    427                        && bhi.compression == BI_BITFIELDS ? 3 * 4 : 0);
    428        final int expectedDataOffset = headerSize + paletteLength;
    429
    430        if (LOGGER.isLoggable(Level.FINE)) {
    431            debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4);
    432            debugNumber("expectedDataOffset", expectedDataOffset, 4);
    433        }
    434        final int extraBytes = bhi.bitmapDataOffset - expectedDataOffset;
    435        if (extraBytes < 0) {
    436            throw new ImageReadException("BMP has invalid image data offset: "
    437                    + bhi.bitmapDataOffset + " (expected: "
    438                    + expectedDataOffset + ", paletteLength: " + paletteLength
    439                    + ", headerSize: " + headerSize + ")");
    440        }
    441        if (extraBytes > 0) {
    442            readBytes("BitmapDataOffset", is, extraBytes, "Not a Valid BMP File");
    443        }
    444
    445        final int imageDataSize = bhi.height * imageLineLength;
    446
    447        if (LOGGER.isLoggable(Level.FINE)) {
    448            debugNumber("imageDataSize", imageDataSize, 4);
    449        }
    450
    451        byte[] imageData;
    452        if (rle) {
    453            imageData = getRLEBytes(is, rleSamplesPerByte);
    454        } else {
    455            imageData = readBytes("ImageData", is, imageDataSize,
    456                    "Not a Valid BMP File");
    457        }
    458
    459        if (LOGGER.isLoggable(Level.FINE)) {
    460            debugNumber("ImageData.length", imageData.length, 4);
    461        }
    462
    463        PixelParser pixelParser;
    464
    465        switch (bhi.compression) {
    466        case BI_RLE4:
    467        case BI_RLE8:
    468            pixelParser = new PixelParserRle(bhi, colorTable, imageData);
    469            break;
    470        case BI_RGB:
    471            pixelParser = new PixelParserRgb(bhi, colorTable, imageData);
    472            break;
    473        case BI_BITFIELDS:
    474            pixelParser = new PixelParserBitFields(bhi, colorTable, imageData);
    475            break;
    476        default:
    477            throw new ImageReadException("BMP: Unknown Compression: "
    478                    + bhi.compression);
    479        }
    480
    481        return new BmpImageContents(bhi, colorTable, imageData, pixelParser);
    482    }
    483
    484    private BmpHeaderInfo readBmpHeaderInfo(final ByteSource byteSource) throws ImageReadException, IOException {
    485        try (InputStream is = byteSource.getInputStream()) {
    486            // readSignature(is);
    487            return readBmpHeaderInfo(is, null);
    488        }
    489    }
    490
    491    @Override
    492    public byte[] getICCProfileBytes(final ByteSource byteSource, final BmpImagingParameters params)
    493            throws ImageReadException, IOException {
    494        return null;
    495    }
    496
    497    @Override
    498    public Dimension getImageSize(final ByteSource byteSource, final BmpImagingParameters params)
    499            throws ImageReadException, IOException {
    500        final BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource);
    501
    502        return new Dimension(bhi.width, bhi.height);
    503
    504    }
    505
    506    @Override
    507    public ImageMetadata getMetadata(final ByteSource byteSource, final BmpImagingParameters params)
    508            throws ImageReadException, IOException {
    509        // TODO this should throw UnsupportedOperationException, but RoundtripTest has to be refactored completely before this can be changed
    510        return null;
    511    }
    512
    513    private String getBmpTypeDescription(final int identifier1, final int identifier2) {
    514        if ((identifier1 == 'B') && (identifier2 == 'M')) {
    515            return "Windows 3.1x, 95, NT,";
    516        }
    517        if ((identifier1 == 'B') && (identifier2 == 'A')) {
    518            return "OS/2 Bitmap Array";
    519        }
    520        if ((identifier1 == 'C') && (identifier2 == 'I')) {
    521            return "OS/2 Color Icon";
    522        }
    523        if ((identifier1 == 'C') && (identifier2 == 'P')) {
    524            return "OS/2 Color Pointer";
    525        }
    526        if ((identifier1 == 'I') && (identifier2 == 'C')) {
    527            return "OS/2 Icon";
    528        }
    529        if ((identifier1 == 'P') && (identifier2 == 'T')) {
    530            return "OS/2 Pointer";
    531        }
    532
    533        return "Unknown";
    534    }
    535
    536    @Override
    537    public ImageInfo getImageInfo(final ByteSource byteSource, final BmpImagingParameters params)
    538            throws ImageReadException, IOException {
    539        BmpImageContents ic = null;
    540        try (InputStream is = byteSource.getInputStream()) {
    541            ic = readImageContents(is, FormatCompliance.getDefault());
    542        }
    543
    544        final BmpHeaderInfo bhi = ic.bhi;
    545        final byte[] colorTable = ic.colorTable;
    546
    547        if (bhi == null) {
    548            throw new ImageReadException("BMP: couldn't read header");
    549        }
    550
    551        final int height = bhi.height;
    552        final int width = bhi.width;
    553
    554        final List<String> comments = new ArrayList<>();
    555        // TODO: comments...
    556
    557        final int bitsPerPixel = bhi.bitsPerPixel;
    558        final ImageFormat format = ImageFormats.BMP;
    559        final String name = "BMP Windows Bitmap";
    560        final String mimeType = "image/x-ms-bmp";
    561        // we ought to count images, but don't yet.
    562        final int numberOfImages = -1;
    563        // not accurate ... only reflects first
    564        final boolean progressive = false;
    565        // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0);
    566        //
    567        // pixels per meter
    568        final int physicalWidthDpi = (int) Math.round(bhi.hResolution * .0254);
    569        final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
    570        // int physicalHeightDpi = 72;
    571        final int physicalHeightDpi = (int) Math.round(bhi.vResolution * .0254);
    572        final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);
    573
    574        final String formatDetails = "Bmp (" + (char) bhi.identifier1
    575                + (char) bhi.identifier2 + ": "
    576                + getBmpTypeDescription(bhi.identifier1, bhi.identifier2) + ")";
    577
    578        final boolean transparent = false;
    579
    580        final boolean usesPalette = colorTable != null;
    581        final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
    582        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.RLE;
    583
    584        return new ImageInfo(formatDetails, bitsPerPixel, comments,
    585                format, name, height, mimeType, numberOfImages,
    586                physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
    587                physicalWidthInch, width, progressive, transparent,
    588                usesPalette, colorType, compressionAlgorithm);
    589    }
    590
    591    @Override
    592    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
    593            throws ImageReadException, IOException {
    594        pw.println("bmp.dumpImageFile");
    595
    596        final ImageInfo imageData = getImageInfo(byteSource, null);
    597
    598        imageData.toString(pw, "");
    599
    600        pw.println("");
    601
    602        return true;
    603    }
    604
    605    @Override
    606    public FormatCompliance getFormatCompliance(final ByteSource byteSource)
    607            throws ImageReadException, IOException {
    608        final FormatCompliance result = new FormatCompliance(
    609                byteSource.getDescription());
    610
    611        try (InputStream is = byteSource.getInputStream()) {
    612            readImageContents(is, result);
    613        }
    614
    615        return result;
    616    }
    617
    618    @Override
    619    public BufferedImage getBufferedImage(final ByteSource byteSource, final BmpImagingParameters params)
    620            throws ImageReadException, IOException {
    621        try (InputStream is = byteSource.getInputStream()) {
    622            return getBufferedImage(is, params);
    623        }
    624    }
    625
    626    public BufferedImage getBufferedImage(final InputStream inputStream, final BmpImagingParameters params)
    627            throws ImageReadException, IOException {
    628        final BmpImageContents ic = readImageContents(inputStream, FormatCompliance.getDefault());
    629
    630        final BmpHeaderInfo bhi = ic.bhi;
    631        // byte colorTable[] = ic.colorTable;
    632        // byte imageData[] = ic.imageData;
    633
    634        final int width = bhi.width;
    635        final int height = bhi.height;
    636
    637        if (LOGGER.isLoggable(Level.FINE)) {
    638            LOGGER.fine("width: " + width);
    639            LOGGER.fine("height: " + height);
    640            LOGGER.fine("width*height: " + width * height);
    641            LOGGER.fine("width*height*4: " + width * height * 4);
    642        }
    643
    644        final PixelParser pixelParser = ic.pixelParser;
    645        final ImageBuilder imageBuilder = new ImageBuilder(width, height, true);
    646        pixelParser.processImage(imageBuilder);
    647
    648        return imageBuilder.getBufferedImage();
    649
    650    }
    651
    652    @Override
    653    public void writeImage(final BufferedImage src, final OutputStream os, BmpImagingParameters params) throws ImageWriteException, IOException {
    654        if (params == null) {
    655            params = new BmpImagingParameters();
    656        }
    657        PixelDensity pixelDensity = params.getPixelDensity();
    658
    659        final SimplePalette palette = new PaletteFactory().makeExactRgbPaletteSimple(
    660                src, 256);
    661
    662        BmpWriter writer;
    663        if (palette == null) {
    664            writer = new BmpWriterRgb();
    665        } else {
    666            writer = new BmpWriterPalette(palette);
    667        }
    668
    669        final byte[] imageData = writer.getImageData(src);
    670        final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN);
    671
    672        // write BitmapFileHeader
    673        os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap
    674        os.write(0x4d); // M
    675
    676        final int filesize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header size
    677                4 * writer.getPaletteSize() + // palette size in bytes
    678                imageData.length;
    679        bos.write4Bytes(filesize);
    680
    681        bos.write4Bytes(0); // reserved
    682        bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE
    683                + 4 * writer.getPaletteSize()); // Bitmap Data Offset
    684
    685        final int width = src.getWidth();
    686        final int height = src.getHeight();
    687
    688        // write BitmapInfoHeader
    689        bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size
    690        bos.write4Bytes(width); // width
    691        bos.write4Bytes(height); // height
    692        bos.write2Bytes(1); // Number of Planes
    693        bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel
    694
    695        bos.write4Bytes(BI_RGB); // Compression
    696        bos.write4Bytes(imageData.length); // Bitmap Data Size
    697        bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.horizontalDensityMetres()) : 0); // HResolution
    698        bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.verticalDensityMetres()) : 0); // VResolution
    699        if (palette == null) {
    700            bos.write4Bytes(0); // Colors
    701        } else {
    702            bos.write4Bytes(palette.length()); // Colors
    703        }
    704        bos.write4Bytes(0); // Important Colors
    705        // bos.write_4_bytes(0); // Compression
    706
    707        // write Palette
    708        writer.writePalette(bos);
    709        // write Image Data
    710        bos.write(imageData);
    711    }
    712}