View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.bmp;
18  
19  import static org.apache.commons.imaging.ImagingConstants.BUFFERED_IMAGE_FACTORY;
20  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
21  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_PIXEL_DENSITY;
22  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_VERBOSE;
23  import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
24  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
25  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
26  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
27  
28  import java.awt.Dimension;
29  import java.awt.image.BufferedImage;
30  import java.io.ByteArrayOutputStream;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.OutputStream;
34  import java.io.PrintWriter;
35  import java.nio.ByteOrder;
36  import java.util.ArrayList;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  
41  import org.apache.commons.imaging.FormatCompliance;
42  import org.apache.commons.imaging.ImageFormat;
43  import org.apache.commons.imaging.ImageFormats;
44  import org.apache.commons.imaging.ImageInfo;
45  import org.apache.commons.imaging.ImageParser;
46  import org.apache.commons.imaging.ImageReadException;
47  import org.apache.commons.imaging.ImageWriteException;
48  import org.apache.commons.imaging.PixelDensity;
49  import org.apache.commons.imaging.common.BinaryOutputStream;
50  import org.apache.commons.imaging.common.ImageBuilder;
51  import org.apache.commons.imaging.common.ImageMetadata;
52  import org.apache.commons.imaging.common.bytesource.ByteSource;
53  import org.apache.commons.imaging.palette.PaletteFactory;
54  import org.apache.commons.imaging.palette.SimplePalette;
55  
56  public class BmpImageParser extends ImageParser {
57      private static final String DEFAULT_EXTENSION = ".bmp";
58      private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, };
59      private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, };
60      private static final int BI_RGB = 0;
61      private static final int BI_RLE4 = 2;
62      private static final int BI_RLE8 = 1;
63      private static final int BI_BITFIELDS = 3;
64      private static final int BITMAP_FILE_HEADER_SIZE = 14;
65      private static final int BITMAP_INFO_HEADER_SIZE = 40;
66  
67      public BmpImageParser() {
68          super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
69      }
70  
71      @Override
72      public String getName() {
73          return "Bmp-Custom";
74      }
75  
76      @Override
77      public String getDefaultExtension() {
78          return DEFAULT_EXTENSION;
79      }
80  
81      @Override
82      protected String[] getAcceptedExtensions() {
83          return ACCEPTED_EXTENSIONS;
84      }
85  
86      @Override
87      protected ImageFormat[] getAcceptedTypes() {
88          return new ImageFormat[] { ImageFormats.BMP, //
89          };
90      }
91  
92      private BmpHeaderInfo readBmpHeaderInfo(final InputStream is,
93              final FormatCompliance formatCompliance, final boolean verbose)
94              throws ImageReadException, IOException {
95          final byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File");
96          final byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File");
97  
98          if (formatCompliance != null) {
99              formatCompliance.compareBytes("Signature", BMP_HEADER_SIGNATURE,
100                     new byte[]{identifier1, identifier2,});
101         }
102 
103         final int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File", getByteOrder());
104         final int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
105         final int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, "Not a Valid BMP File", getByteOrder());
106 
107         final int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, "Not a Valid BMP File", getByteOrder());
108         int width = 0;
109         int height = 0;
110         int planes = 0;
111         int bitsPerPixel = 0;
112         int compression = 0;
113         int bitmapDataSize = 0;
114         int hResolution = 0;
115         int vResolution = 0;
116         int colorsUsed = 0;
117         int colorsImportant = 0;
118         int redMask = 0;
119         int greenMask = 0;
120         int blueMask = 0;
121         int alphaMask = 0;
122         int colorSpaceType = 0;
123         final BmpHeaderInfo.ColorSpace colorSpace = new BmpHeaderInfo.ColorSpace();
124         colorSpace.red = new BmpHeaderInfo.ColorSpaceCoordinate();
125         colorSpace.green = new BmpHeaderInfo.ColorSpaceCoordinate();
126         colorSpace.blue = new BmpHeaderInfo.ColorSpaceCoordinate();
127         int gammaRed = 0;
128         int gammaGreen = 0;
129         int gammaBlue = 0;
130         int intent = 0;
131         int profileData = 0;
132         int profileSize = 0;
133         int reservedV5 = 0;
134 
135         if (bitmapHeaderSize >= 40) {
136             // BITMAPINFOHEADER
137             width = read4Bytes("Width", is, "Not a Valid BMP File", getByteOrder());
138             height = read4Bytes("Height", is, "Not a Valid BMP File", getByteOrder());
139             planes = read2Bytes("Planes", is, "Not a Valid BMP File", getByteOrder());
140             bitsPerPixel = read2Bytes("Bits Per Pixel", is, "Not a Valid BMP File", getByteOrder());
141             compression = read4Bytes("Compression", is, "Not a Valid BMP File", getByteOrder());
142             bitmapDataSize = read4Bytes("Bitmap Data Size", is, "Not a Valid BMP File", getByteOrder());
143             hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File", getByteOrder());
144             vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File", getByteOrder());
145             colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File", getByteOrder());
146             colorsImportant = read4Bytes("ColorsImportant", is, "Not a Valid BMP File", getByteOrder());
147             if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
148                 // 52 = BITMAPV2INFOHEADER, now undocumented
149                 // see http://en.wikipedia.org/wiki/BMP_file_format
150                 redMask = read4Bytes("RedMask", is, "Not a Valid BMP File", getByteOrder());
151                 greenMask = read4Bytes("GreenMask", is, "Not a Valid BMP File", getByteOrder());
152                 blueMask = read4Bytes("BlueMask", is, "Not a Valid BMP File", getByteOrder());
153             }
154             if (bitmapHeaderSize >= 56) {
155                 // 56 = the now undocumented BITMAPV3HEADER sometimes used by
156                 // Photoshop
157                 // see http://forums.adobe.com/thread/751592?tstart=1
158                 alphaMask = read4Bytes("AlphaMask", is, "Not a Valid BMP File", getByteOrder());
159             }
160             if (bitmapHeaderSize >= 108) {
161                 // BITMAPV4HEADER
162                 colorSpaceType = read4Bytes("ColorSpaceType", is, "Not a Valid BMP File", getByteOrder());
163                 colorSpace.red.x = read4Bytes("ColorSpaceRedX", is, "Not a Valid BMP File", getByteOrder());
164                 colorSpace.red.y = read4Bytes("ColorSpaceRedY", is, "Not a Valid BMP File", getByteOrder());
165                 colorSpace.red.z = read4Bytes("ColorSpaceRedZ", is, "Not a Valid BMP File", getByteOrder());
166                 colorSpace.green.x = read4Bytes("ColorSpaceGreenX", is, "Not a Valid BMP File", getByteOrder());
167                 colorSpace.green.y = read4Bytes("ColorSpaceGreenY", is, "Not a Valid BMP File", getByteOrder());
168                 colorSpace.green.z = read4Bytes("ColorSpaceGreenZ", is, "Not a Valid BMP File", getByteOrder());
169                 colorSpace.blue.x = read4Bytes("ColorSpaceBlueX", is, "Not a Valid BMP File", getByteOrder());
170                 colorSpace.blue.y = read4Bytes("ColorSpaceBlueY", is, "Not a Valid BMP File", getByteOrder());
171                 colorSpace.blue.z = read4Bytes("ColorSpaceBlueZ", is, "Not a Valid BMP File", getByteOrder());
172                 gammaRed = read4Bytes("GammaRed", is, "Not a Valid BMP File", getByteOrder());
173                 gammaGreen = read4Bytes("GammaGreen", is, "Not a Valid BMP File", getByteOrder());
174                 gammaBlue = read4Bytes("GammaBlue", is, "Not a Valid BMP File", getByteOrder());
175             }
176             if (bitmapHeaderSize >= 124) {
177                 // BITMAPV5HEADER
178                 intent = read4Bytes("Intent", is, "Not a Valid BMP File", getByteOrder());
179                 profileData = read4Bytes("ProfileData", is, "Not a Valid BMP File", getByteOrder());
180                 profileSize = read4Bytes("ProfileSize", is, "Not a Valid BMP File", getByteOrder());
181                 reservedV5 = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder());
182             }
183         } else {
184             throw new ImageReadException("Invalid/unsupported BMP file");
185         }
186 
187         if (verbose) {
188             debugNumber("identifier1", identifier1, 1);
189             debugNumber("identifier2", identifier2, 1);
190             debugNumber("fileSize", fileSize, 4);
191             debugNumber("reserved", reserved, 4);
192             debugNumber("bitmapDataOffset", bitmapDataOffset, 4);
193             debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4);
194             debugNumber("width", width, 4);
195             debugNumber("height", height, 4);
196             debugNumber("planes", planes, 2);
197             debugNumber("bitsPerPixel", bitsPerPixel, 2);
198             debugNumber("compression", compression, 4);
199             debugNumber("bitmapDataSize", bitmapDataSize, 4);
200             debugNumber("hResolution", hResolution, 4);
201             debugNumber("vResolution", vResolution, 4);
202             debugNumber("colorsUsed", colorsUsed, 4);
203             debugNumber("colorsImportant", colorsImportant, 4);
204             if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) {
205                 debugNumber("redMask", redMask, 4);
206                 debugNumber("greenMask", greenMask, 4);
207                 debugNumber("blueMask", blueMask, 4);
208             }
209             if (bitmapHeaderSize >= 56) {
210                 debugNumber("alphaMask", alphaMask, 4);
211             }
212             if (bitmapHeaderSize >= 108) {
213                 debugNumber("colorSpaceType", colorSpaceType, 4);
214                 debugNumber("colorSpace.red.x", colorSpace.red.x, 1);
215                 debugNumber("colorSpace.red.y", colorSpace.red.y, 1);
216                 debugNumber("colorSpace.red.z", colorSpace.red.z, 1);
217                 debugNumber("colorSpace.green.x", colorSpace.green.x, 1);
218                 debugNumber("colorSpace.green.y", colorSpace.green.y, 1);
219                 debugNumber("colorSpace.green.z", colorSpace.green.z, 1);
220                 debugNumber("colorSpace.blue.x", colorSpace.blue.x, 1);
221                 debugNumber("colorSpace.blue.y", colorSpace.blue.y, 1);
222                 debugNumber("colorSpace.blue.z", colorSpace.blue.z, 1);
223                 debugNumber("gammaRed", gammaRed, 4);
224                 debugNumber("gammaGreen", gammaGreen, 4);
225                 debugNumber("gammaBlue", gammaBlue, 4);
226             }
227             if (bitmapHeaderSize >= 124) {
228                 debugNumber("intent", intent, 4);
229                 debugNumber("profileData", profileData, 4);
230                 debugNumber("profileSize", profileSize, 4);
231                 debugNumber("reservedV5", reservedV5, 4);
232             }
233         }
234 
235         return new BmpHeaderInfo(identifier1, identifier2,
236                 fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width,
237                 height, planes, bitsPerPixel, compression, bitmapDataSize,
238                 hResolution, vResolution, colorsUsed, colorsImportant, redMask,
239                 greenMask, blueMask, alphaMask, colorSpaceType, colorSpace,
240                 gammaRed, gammaGreen, gammaBlue, intent, profileData,
241                 profileSize, reservedV5);
242     }
243 
244     private byte[] getRLEBytes(final InputStream is, final int rleSamplesPerByte) throws IOException {
245         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
246 
247         // this.setDebug(true);
248 
249         boolean done = false;
250         while (!done) {
251             final int a = 0xff & readByte("RLE a", is, "BMP: Bad RLE");
252             baos.write(a);
253             final int b = 0xff & readByte("RLE b", is, "BMP: Bad RLE");
254             baos.write(b);
255 
256             if (a == 0) {
257                 switch (b) {
258                 case 0: // EOL
259                     break;
260                 case 1: // EOF
261                     // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
262                     // );
263                     done = true;
264                     break;
265                 case 2: {
266                     // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
267                     // );
268                     final int c = 0xff & readByte("RLE c", is, "BMP: Bad RLE");
269                     baos.write(c);
270                     final int d = 0xff & readByte("RLE d", is, "BMP: Bad RLE");
271                     baos.write(d);
272 
273                 }
274                     break;
275                 default: {
276                     int size = b / rleSamplesPerByte;
277                     if ((b % rleSamplesPerByte) > 0) {
278                         size++;
279                     }
280                     if ((size % 2) != 0) {
281                         size++;
282                     }
283 
284                     // System.out.println("b: " + b);
285                     // System.out.println("size: " + size);
286                     // System.out.println("RLESamplesPerByte: " +
287                     // RLESamplesPerByte);
288                     // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
289                     // );
290                     final byte[] bytes = readBytes("bytes", is, size,
291                             "RLE: Absolute Mode");
292                     baos.write(bytes);
293                 }
294                     break;
295                 }
296             }
297         }
298 
299         return baos.toByteArray();
300     }
301 
302     private BmpImageContents readImageContents(final InputStream is,
303             final FormatCompliance formatCompliance, final boolean verbose)
304             throws ImageReadException, IOException {
305         final BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance, verbose);
306 
307         int colorTableSize = bhi.colorsUsed;
308         if (colorTableSize == 0) {
309             colorTableSize = (1 << bhi.bitsPerPixel);
310         }
311 
312         if (verbose) {
313             debugNumber("ColorsUsed", bhi.colorsUsed, 4);
314             debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4);
315             debugNumber("ColorTableSize", colorTableSize, 4);
316             debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4);
317             debugNumber("Compression", bhi.compression, 4);
318         }
319 
320         // A palette is always valid, even for images that don't need it
321         // (like 32 bpp), it specifies the "optimal color palette" for
322         // when the image is displayed on a <= 256 color graphics card.
323         int paletteLength;
324         int rleSamplesPerByte = 0;
325         boolean rle = false;
326 
327         switch (bhi.compression) {
328         case BI_RGB:
329             if (verbose) {
330                 System.out.println("Compression: BI_RGB");
331             }
332             if (bhi.bitsPerPixel <= 8) {
333                 paletteLength = 4 * colorTableSize;
334             } else {
335                 paletteLength = 0;
336             }
337             // BytesPerPaletteEntry = 0;
338             // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel);
339             // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel
340             // <= 16));
341             break;
342 
343         case BI_RLE4:
344             if (verbose) {
345                 System.out.println("Compression: BI_RLE4");
346             }
347             paletteLength = 4 * colorTableSize;
348             rleSamplesPerByte = 2;
349             // ExtraBitsPerPixel = 4;
350             rle = true;
351             // // BytesPerPixel = 2;
352             // // BytesPerPaletteEntry = 0;
353             break;
354         //
355         case BI_RLE8:
356             if (verbose) {
357                 System.out.println("Compression: BI_RLE8");
358             }
359             paletteLength = 4 * colorTableSize;
360             rleSamplesPerByte = 1;
361             // ExtraBitsPerPixel = 8;
362             rle = true;
363             // BytesPerPixel = 2;
364             // BytesPerPaletteEntry = 0;
365             break;
366         //
367         case BI_BITFIELDS:
368             if (verbose) {
369                 System.out.println("Compression: BI_BITFIELDS");
370             }
371             if (bhi.bitsPerPixel <= 8) {
372                 paletteLength = 4 * colorTableSize;
373             } else {
374                 paletteLength = 0;
375             }
376             // BytesPerPixel = 2;
377             // BytesPerPaletteEntry = 4;
378             break;
379 
380         default:
381             throw new ImageReadException("BMP: Unknown Compression: "
382                     + bhi.compression);
383         }
384 
385         byte[] colorTable = null;
386         if (paletteLength > 0) {
387             colorTable = readBytes("ColorTable", is, paletteLength,
388                     "Not a Valid BMP File");
389         }
390 
391         if (verbose) {
392             debugNumber("paletteLength", paletteLength, 4);
393             System.out.println("ColorTable: "
394                     + ((colorTable == null) ? "null" : Integer.toString(colorTable.length)));
395         }
396 
397         final int pixelCount = bhi.width * bhi.height;
398 
399         int imageLineLength = (((bhi.bitsPerPixel) * bhi.width) + 7) / 8;
400 
401         if (verbose) {
402             // this.debugNumber("Total BitsPerPixel",
403             // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4);
404             // this.debugNumber("Total Bit Per Line",
405             // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4);
406             // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4);
407             debugNumber("bhi.Width", bhi.width, 4);
408             debugNumber("bhi.Height", bhi.height, 4);
409             debugNumber("ImageLineLength", imageLineLength, 4);
410             // this.debugNumber("imageDataSize", imageDataSize, 4);
411             debugNumber("PixelCount", pixelCount, 4);
412         }
413         // int ImageLineLength = BytesPerPixel * bhi.Width;
414         while ((imageLineLength % 4) != 0) {
415             imageLineLength++;
416         }
417 
418         final int headerSize = BITMAP_FILE_HEADER_SIZE
419                 + bhi.bitmapHeaderSize
420                 + (bhi.bitmapHeaderSize == 40
421                         && bhi.compression == BI_BITFIELDS ? 3 * 4 : 0);
422         final int expectedDataOffset = headerSize + paletteLength;
423 
424         if (verbose) {
425             debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4);
426             debugNumber("expectedDataOffset", expectedDataOffset, 4);
427         }
428         final int extraBytes = bhi.bitmapDataOffset - expectedDataOffset;
429         if (extraBytes < 0) {
430             throw new ImageReadException("BMP has invalid image data offset: "
431                     + bhi.bitmapDataOffset + " (expected: "
432                     + expectedDataOffset + ", paletteLength: " + paletteLength
433                     + ", headerSize: " + headerSize + ")");
434         } else if (extraBytes > 0) {
435             readBytes("BitmapDataOffset", is, extraBytes, "Not a Valid BMP File");
436         }
437 
438         final int imageDataSize = bhi.height * imageLineLength;
439 
440         if (verbose) {
441             debugNumber("imageDataSize", imageDataSize, 4);
442         }
443 
444         byte[] imageData;
445         if (rle) {
446             imageData = getRLEBytes(is, rleSamplesPerByte);
447         } else {
448             imageData = readBytes("ImageData", is, imageDataSize,
449                     "Not a Valid BMP File");
450         }
451 
452         if (verbose) {
453             debugNumber("ImageData.length", imageData.length, 4);
454         }
455 
456         PixelParser pixelParser;
457 
458         switch (bhi.compression) {
459         case BI_RLE4:
460         case BI_RLE8:
461             pixelParser = new PixelParserRle(bhi, colorTable, imageData);
462             break;
463         case BI_RGB:
464             pixelParser = new PixelParserRgb(bhi, colorTable, imageData);
465             break;
466         case BI_BITFIELDS:
467             pixelParser = new PixelParserBitFields(bhi, colorTable, imageData);
468             break;
469         default:
470             throw new ImageReadException("BMP: Unknown Compression: "
471                     + bhi.compression);
472         }
473 
474         return new BmpImageContents(bhi, colorTable, imageData, pixelParser);
475     }
476 
477     private BmpHeaderInfo readBmpHeaderInfo(final ByteSource byteSource,
478             final boolean verbose) throws ImageReadException, IOException {
479         try (InputStream is = byteSource.getInputStream()) {
480             // readSignature(is);
481             final BmpHeaderInfo ret = readBmpHeaderInfo(is, null, verbose);
482             return ret;
483         }
484     }
485 
486     @Override
487     public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
488             throws ImageReadException, IOException {
489         return null;
490     }
491 
492     @Override
493     public Dimension getImageSize(final ByteSource byteSource, Map<String, Object> params)
494             throws ImageReadException, IOException {
495         // make copy of params; we'll clear keys as we consume them.
496         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
497 
498         final boolean verbose =  Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE));
499 
500         if (params.containsKey(PARAM_KEY_VERBOSE)) {
501             params.remove(PARAM_KEY_VERBOSE);
502         }
503 
504         if (!params.isEmpty()) {
505             final Object firstKey = params.keySet().iterator().next();
506             throw new ImageReadException("Unknown parameter: " + firstKey);
507         }
508 
509         final BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource, verbose);
510 
511         if (bhi == null) {
512             throw new ImageReadException("BMP: couldn't read header");
513         }
514 
515         return new Dimension(bhi.width, bhi.height);
516 
517     }
518 
519     @Override
520     public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
521             throws ImageReadException, IOException {
522         // TODO this should throw UnsupportedOperationException, but RoundtripTest has to be refactored completely before this can be changed
523         return null;
524     }
525 
526     private String getBmpTypeDescription(final int identifier1, final int identifier2) {
527         if ((identifier1 == 'B') && (identifier2 == 'M')) {
528             return "Windows 3.1x, 95, NT,";
529         }
530         if ((identifier1 == 'B') && (identifier2 == 'A')) {
531             return "OS/2 Bitmap Array";
532         }
533         if ((identifier1 == 'C') && (identifier2 == 'I')) {
534             return "OS/2 Color Icon";
535         }
536         if ((identifier1 == 'C') && (identifier2 == 'P')) {
537             return "OS/2 Color Pointer";
538         }
539         if ((identifier1 == 'I') && (identifier2 == 'C')) {
540             return "OS/2 Icon";
541         }
542         if ((identifier1 == 'P') && (identifier2 == 'T')) {
543             return "OS/2 Pointer";
544         }
545 
546         return "Unknown";
547     }
548 
549     @Override
550     public ImageInfo getImageInfo(final ByteSource byteSource, Map<String, Object> params)
551             throws ImageReadException, IOException {
552         // make copy of params; we'll clear keys as we consume them.
553         params = params == null ? new HashMap<String, Object>() : new HashMap<>(params);
554 
555         final boolean verbose =  Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE));
556 
557         if (params.containsKey(PARAM_KEY_VERBOSE)) {
558             params.remove(PARAM_KEY_VERBOSE);
559         }
560 
561         if (!params.isEmpty()) {
562             final Object firstKey = params.keySet().iterator().next();
563             throw new ImageReadException("Unknown parameter: " + firstKey);
564         }
565 
566         BmpImageContents ic = null;
567         try (InputStream is = byteSource.getInputStream()) {
568             ic = readImageContents(is, FormatCompliance.getDefault(), verbose);
569         }
570 
571         if (ic == null) {
572             throw new ImageReadException("Couldn't read BMP Data");
573         }
574 
575         final BmpHeaderInfo bhi = ic.bhi;
576         final byte[] colorTable = ic.colorTable;
577 
578         if (bhi == null) {
579             throw new ImageReadException("BMP: couldn't read header");
580         }
581 
582         final int height = bhi.height;
583         final int width = bhi.width;
584 
585         final List<String> comments = new ArrayList<>();
586         // TODO: comments...
587 
588         final int bitsPerPixel = bhi.bitsPerPixel;
589         final ImageFormat format = ImageFormats.BMP;
590         final String name = "BMP Windows Bitmap";
591         final String mimeType = "image/x-ms-bmp";
592         // we ought to count images, but don't yet.
593         final int numberOfImages = -1;
594         // not accurate ... only reflects first
595         final boolean progressive = false;
596         // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0);
597         //
598         // pixels per meter
599         final int physicalWidthDpi = (int) (bhi.hResolution * .0254);
600         final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
601         // int physicalHeightDpi = 72;
602         final int physicalHeightDpi = (int) (bhi.vResolution * .0254);
603         final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);
604 
605         final String formatDetails = "Bmp (" + (char) bhi.identifier1
606                 + (char) bhi.identifier2 + ": "
607                 + getBmpTypeDescription(bhi.identifier1, bhi.identifier2) + ")";
608 
609         final boolean transparent = false;
610 
611         final boolean usesPalette = colorTable != null;
612         final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
613         final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.RLE;
614 
615         return new ImageInfo(formatDetails, bitsPerPixel, comments,
616                 format, name, height, mimeType, numberOfImages,
617                 physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
618                 physicalWidthInch, width, progressive, transparent,
619                 usesPalette, colorType, compressionAlgorithm);
620     }
621 
622     @Override
623     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
624             throws ImageReadException, IOException {
625         pw.println("bmp.dumpImageFile");
626 
627         final ImageInfo imageData = getImageInfo(byteSource, null);
628 
629         imageData.toString(pw, "");
630 
631         pw.println("");
632 
633         return true;
634     }
635 
636     @Override
637     public FormatCompliance getFormatCompliance(final ByteSource byteSource)
638             throws ImageReadException, IOException {
639         final boolean verbose = false;
640 
641         final FormatCompliance result = new FormatCompliance(
642                 byteSource.getDescription());
643 
644         try (InputStream is = byteSource.getInputStream()) {
645             readImageContents(is, result, verbose);
646         }
647 
648         return result;
649     }
650 
651     @Override
652     public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params)
653             throws ImageReadException, IOException {
654         try (InputStream is = byteSource.getInputStream()) {
655             final BufferedImage ret = getBufferedImage(is, params);
656             return ret;
657         }
658     }
659 
660     public BufferedImage getBufferedImage(final InputStream inputStream, Map<String, Object> params)
661             throws ImageReadException, IOException {
662         // make copy of params; we'll clear keys as we consume them.
663         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
664 
665         final boolean verbose = Boolean.TRUE.equals(params.get(PARAM_KEY_VERBOSE));
666 
667         if (params.containsKey(PARAM_KEY_VERBOSE)) {
668             params.remove(PARAM_KEY_VERBOSE);
669         }
670         if (params.containsKey(BUFFERED_IMAGE_FACTORY)) {
671             params.remove(BUFFERED_IMAGE_FACTORY);
672         }
673 
674         if (!params.isEmpty()) {
675             final Object firstKey = params.keySet().iterator().next();
676             throw new ImageReadException("Unknown parameter: " + firstKey);
677         }
678 
679         final BmpImageContents ic = readImageContents(inputStream,
680                 FormatCompliance.getDefault(), verbose);
681         if (ic == null) {
682             throw new ImageReadException("Couldn't read BMP Data");
683         }
684 
685         final BmpHeaderInfo bhi = ic.bhi;
686         // byte colorTable[] = ic.colorTable;
687         // byte imageData[] = ic.imageData;
688 
689         final int width = bhi.width;
690         final int height = bhi.height;
691 
692         if (verbose) {
693             System.out.println("width: " + width);
694             System.out.println("height: " + height);
695             System.out.println("width*height: " + width * height);
696             System.out.println("width*height*4: " + width * height * 4);
697         }
698 
699         final PixelParser pixelParser = ic.pixelParser;
700         final ImageBuilder imageBuilder = new ImageBuilder(width, height, true);
701         pixelParser.processImage(imageBuilder);
702 
703         return imageBuilder.getBufferedImage();
704 
705     }
706 
707     @Override
708     public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
709             throws ImageWriteException, IOException {
710         // make copy of params; we'll clear keys as we consume them.
711         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
712 
713         PixelDensity pixelDensity = null;
714 
715         // clear format key.
716         if (params.containsKey(PARAM_KEY_FORMAT)) {
717             params.remove(PARAM_KEY_FORMAT);
718         }
719         if (params.containsKey(PARAM_KEY_PIXEL_DENSITY)) {
720             pixelDensity = (PixelDensity) params.remove(PARAM_KEY_PIXEL_DENSITY);
721         }
722         if (!params.isEmpty()) {
723             final Object firstKey = params.keySet().iterator().next();
724             throw new ImageWriteException("Unknown parameter: " + firstKey);
725         }
726 
727         final SimplePalette palette = new PaletteFactory().makeExactRgbPaletteSimple(
728                 src, 256);
729 
730         BmpWriter writer;
731         if (palette == null) {
732             writer = new BmpWriterRgb();
733         } else {
734             writer = new BmpWriterPalette(palette);
735         }
736 
737         final byte[] imagedata = writer.getImageData(src);
738         final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN);
739 
740         // write BitmapFileHeader
741         os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap
742         os.write(0x4d); // M
743 
744         final int filesize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header size
745                 4 * writer.getPaletteSize() + // palette size in bytes
746                 imagedata.length;
747         bos.write4Bytes(filesize);
748 
749         bos.write4Bytes(0); // reserved
750         bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE
751                 + 4 * writer.getPaletteSize()); // Bitmap Data Offset
752 
753         final int width = src.getWidth();
754         final int height = src.getHeight();
755 
756         // write BitmapInfoHeader
757         bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size
758         bos.write4Bytes(width); // width
759         bos.write4Bytes(height); // height
760         bos.write2Bytes(1); // Number of Planes
761         bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel
762 
763         bos.write4Bytes(BI_RGB); // Compression
764         bos.write4Bytes(imagedata.length); // Bitmap Data Size
765         bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.horizontalDensityMetres()) : 0); // HResolution
766         bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.verticalDensityMetres()) : 0); // VResolution
767         if (palette == null) {
768             bos.write4Bytes(0); // Colors
769         } else {
770             bos.write4Bytes(palette.length()); // Colors
771         }
772         bos.write4Bytes(0); // Important Colors
773         // bos.write_4_bytes(0); // Compression
774 
775         // write Palette
776         writer.writePalette(bos);
777         // write Image Data
778         bos.write(imagedata);
779     }
780 
781     /**
782      * Extracts embedded XML metadata as XML string.
783      * <p>
784      * 
785      * @param byteSource
786      *            File containing image data.
787      * @param params
788      *            Map of optional parameters, defined in ImagingConstants.
789      * @return Xmp Xml as String, if present. Otherwise, returns null.
790      */
791     @Override
792     public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params)
793             throws ImageReadException, IOException {
794         return null;
795     }
796 
797 }