blob: cd787817df71803ef44b3dd357b2c5e0d9b22505 [file] [log] [blame]
package com.bumptech.glide.load.resource.bitmap;
import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.GIF;
import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.JPEG;
import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG;
import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG_A;
import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.UNKNOWN;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* A class for parsing the exif orientation and other data from an image header.
*/
public class ImageHeaderParser {
private static final String TAG = "ImageHeaderParser";
/**
* The format of the image data including whether or not the image may include transparent pixels.
*/
public static enum ImageType {
/** GIF type. */
GIF(true),
/** JPG type. */
JPEG(false),
/** PNG type with alpha. */
PNG_A(true),
/** PNG type without alpha. */
PNG(false),
/** Unrecognized type. */
UNKNOWN(false);
private final boolean hasAlpha;
ImageType(boolean hasAlpha) {
this.hasAlpha = hasAlpha;
}
public boolean hasAlpha() {
return hasAlpha;
}
}
private static final int GIF_HEADER = 0x474946;
private static final int PNG_HEADER = 0x89504E47;
private 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;
private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
private static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES;
private static final int SEGMENT_SOS = 0xDA;
private static final int MARKER_EOI = 0xD9;
private static final int SEGMENT_START_ID = 0xFF;
private 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 };
private final StreamReader streamReader;
static {
byte[] bytes = new byte[0];
try {
bytes = JPEG_EXIF_SEGMENT_PREAMBLE.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// Ignore.
}
JPEG_EXIF_SEGMENT_PREAMBLE_BYTES = bytes;
}
public ImageHeaderParser(InputStream is) {
streamReader = new StreamReader(is);
}
// 0xD0A3C68 -> <htm
// 0xCAFEBABE -> <!DOCTYPE...
public boolean hasAlpha() throws IOException {
return getType().hasAlpha();
}
public ImageType getType() throws IOException {
int firstByte = streamReader.getUInt8();
// JPEG.
if (firstByte == EXIF_MAGIC_NUMBER >> 8) {
return JPEG;
}
final int firstTwoBytes = firstByte << 8 & 0xFF00 | streamReader.getUInt8() & 0xFF;
final int firstFourBytes = firstTwoBytes << 16 & 0xFFFF0000 | streamReader.getUInt16() & 0xFFFF;
// PNG.
if (firstFourBytes == PNG_HEADER) {
// See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha-color-type
streamReader.skip(25 - 4);
int alpha = streamReader.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;
}
return UNKNOWN;
}
/**
* 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
*/
public int getOrientation() throws IOException {
final int magicNumber = streamReader.getUInt16();
if (!handles(magicNumber)) {
return -1;
} else {
byte[] exifData = getExifSegment();
boolean hasJpegExifPreamble = exifData != null
&& exifData.length >= JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length;
if (hasJpegExifPreamble) {
for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) {
if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) {
hasJpegExifPreamble = false;
break;
}
}
}
if (hasJpegExifPreamble) {
return parseExifSegment(new RandomAccessReader(exifData));
} else {
return -1;
}
}
}
private byte[] getExifSegment() throws IOException {
short segmentId, segmentType;
int segmentLength;
while (true) {
segmentId = streamReader.getUInt8();
if (segmentId != SEGMENT_START_ID) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unknown segmentId=" + segmentId);
}
return null;
}
segmentType = streamReader.getUInt8();
if (segmentType == SEGMENT_SOS) {
return null;
} else if (segmentType == MARKER_EOI) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Found MARKER_EOI in exif segment");
}
return null;
}
// Segment length includes bytes for segment length.
segmentLength = streamReader.getUInt16() - 2;
if (segmentType != EXIF_SEGMENT_TYPE) {
if (segmentLength != streamReader.skip(segmentLength)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to skip enough data for type=" + segmentType);
}
return null;
}
} else {
byte[] segmentData = new byte[segmentLength];
if (segmentLength != streamReader.read(segmentData)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to read segment data for type=" + segmentType + " length=" + segmentLength);
}
return null;
} else {
return segmentData;
}
}
}
}
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 class RandomAccessReader {
private final ByteBuffer data;
public RandomAccessReader(byte[] data) {
this.data = ByteBuffer.wrap(data);
this.data.order(ByteOrder.BIG_ENDIAN);
}
public void order(ByteOrder byteOrder) {
this.data.order(byteOrder);
}
public int length() {
return data.array().length;
}
public int getInt32(int offset) {
return data.getInt(offset);
}
public short getInt16(int offset) {
return data.getShort(offset);
}
}
private static class StreamReader {
private final InputStream is;
//motorola / big endian byte order
public StreamReader(InputStream is) {
this.is = is;
}
public int getUInt16() throws IOException {
return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);
}
public short getUInt8() throws IOException {
return (short) (is.read() & 0xFF);
}
public long skip(long total) throws IOException {
return is.skip(total);
}
public int read(byte[] buffer) throws IOException {
return is.read(buffer);
}
public int getByte() throws IOException {
return is.read();
}
}
}