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