com.drew.metadata.bmp.BmpReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metadata-extractor Show documentation
Show all versions of metadata-extractor Show documentation
This is a fork of com.drewnoakes' metadata-extractor that relocates com.adobe.internal to com.adobe.
Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image and video files.
/*
* Copyright 2002-2019 Drew Noakes and contributors
*
* 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.
*
* References:
*
*
* @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);
}
}
}