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.common.BinaryFunctions.read2Bytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
21  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
22  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
23  
24  import java.awt.Dimension;
25  import java.awt.image.BufferedImage;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.PrintWriter;
31  import java.nio.ByteOrder;
32  import java.util.ArrayList;
33  import java.util.List;
34  import java.util.logging.Level;
35  import java.util.logging.Logger;
36  
37  import org.apache.commons.imaging.AbstractImageParser;
38  import org.apache.commons.imaging.FormatCompliance;
39  import org.apache.commons.imaging.ImageFormat;
40  import org.apache.commons.imaging.ImageFormats;
41  import org.apache.commons.imaging.ImageInfo;
42  import org.apache.commons.imaging.ImagingException;
43  import org.apache.commons.imaging.PixelDensity;
44  import org.apache.commons.imaging.bytesource.ByteSource;
45  import org.apache.commons.imaging.common.BinaryOutputStream;
46  import org.apache.commons.imaging.common.ImageBuilder;
47  import org.apache.commons.imaging.common.ImageMetadata;
48  import org.apache.commons.imaging.palette.PaletteFactory;
49  import org.apache.commons.imaging.palette.SimplePalette;
50  
51  public class BmpImageParser extends AbstractImageParser<BmpImagingParameters> {
52  
53      private static final Logger LOGGER = Logger.getLogger(BmpImageParser.class.getName());
54  
55      private static final String DEFAULT_EXTENSION = ImageFormats.BMP.getDefaultExtension();
56      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.BMP.getExtensions();
57      private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, };
58      private static final int BI_RGB = 0;
59      private static final int BI_RLE4 = 2;
60      private static final int BI_RLE8 = 1;
61      private static final int BI_BITFIELDS = 3;
62      private static final int BITMAP_FILE_HEADER_SIZE = 14;
63      private static final int BITMAP_INFO_HEADER_SIZE = 40;
64  
65      public BmpImageParser() {
66          super(ByteOrder.LITTLE_ENDIAN);
67      }
68  
69      @Override
70      public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
71          pw.println("bmp.dumpImageFile");
72  
73          final ImageInfo imageData = getImageInfo(byteSource, null);
74  
75          imageData.toString(pw, "");
76  
77          pw.println("");
78  
79          return true;
80      }
81  
82      @Override
83      protected String[] getAcceptedExtensions() {
84          return ACCEPTED_EXTENSIONS;
85      }
86  
87      @Override
88      protected ImageFormat[] getAcceptedTypes() {
89          return new ImageFormat[] { ImageFormats.BMP };
90      }
91  
92      private String getBmpTypeDescription(final int identifier1, final int identifier2) {
93          if (identifier1 == 'B' && identifier2 == 'M') {
94              return "Windows 3.1x, 95, NT,";
95          }
96          if (identifier1 == 'B' && identifier2 == 'A') {
97              return "OS/2 Bitmap Array";
98          }
99          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 }