Skip to content

Commit

Permalink
Obey exif orientations in image headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam Judd committed Aug 14, 2013
1 parent acf9ecc commit d7efd9c
Show file tree
Hide file tree
Showing 3 changed files with 369 additions and 4 deletions.
44 changes: 40 additions & 4 deletions library/src/com/bumptech/glide/resize/load/Downsampler.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.os.Build;
import com.bumptech.glide.resize.RecyclableBufferedInputStream;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.Log;

import java.io.IOException;

Expand Down Expand Up @@ -68,11 +69,39 @@ protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeig
* @return A new bitmap containing the image from the given InputStream, or recycle if recycle is not null
*/
public Bitmap downsample(RecyclableBufferedInputStream bis, BitmapFactory.Options options, BitmapPool pool, int outWidth, int outHeight) {
bis.mark(MARK_POSITION);
int orientation = 0;
try {
orientation = new ExifOrientationParser(bis).getOrientation();//ImageResizer.getOrientation(bis);
} catch (IOException e) {
e.printStackTrace();
}
try {
bis.reset();
} catch (IOException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
Log.d("EXIF: got orientation=" + orientation);


final int[] inDimens = getDimensions(bis, options);
final int inWidth = inDimens[0];
final int inHeight = inDimens[1];
int inWidth = inDimens[0];
int inHeight = inDimens[1];

final int degreesToRotate = ImageResizer.getExifOrientationDegrees(orientation);
final int sampleSize;
if (degreesToRotate == 90 || degreesToRotate == 270) {
final int temp = inWidth;
inWidth = inHeight;
inHeight = temp;

final int sampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);
sampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);

inHeight = inWidth;
inWidth = temp;
} else {
sampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);
}

//sample sizes <= 1 do nothing
if (sampleSize > 1) {
Expand All @@ -81,7 +110,14 @@ public Bitmap downsample(RecyclableBufferedInputStream bis, BitmapFactory.Option
setInBitmap(options, pool.get(inWidth, inHeight));
}

return decodeStream(bis, options);
final Bitmap downsampled = decodeStream(bis, options);
final Bitmap rotated = ImageResizer.rotateImageExif(downsampled, pool, orientation);

if (downsampled != rotated) {
pool.put(downsampled);
}

return rotated;
}

/**
Expand Down
232 changes: 232 additions & 0 deletions library/src/com/bumptech/glide/resize/load/ExifOrientationParser.java
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);
}
}
}

Loading

0 comments on commit d7efd9c

Please sign in to comment.