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