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