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