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