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