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}