-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Obey exif orientations in image headers
- Loading branch information
Sam Judd
committed
Aug 14, 2013
1 parent
acf9ecc
commit d7efd9c
Showing
3 changed files
with
369 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
232 changes: 232 additions & 0 deletions
232
library/src/com/bumptech/glide/resize/load/ExifOrientationParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
package com.bumptech.glide.resize.load; | ||
|
||
import com.bumptech.glide.util.Log; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.ByteBuffer; | ||
import java.nio.ByteOrder; | ||
|
||
/** | ||
* A class for parsing the exif orientation from an InputStream for an image. Handles jpegs and tiffs. | ||
*/ | ||
public class ExifOrientationParser { | ||
private static final int EXIF_MAGIC_NUMBER = 0xFFD8; | ||
private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D; // "MM" | ||
private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949; // "II" | ||
private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; | ||
|
||
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; | ||
|
||
public ExifOrientationParser(InputStream is) { | ||
streamReader = new StreamReader(is); | ||
} | ||
|
||
/** | ||
* 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(); | ||
if (exifData != null && exifData.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && | ||
new String(exifData, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE)) { | ||
return parseExifSegment(new RandomAccessReader(exifData)); | ||
} else { | ||
Log.d("EXIF: segment data too short or missing jpeg exif segment preamble"); | ||
return -1; | ||
} | ||
} | ||
} | ||
|
||
private byte[] getExifSegment() throws IOException { | ||
short segmentId, segmentType; | ||
int segmentLength; | ||
while (true) { | ||
segmentId = streamReader.getUInt8(); | ||
|
||
if (segmentId != SEGMENT_START_ID) { | ||
Log.d("EXIF: unknown segmentId=" + segmentId); | ||
return null; | ||
} | ||
|
||
segmentType = streamReader.getUInt8(); | ||
|
||
if (segmentType == SEGMENT_SOS) { | ||
Log.d("EXIF: found SEGMENT_SOS"); | ||
return null; | ||
} else if (segmentType == MARKER_EOI) { | ||
Log.d("EXIF: found MARKER_EOI"); | ||
return null; | ||
} | ||
|
||
segmentLength = streamReader.getUInt16() - 2; //segment length includes bytes for segment length | ||
|
||
if (segmentType != EXIF_SEGMENT_TYPE) { | ||
if (segmentLength != streamReader.skip(segmentLength)) { | ||
Log.d("EXIF: unable to skip enough data for type=" + segmentType); | ||
return null; | ||
} | ||
} else { | ||
byte[] segmentData = new byte[segmentLength]; | ||
|
||
if (segmentLength != streamReader.read(segmentData)) { | ||
Log.d("EXIF: unable to read segment data for type=" + segmentType + " length=" + segmentLength); | ||
return null; | ||
} else { | ||
return segmentData; | ||
} | ||
} | ||
} | ||
} | ||
|
||
private 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 { | ||
Log.d("EXIF: 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); | ||
|
||
if (tagType != ORIENTATION_TAG_TYPE) { //we only want orientation | ||
continue; | ||
} | ||
|
||
formatCode = segmentData.getInt16(tagOffset + 2); | ||
|
||
if (formatCode < 1 || formatCode > 12) { //12 is max format code | ||
Log.d("EXIF: got invalid format code = " + formatCode); | ||
continue; | ||
} | ||
|
||
componentCount = segmentData.getInt32(tagOffset + 4); | ||
|
||
if (componentCount < 0) { | ||
Log.d("EXIF: negative tiff component count"); | ||
continue; | ||
} | ||
|
||
Log.d("EXIF: got tagIndex=" + i + " tagType=" + tagType + " formatCode =" + formatCode + " componentCount=" + componentCount); | ||
|
||
final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode]; | ||
|
||
if (byteCount > 4) { | ||
Log.d("EXIF: got byte count > 4, not orientation, continuing, formatCode=" + formatCode); | ||
continue; | ||
} | ||
|
||
final int tagValueOffset = tagOffset + 8; | ||
|
||
if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) { | ||
Log.d("EXIF: illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType); | ||
continue; | ||
} | ||
|
||
if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) { | ||
Log.d("EXIF: 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 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); | ||
} | ||
} | ||
} | ||
|
Oops, something went wrong.