package com.bumptech.glide.load.resource.bitmap;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.JPEG;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG_A;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.UNKNOWN;
import android.util.Log;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.util.Preconditions;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
/**
* A class for parsing the exif orientation and other data from an image header.
*/
public final class DefaultImageHeaderParser implements ImageHeaderParser {
// Due to https://code.google.com/p/android/issues/detail?id=97751.
// TAG needs to be under 23 chars, so "Default" > "Dflt".
private static final String TAG = "DfltImageHeaderParser";
private static final int GIF_HEADER = 0x474946;
private static final int PNG_HEADER = 0x89504E47;
static final int EXIF_MAGIC_NUMBER = 0xFFD8;
// "MM".
private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D;
// "II".
private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949;
static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES =
JPEG_EXIF_SEGMENT_PREAMBLE.getBytes(Charset.forName("UTF-8"));
private static final int SEGMENT_SOS = 0xDA;
private static final int MARKER_EOI = 0xD9;
static final int SEGMENT_START_ID = 0xFF;
static final int EXIF_SEGMENT_TYPE = 0xE1;
private static final int ORIENTATION_TAG_TYPE = 0x0112;
private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
// WebP-related
// "RIFF"
private static final int RIFF_HEADER = 0x52494646;
// "WEBP"
private static final int WEBP_HEADER = 0x57454250;
// "VP8" null.
private static final int VP8_HEADER = 0x56503800;
private static final int VP8_HEADER_MASK = 0xFFFFFF00;
private static final int VP8_HEADER_TYPE_MASK = 0x000000FF;
// 'X'
private static final int VP8_HEADER_TYPE_EXTENDED = 0x00000058;
// 'L'
private static final int VP8_HEADER_TYPE_LOSSLESS = 0x0000004C;
private static final int WEBP_EXTENDED_ALPHA_FLAG = 1 << 4;
private static final int WEBP_LOSSLESS_ALPHA_FLAG = 1 << 3;
@Override
public ImageType getType(InputStream is) throws IOException {
return getType(new StreamReader(Preconditions.checkNotNull(is)));
}
@Override
public ImageType getType(ByteBuffer byteBuffer) throws IOException {
return getType(new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)));
}
@Override
public int getOrientation(InputStream is, ArrayPool byteArrayPool) throws IOException {
return getOrientation(new StreamReader(Preconditions.checkNotNull(is)),
Preconditions.checkNotNull(byteArrayPool));
}
@Override
public int getOrientation(ByteBuffer byteBuffer, ArrayPool byteArrayPool) throws IOException {
return getOrientation(new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)),
Preconditions.checkNotNull(byteArrayPool));
}
private ImageType getType(Reader reader) throws IOException {
int firstTwoBytes = reader.getUInt16();
// JPEG.
if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
return JPEG;
}
final int firstFourBytes = firstTwoBytes << 16 & 0xFFFF0000 | reader.getUInt16() & 0xFFFF;
// PNG.
if (firstFourBytes == PNG_HEADER) {
// See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha
// -color-type
reader.skip(25 - 4);
int alpha = reader.getByte();
// A RGB indexed PNG can also have transparency. Better safe than sorry!
return alpha >= 3 ? PNG_A : PNG;
}
// GIF from first 3 bytes.
if (firstFourBytes >> 8 == GIF_HEADER) {
return GIF;
}
// WebP (reads up to 21 bytes). See https://developers.google.com/speed/webp/docs/riff_container
// for details.
if (firstFourBytes != RIFF_HEADER) {
return UNKNOWN;
}
// Bytes 4 - 7 contain length information. Skip these.
reader.skip(4);
final int thirdFourBytes = reader.getUInt16() << 16 & 0xFFFF0000 | reader.getUInt16() & 0xFFFF;
if (thirdFourBytes != WEBP_HEADER) {
return UNKNOWN;
}
final int fourthFourBytes = reader.getUInt16() << 16 & 0xFFFF0000 | reader.getUInt16() & 0xFFFF;
if ((fourthFourBytes & VP8_HEADER_MASK) != VP8_HEADER) {
return UNKNOWN;
}
if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_EXTENDED) {
// Skip some more length bytes and check for transparency/alpha flag.
reader.skip(4);
return (reader.getByte() & WEBP_EXTENDED_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
}
if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_LOSSLESS) {
// See chromium.googlesource.com/webm/libwebp/+/master/doc/webp-lossless-bitstream-spec.txt
// for more info.
reader.skip(4);
return (reader.getByte() & WEBP_LOSSLESS_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
}
return ImageType.WEBP;
}
/**
* Parse the orientation from the image header. If it doesn't handle this image type (or this is
* not an image) it will return a default value rather than throwing an exception.
*
* @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't
* contain an orientation
* @throws IOException
*/
private int getOrientation(Reader reader, ArrayPool byteArrayPool) throws IOException {
final int magicNumber = reader.getUInt16();
if (!handles(magicNumber)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Parser doesn't handle magic number: " + magicNumber);
}
return UNKNOWN_ORIENTATION;
} else {
int exifSegmentLength = moveToExifSegmentAndGetLength(reader);
if (exifSegmentLength == -1) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to parse exif segment length, or exif segment not found");
}
return UNKNOWN_ORIENTATION;
}
byte[] exifData = byteArrayPool.get(exifSegmentLength, byte[].class);
try {
return parseExifSegment(reader, exifData, exifSegmentLength);
} finally {
byteArrayPool.put(exifData, byte[].class);
}
}
}
private int parseExifSegment(Reader reader, byte[] tempArray, int exifSegmentLength)
throws IOException {
int read = reader.read(tempArray, exifSegmentLength);
if (read != exifSegmentLength) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to read exif segment data"
+ ", length: " + exifSegmentLength
+ ", actually read: " + read);
}
return UNKNOWN_ORIENTATION;
}
boolean hasJpegExifPreamble = hasJpegExifPreamble(tempArray, exifSegmentLength);
if (hasJpegExifPreamble) {
return parseExifSegment(new RandomAccessReader(tempArray, exifSegmentLength));
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Missing jpeg exif preamble");
}
return UNKNOWN_ORIENTATION;
}
}
private boolean hasJpegExifPreamble(byte[] exifData, int exifSegmentLength) {
boolean result =
exifData != null && exifSegmentLength > JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length;
if (result) {
for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) {
if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) {
result = false;
break;
}
}
}
return result;
}
/**
* Moves reader to the start of the exif segment and returns the length of the exif segment or
* {@code -1} if no exif segment is found.
*/
private int moveToExifSegmentAndGetLength(Reader reader) throws IOException {
short segmentId, segmentType;
int segmentLength;
while (true) {
segmentId = reader.getUInt8();
if (segmentId != SEGMENT_START_ID) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unknown segmentId=" + segmentId);
}
return -1;
}
segmentType = reader.getUInt8();
if (segmentType == SEGMENT_SOS) {
return -1;
} else if (segmentType == MARKER_EOI) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Found MARKER_EOI in exif segment");
}
return -1;
}
// Segment length includes bytes for segment length.
segmentLength = reader.getUInt16() - 2;
if (segmentType != EXIF_SEGMENT_TYPE) {
long skipped = reader.skip(segmentLength);
if (skipped != segmentLength) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to skip enough data"
+ ", type: " + segmentType
+ ", wanted to skip: " + segmentLength
+ ", but actually skipped: " + skipped);
}
return -1;
}
} else {
return segmentLength;
}
}
}
private static int parseExifSegment(RandomAccessReader segmentData) {
final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length();
short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize);
final ByteOrder byteOrder;
if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) {
byteOrder = ByteOrder.BIG_ENDIAN;
} else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) {
byteOrder = ByteOrder.LITTLE_ENDIAN;
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier);
}
byteOrder = ByteOrder.BIG_ENDIAN;
}
segmentData.order(byteOrder);
int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize;
int tagCount = segmentData.getInt16(firstIfdOffset);
int tagOffset, tagType, formatCode, componentCount;
for (int i = 0; i < tagCount; i++) {
tagOffset = calcTagOffset(firstIfdOffset, i);
tagType = segmentData.getInt16(tagOffset);
// We only want orientation.
if (tagType != ORIENTATION_TAG_TYPE) {
continue;
}
formatCode = segmentData.getInt16(tagOffset + 2);
// 12 is max format code.
if (formatCode < 1 || formatCode > 12) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Got invalid format code = " + formatCode);
}
continue;
}
componentCount = segmentData.getInt32(tagOffset + 4);
if (componentCount < 0) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Negative tiff component count");
}
continue;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode=" + formatCode
+ " componentCount=" + componentCount);
}
final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode];
if (byteCount > 4) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode);
}
continue;
}
final int tagValueOffset = tagOffset + 8;
if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType);
}
continue;
}
if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType);
}
continue;
}
//assume componentCount == 1 && fmtCode == 3
return segmentData.getInt16(tagValueOffset);
}
return -1;
}
private static int calcTagOffset(int ifdOffset, int tagIndex) {
return ifdOffset + 2 + 12 * tagIndex;
}
private static boolean handles(int imageMagicNumber) {
return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER
|| imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER
|| imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER;
}
private static final class RandomAccessReader {
private final ByteBuffer data;
RandomAccessReader(byte[] data, int length) {
this.data = (ByteBuffer) ByteBuffer.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
.limit(length);
}
void order(ByteOrder byteOrder) {
this.data.order(byteOrder);
}
int length() {
return data.remaining();
}
int getInt32(int offset) {
return isAvailable(offset, 4) ? data.getInt(offset) : -1;
}
short getInt16(int offset) {
return isAvailable(offset, 2) ? data.getShort(offset) : -1;
}
private boolean isAvailable(int offset, int byteSize) {
return data.remaining() - offset >= byteSize;
}
}
private interface Reader {
int getUInt16() throws IOException;
short getUInt8() throws IOException;
long skip(long total) throws IOException;
int read(byte[] buffer, int byteCount) throws IOException;
int getByte() throws IOException;
}
private static final class ByteBufferReader implements Reader {
private final ByteBuffer byteBuffer;
ByteBufferReader(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
byteBuffer.order(ByteOrder.BIG_ENDIAN);
}
@Override
public int getUInt16() throws IOException {
return (getByte() << 8 & 0xFF00) | (getByte() & 0xFF);
}
@Override
public short getUInt8() throws IOException {
return (short) (getByte() & 0xFF);
}
@Override
public long skip(long total) throws IOException {
int toSkip = (int) Math.min(byteBuffer.remaining(), total);
byteBuffer.position(byteBuffer.position() + toSkip);
return toSkip;
}
@Override
public int read(byte[] buffer, int byteCount) throws IOException {
int toRead = Math.min(byteCount, byteBuffer.remaining());
if (toRead == 0) {
return -1;
}
byteBuffer.get(buffer, 0 /*dstOffset*/, toRead);
return toRead;
}
@Override
public int getByte() throws IOException {
if (byteBuffer.remaining() < 1) {
return -1;
}
return byteBuffer.get();
}
}
private static final class StreamReader implements Reader {
private final InputStream is;
// Motorola / big endian byte order.
StreamReader(InputStream is) {
this.is = is;
}
@Override
public int getUInt16() throws IOException {
return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);
}
@Override
public short getUInt8() throws IOException {
return (short) (is.read() & 0xFF);
}
@Override
public long skip(long total) throws IOException {
if (total < 0) {
return 0;
}
long toSkip = total;
while (toSkip > 0) {
long skipped = is.skip(toSkip);
if (skipped > 0) {
toSkip -= skipped;
} else {
// Skip has no specific contract as to what happens when you reach the end of
// the stream. To differentiate between temporarily not having more data and
// having finished the stream, we read a single byte when we fail to skip any
// amount of data.
int testEofByte = is.read();
if (testEofByte == -1) {
break;
} else {
toSkip--;
}
}
}
return total - toSkip;
}
@Override
public int read(byte[] buffer, int byteCount) throws IOException {
int toRead = byteCount;
int read;
while (toRead > 0 && ((read = is.read(buffer, byteCount - toRead, toRead)) != -1)) {
toRead -= read;
}
return byteCount - toRead;
}
@Override
public int getByte() throws IOException {
return is.read();
}
}
}