File: BmpReader.java

package info (click to toggle)
libmetadata-extractor-java 2.11.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, buster, forky, sid, trixie
  • size: 6,416 kB
  • sloc: java: 35,343; xml: 200; sh: 11; makefile: 2
file content (401 lines) | stat: -rw-r--r-- 20,338 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/*
 * Copyright 2002-2017 Drew Noakes
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 *
 * More information about this project is available at:
 *
 *    https://drewnoakes.com/code/exif/
 *    https://github.com/drewnoakes/metadata-extractor
 */
package com.drew.metadata.bmp;

import com.drew.lang.ByteArrayReader;
import com.drew.lang.Charsets;
import com.drew.lang.SequentialReader;
import com.drew.lang.annotations.NotNull;
import com.drew.metadata.Directory;
import com.drew.metadata.ErrorDirectory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.bmp.BmpHeaderDirectory.ColorSpaceType;
import com.drew.metadata.icc.IccReader;

import java.io.IOException;

/**
 * Reader for Windows and OS/2 bitmap files.
 * <p>
 * References:
 * <ul>
 *   <li><a href="https://web.archive.org/web/20170302205626/http://fileformats.archiveteam.org/wiki/BMP">Fileformats Wiki BMP overview</a></li>
 *   <li><a href="http://web.archive.org/web/20170303000822/http://netghost.narod.ru/gff/graphics/summary/micbmp.htm">Graphics File Formats encyclopedia Windows bitmap description</a></li>
 *   <li><a href="https://web.archive.org/web/20170302205330/http://netghost.narod.ru/gff/graphics/summary/os2bmp.htm">Graphics File Formats encyclopedia OS/2 bitmap description</a></li>
 *   <li><a href="https://web.archive.org/web/20170302205457/http://www.fileformat.info/format/os2bmp/spec/902d5c253f2a43ada39c2b81034f27fd/view.htm">OS/2 bitmap specification</a></li>
 *   <li><a href="https://msdn.microsoft.com/en-us/library/dd183392(v=vs.85).aspx">Microsoft Bitmap Structures</a></li>
 * </ul>
 *
 * @author Drew Noakes https://drewnoakes.com
 * @author Nadahar
 */
public class BmpReader
{
    // Possible "magic bytes" indicating bitmap type:
    /**
     * "BM" - Windows or OS/2 bitmap
     */
    public static final int BITMAP = 0x4D42;
    /**
     * "BA" - OS/2 Bitmap array (multiple bitmaps)
     */
    public static final int OS2_BITMAP_ARRAY = 0x4142;
    /**
     * "IC" - OS/2 Icon
     */
    public static final int OS2_ICON = 0x4349;
    /**
     * "CI" - OS/2 Color icon
     */
    public static final int OS2_COLOR_ICON = 0x4943;
    /**
     * "CP" - OS/2 Color pointer
     */
    public static final int OS2_COLOR_POINTER = 0x5043;
    /**
     * "PT" - OS/2 Pointer
     */
    public static final int OS2_POINTER = 0x5450;

    public void extract(@NotNull final SequentialReader reader, final @NotNull Metadata metadata)
    {
        reader.setMotorolaByteOrder(false);

        // BITMAP INFORMATION HEADER
        //
        // The first four bytes of the header give the size, which is a discriminator of the actual header format.
        // See this for more information http://en.wikipedia.org/wiki/BMP_file_format

        readFileHeader(reader, metadata, true);
    }

    protected void readFileHeader(@NotNull final SequentialReader reader, final @NotNull Metadata metadata, boolean allowArray) {
        /*
         * There are two possible headers a file can start with. If the magic
         * number is OS/2 Bitmap Array (0x4142) the OS/2 Bitmap Array Header
         * will follow. In all other cases the file header will follow, which
         * is identical for Windows and OS/2.
         *
         * File header:
         *
         *    WORD   FileType;      - File type identifier
         *    DWORD  FileSize;      - Size of the file in bytes
         *    WORD   XHotSpot;      - X coordinate of hotspot for pointers
         *    WORD   YHotSpot;      - Y coordinate of hotspot for pointers
         *    DWORD  BitmapOffset;  - Starting position of image data in bytes
         *
         * OS/2 Bitmap Array Header:
         *
         *     WORD  Type;          - Header type, always 4142h ("BA")
         *     DWORD Size;          - Size of this header
         *     DWORD OffsetToNext;  - Offset to next OS2BMPARRAYFILEHEADER
         *     WORD  ScreenWidth;   - Width of the image display in pixels
         *     WORD  ScreenHeight;  - Height of the image display in pixels
         *
         */

        final int magicNumber;
        try {
            magicNumber = reader.getUInt16();
        } catch (IOException e) {
            metadata.addDirectory(new ErrorDirectory("Couldn't determine bitmap type: " + e.getMessage()));
            return;
        }

        Directory directory = null;
        try {
            switch (magicNumber) {
                case OS2_BITMAP_ARRAY:
                    if (!allowArray) {
                        addError("Invalid bitmap file - nested arrays not allowed", metadata);
                        return;
                    }
                    reader.skip(4); // Size
                    long nextHeaderOffset = reader.getUInt32();
                    reader.skip(2 + 2); // Screen Resolution
                    readFileHeader(reader, metadata, false);
                    if (nextHeaderOffset == 0) {
                        return; // No more bitmaps
                    }
                    if (reader.getPosition() > nextHeaderOffset) {
                        addError("Invalid next header offset", metadata);
                        return;
                    }
                    reader.skip(nextHeaderOffset - reader.getPosition());
                    readFileHeader(reader, metadata, true);
                    break;
                case BITMAP:
                case OS2_ICON:
                case OS2_COLOR_ICON:
                case OS2_COLOR_POINTER:
                case OS2_POINTER:
                    directory = new BmpHeaderDirectory();
                    metadata.addDirectory(directory);
                    directory.setInt(BmpHeaderDirectory.TAG_BITMAP_TYPE, magicNumber);
                    // skip past the rest of the file header
                    reader.skip(4 + 2 + 2 + 4);
                    readBitmapHeader(reader, (BmpHeaderDirectory) directory, metadata);
                    break;
                default:
                    metadata.addDirectory(new ErrorDirectory("Invalid BMP magic number 0x" + Integer.toHexString(magicNumber)));
                    return;
            }
        } catch (IOException e) {
            if (directory == null) {
                 addError("Unable to read BMP file header", metadata);
            } else {
                directory.addError("Unable to read BMP file header");
            }
        }
    }

    protected void readBitmapHeader(@NotNull final SequentialReader reader, final @NotNull BmpHeaderDirectory directory, final @NotNull Metadata metadata) {
        /*
         * BITMAPCOREHEADER (12 bytes):
         *
         *    DWORD Size              - Size of this header in bytes
         *    SHORT Width             - Image width in pixels
         *    SHORT Height            - Image height in pixels
         *    WORD  Planes            - Number of color planes
         *    WORD  BitsPerPixel      - Number of bits per pixel
         *
         * OS21XBITMAPHEADER (12 bytes):
         *
         *    DWORD  Size             - Size of this structure in bytes
         *    WORD   Width            - Bitmap width in pixels
         *    WORD   Height           - Bitmap height in pixel
         *      WORD   NumPlanes        - Number of bit planes (color depth)
         *    WORD   BitsPerPixel     - Number of bits per pixel per plane
         *
         * OS22XBITMAPHEADER (16/64 bytes):
         *
         *    DWORD  Size             - Size of this structure in bytes
         *    DWORD  Width            - Bitmap width in pixels
         *    DWORD  Height           - Bitmap height in pixel
         *      WORD   NumPlanes        - Number of bit planes (color depth)
         *    WORD   BitsPerPixel     - Number of bits per pixel per plane
         *
         *    - Short version ends here -
         *
         *    DWORD  Compression      - Bitmap compression scheme
         *    DWORD  ImageDataSize    - Size of bitmap data in bytes
         *    DWORD  XResolution      - X resolution of display device
         *    DWORD  YResolution      - Y resolution of display device
         *    DWORD  ColorsUsed       - Number of color table indices used
         *    DWORD  ColorsImportant  - Number of important color indices
         *    WORD   Units            - Type of units used to measure resolution
         *    WORD   Reserved         - Pad structure to 4-byte boundary
         *    WORD   Recording        - Recording algorithm
         *    WORD   Rendering        - Halftoning algorithm used
         *    DWORD  Size1            - Reserved for halftoning algorithm use
         *    DWORD  Size2            - Reserved for halftoning algorithm use
         *    DWORD  ColorEncoding    - Color model used in bitmap
         *    DWORD  Identifier       - Reserved for application use
         *
         * BITMAPINFOHEADER (40 bytes), BITMAPV2INFOHEADER (52 bytes), BITMAPV3INFOHEADER (56 bytes),
         * BITMAPV4HEADER (108 bytes) and BITMAPV5HEADER (124 bytes):
         *
         *    DWORD Size              - Size of this header in bytes
         *    LONG  Width             - Image width in pixels
         *    LONG  Height            - Image height in pixels
         *    WORD  Planes            - Number of color planes
         *    WORD  BitsPerPixel      - Number of bits per pixel
         *    DWORD Compression       - Compression methods used
         *    DWORD SizeOfBitmap      - Size of bitmap in bytes
         *    LONG  HorzResolution    - Horizontal resolution in pixels per meter
         *    LONG  VertResolution    - Vertical resolution in pixels per meter
         *    DWORD ColorsUsed        - Number of colors in the image
         *    DWORD ColorsImportant   - Minimum number of important colors
         *
         *    - BITMAPINFOHEADER ends here -
         *
         *    DWORD RedMask           - Mask identifying bits of red component
         *    DWORD GreenMask         - Mask identifying bits of green component
         *    DWORD BlueMask          - Mask identifying bits of blue component
         *
         *    - BITMAPV2INFOHEADER ends here -
         *
         *    DWORD AlphaMask         - Mask identifying bits of alpha component
         *
         *    - BITMAPV3INFOHEADER ends here -
         *
         *    DWORD CSType            - Color space type
         *    LONG  RedX              - X coordinate of red endpoint
         *    LONG  RedY              - Y coordinate of red endpoint
         *    LONG  RedZ              - Z coordinate of red endpoint
         *    LONG  GreenX            - X coordinate of green endpoint
         *    LONG  GreenY            - Y coordinate of green endpoint
         *    LONG  GreenZ            - Z coordinate of green endpoint
         *    LONG  BlueX             - X coordinate of blue endpoint
         *    LONG  BlueY             - Y coordinate of blue endpoint
         *    LONG  BlueZ             - Z coordinate of blue endpoint
         *    DWORD GammaRed          - Gamma red coordinate scale value
         *    DWORD GammaGreen        - Gamma green coordinate scale value
         *    DWORD GammaBlue         - Gamma blue coordinate scale value
         *
         *    - BITMAPV4HEADER ends here -
         *
         *    DWORD Intent            - Rendering intent for bitmap
         *    DWORD ProfileData       - Offset of the profile data relative to BITMAPV5HEADER
         *    DWORD ProfileSize       - Size, in bytes, of embedded profile data
         *    DWORD Reserved          - Shall be zero
         *
         */

        try {
            int bitmapType = directory.getInt(BmpHeaderDirectory.TAG_BITMAP_TYPE);
            long headerOffset = reader.getPosition();
            int headerSize = reader.getInt32();

            directory.setInt(BmpHeaderDirectory.TAG_HEADER_SIZE, headerSize);

            /*
             * Known header type sizes:
             *
             *  12 - BITMAPCOREHEADER or OS21XBITMAPHEADER
             *  16 - OS22XBITMAPHEADER (short)
             *  40 - BITMAPINFOHEADER
             *  52 - BITMAPV2INFOHEADER
             *  56 - BITMAPV3INFOHEADER
             *  64 - OS22XBITMAPHEADER (full)
             * 108 - BITMAPV4HEADER
             * 124 - BITMAPV5HEADER
             *
             */

            if (headerSize == 12 && bitmapType == BITMAP) { //BITMAPCOREHEADER
                /*
                 * There's no way to tell BITMAPCOREHEADER and OS21XBITMAPHEADER
                 * apart for the "standard" bitmap type. The difference is only
                 * that BITMAPCOREHEADER has signed width and height while
                 * in OS21XBITMAPHEADER they are unsigned. Since BITMAPCOREHEADER,
                 * the Windows version, is most common, read them as signed.
                 */
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_WIDTH, reader.getInt16());
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getInt16());
                directory.setInt(BmpHeaderDirectory.TAG_COLOUR_PLANES, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16());
            } else if (headerSize == 12) { // OS21XBITMAPHEADER
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_WIDTH, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_COLOUR_PLANES, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16());
            } else if (headerSize == 16 || headerSize == 64) { // OS22XBITMAPHEADER
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_WIDTH, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_COLOUR_PLANES, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16());
                if (headerSize > 16) {
                    directory.setInt(BmpHeaderDirectory.TAG_COMPRESSION, reader.getInt32());
                    reader.skip(4); // skip the pixel data length
                    directory.setInt(BmpHeaderDirectory.TAG_X_PIXELS_PER_METER, reader.getInt32());
                    directory.setInt(BmpHeaderDirectory.TAG_Y_PIXELS_PER_METER, reader.getInt32());
                    directory.setInt(BmpHeaderDirectory.TAG_PALETTE_COLOUR_COUNT, reader.getInt32());
                    directory.setInt(BmpHeaderDirectory.TAG_IMPORTANT_COLOUR_COUNT, reader.getInt32());
                    reader.skip(
                        2 + // Skip Units, can only be 0 (pixels per meter)
                        2 + // Skip padding
                        2   // Skip Recording, can only be 0 (left to right, bottom to top)
                    );
                    directory.setInt(BmpHeaderDirectory.TAG_RENDERING, reader.getUInt16());
                    reader.skip(4 + 4); // Skip Size1 and Size2
                    directory.setInt(BmpHeaderDirectory.TAG_COLOR_ENCODING, reader.getInt32());
                    reader.skip(4); // Skip Identifier
                }
            } else if (
                headerSize == 40 || headerSize == 52 || headerSize == 56 ||
                headerSize == 108 || headerSize == 124
            ) { // BITMAPINFOHEADER V1-5
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_WIDTH, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_COLOUR_PLANES, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16());
                directory.setInt(BmpHeaderDirectory.TAG_COMPRESSION, reader.getInt32());
                // skip the pixel data length
                reader.skip(4);
                directory.setInt(BmpHeaderDirectory.TAG_X_PIXELS_PER_METER, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_Y_PIXELS_PER_METER, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_PALETTE_COLOUR_COUNT, reader.getInt32());
                directory.setInt(BmpHeaderDirectory.TAG_IMPORTANT_COLOUR_COUNT, reader.getInt32());
                if (headerSize == 40) { // BITMAPINFOHEADER end
                    return;
                }
                directory.setLong(BmpHeaderDirectory.TAG_RED_MASK, reader.getUInt32());
                directory.setLong(BmpHeaderDirectory.TAG_GREEN_MASK, reader.getUInt32());
                directory.setLong(BmpHeaderDirectory.TAG_BLUE_MASK, reader.getUInt32());
                if (headerSize == 52) { // BITMAPV2INFOHEADER end
                    return;
                }
                directory.setLong(BmpHeaderDirectory.TAG_ALPHA_MASK, reader.getUInt32());
                if (headerSize == 56) { // BITMAPV3INFOHEADER end
                    return;
                }
                long csType = reader.getUInt32();
                directory.setLong(BmpHeaderDirectory.TAG_COLOR_SPACE_TYPE, csType);
                reader.skip(36); // Skip color endpoint coordinates
                directory.setLong(BmpHeaderDirectory.TAG_GAMMA_RED, reader.getUInt32());
                directory.setLong(BmpHeaderDirectory.TAG_GAMMA_GREEN, reader.getUInt32());
                directory.setLong(BmpHeaderDirectory.TAG_GAMMA_BLUE, reader.getUInt32());
                if (headerSize == 108) { // BITMAPV4HEADER end
                    return;
                }
                directory.setInt(BmpHeaderDirectory.TAG_INTENT, reader.getInt32());
                if (csType == ColorSpaceType.PROFILE_EMBEDDED.getValue() || csType == ColorSpaceType.PROFILE_LINKED.getValue()) {
                    long profileOffset = reader.getUInt32();
                    int profileSize = reader.getInt32();
                    if (reader.getPosition() > headerOffset + profileOffset) {
                        directory.addError("Invalid profile data offset 0x" + Long.toHexString(headerOffset + profileOffset));
                        return;
                    }
                    reader.skip(headerOffset + profileOffset - reader.getPosition());
                    if (csType == ColorSpaceType.PROFILE_LINKED.getValue()) {
                        directory.setString(BmpHeaderDirectory.TAG_LINKED_PROFILE, reader.getNullTerminatedString(profileSize, Charsets.WINDOWS_1252));
                    } else {
                        ByteArrayReader randomAccessReader = new ByteArrayReader(reader.getBytes(profileSize));
                        new IccReader().extract(randomAccessReader, metadata, directory);
                    }
                } else {
                    reader.skip(
                        4 + // Skip ProfileData offset
                        4 + // Skip ProfileSize
                        4   // Skip Reserved
                    );
                }
            } else {
                directory.addError("Unexpected DIB header size: " + headerSize);
            }
        } catch (IOException e) {
            directory.addError("Unable to read BMP header");
        } catch (MetadataException e) {
            directory.addError("Internal error");
        }
    }

    protected void addError(@NotNull String errorMessage, final @NotNull Metadata metadata) {
        ErrorDirectory directory = metadata.getFirstDirectoryOfType(ErrorDirectory.class);
        if (directory == null) {
            metadata.addDirectory(new ErrorDirectory(errorMessage));
        } else {
            directory.addError(errorMessage);
        }
    }
}