提交 ff503990 编写于 作者: S Sam Judd

Insert exif orientation into thumbnail streams.

Fixes #400
上级 f20687d0
package com.bumptech.glide.load.data;
import static com.google.common.truth.Truth.assertThat;
import com.bumptech.glide.load.resource.bitmap.ImageHeaderParser;
import com.bumptech.glide.testutil.TestResourceUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.IOException;
import java.io.InputStream;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, emulateSdk = 18)
public class ExifOrientationStreamTest {
private InputStream openOrientationExample(boolean isLandscape, int item) {
String filePrefix = isLandscape ? "Landscape" : "Portrait";
return TestResourceUtil.openResource(getClass(),
"exif-orientation-examples/" + filePrefix + "_" + item + ".jpg");
}
@Test
public void testIncludesGivenExifOrientation() throws IOException {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
InputStream toWrap = openOrientationExample(true /*isLandscape*/, j + 1);
InputStream wrapped = new ExifOrientationStream(toWrap, i);
ImageHeaderParser parser = new ImageHeaderParser(wrapped);
assertThat(parser.getOrientation()).isEqualTo(i);
toWrap = openOrientationExample(false /*isLandscape*/, j + 1);
wrapped = new ExifOrientationStream(toWrap, i);
parser = new ImageHeaderParser(wrapped);
assertThat(parser.getOrientation()).isEqualTo(i);
}
}
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package com.bumptech.glide.load.data;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
......@@ -38,10 +39,11 @@ public class MediaStoreThumbFetcherTest {
public void testReturnsInputStreamFromThumbnailOpener() throws Exception {
InputStream expected = new ByteArrayInputStream(new byte[0]);
when(harness.thumbnailFetcher.open(eq(Robolectric.application), eq(harness.uri))).thenReturn(expected);
when(harness.thumbnailFetcher.open(eq(Robolectric.application), eq(harness.uri))).thenReturn(
expected);
InputStream result = harness.get().loadData(Priority.LOW);
assertEquals(expected, result);
assertNotNull(result);
}
@Test
......
......@@ -38,14 +38,14 @@ public class ThumbnailStreamOpenerTest {
@Test
public void testReturnsNullIfCursorIsNull() throws FileNotFoundException {
when(harness.query.query(eq(Robolectric.application), eq(harness.uri))).thenReturn(null);
when(harness.query.queryPath(eq(Robolectric.application), eq(harness.uri))).thenReturn(null);
assertNull(harness.get()
.open(Robolectric.application, harness.uri));
}
@Test
public void testReturnsNullIfCursorIsEmpty() throws FileNotFoundException {
when(harness.query.query(eq(Robolectric.application), eq(harness.uri))).thenReturn(
when(harness.query.queryPath(eq(Robolectric.application), eq(harness.uri))).thenReturn(
new MatrixCursor(new String[1]));
assertNull(harness.get()
.open(Robolectric.application, harness.uri));
......@@ -55,7 +55,7 @@ public class ThumbnailStreamOpenerTest {
public void testReturnsNullIfCursorHasEmptyPath() throws FileNotFoundException {
MatrixCursor cursor = new MatrixCursor(new String[1]);
cursor.addRow(new Object[]{ "" });
when(harness.query.query(eq(Robolectric.application), eq(harness.uri))).thenReturn(cursor);
when(harness.query.queryPath(eq(Robolectric.application), eq(harness.uri))).thenReturn(cursor);
assertNull(harness.get()
.open(Robolectric.application, harness.uri));
}
......@@ -93,7 +93,7 @@ public class ThumbnailStreamOpenerTest {
MediaStoreThumbFetcher.VideoThumbnailQuery query = new MediaStoreThumbFetcher.VideoThumbnailQuery();
TestCursor testCursor = new SimpleTestCursor();
Robolectric.shadowOf(Robolectric.application.getContentResolver()).setCursor(queryUri, testCursor);
assertEquals(testCursor, query.query(Robolectric.application, harness.uri));
assertEquals(testCursor, query.queryPath(Robolectric.application, harness.uri));
}
@Test
......@@ -102,7 +102,7 @@ public class ThumbnailStreamOpenerTest {
MediaStoreThumbFetcher.ImageThumbnailQuery query = new MediaStoreThumbFetcher.ImageThumbnailQuery();
TestCursor testCursor = new SimpleTestCursor();
Robolectric.shadowOf(Robolectric.application.getContentResolver()).setCursor(queryUri, testCursor);
assertEquals(testCursor, query.query(Robolectric.application, harness.uri));
assertEquals(testCursor, query.queryPath(Robolectric.application, harness.uri));
}
private static class Harness {
......@@ -114,7 +114,7 @@ public class ThumbnailStreamOpenerTest {
public Harness() {
cursor.addRow(new String[] { file.getAbsolutePath() });
when(query.query(eq(Robolectric.application), eq(uri))).thenReturn(cursor);
when(query.queryPath(eq(Robolectric.application), eq(uri))).thenReturn(cursor);
when(service.get(eq(file.getAbsolutePath()))).thenReturn(file);
when(service.exists(eq(file))).thenReturn(true);
when(service.length(eq(file))).thenReturn(1L);
......
package com.bumptech.glide.load.data;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Adds an exif segment with an orientation attribute to a wrapped {@link InputStream} containing
* image data.
*
* <p>This class assumes that the wrapped stream contains an image format that can contain
* exif information and performs no verification. </p>
*/
public class ExifOrientationStream extends FilterInputStream {
/** Allow two bytes for the file format. */
private static final int SEGMENT_START_POSITION = 2;
private static final byte[] EXIF_SEGMENT = new byte[] {
/** segment start id. */
(byte) 0xFF,
/** segment type. */
(byte) 0xE1,
/** segmentLength. */
0x00,
(byte) 0x1C,
/** exif identifier. */
0x45,
0x78,
0x69,
0x66,
0x00,
0x00,
/** mototorola byte order (big endian). */
(byte) 0x4D,
(byte) 0x4D,
/** filler? */
0x00,
0x00,
/** first id offset. */
0x00,
0x00,
0x00,
0x08,
/** tagCount. */
0x00,
0x01,
/** exif tag type. */
0x01,
0x12,
/** 2 byte format. */
0x00,
0x02,
/** component count. */
0x00,
0x00,
0x00,
0x01,
/** 2 byte orientation value, the first byte of which is always 0. */
0x00,
};
private static final int SEGMENT_LENGTH = EXIF_SEGMENT.length;
private static final int ORIENTATION_POSITION = SEGMENT_LENGTH + SEGMENT_START_POSITION;
private final byte orientation;
private int position;
public ExifOrientationStream(InputStream in, int orientation) {
super(in);
if (orientation < -1 || orientation > 8) {
throw new IllegalArgumentException("Cannot add invalid orientation: " + orientation);
}
this.orientation = (byte) orientation;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void mark(int readlimit) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
final int result;
if (position < SEGMENT_START_POSITION || position > ORIENTATION_POSITION) {
result = super.read();
} else if (position == ORIENTATION_POSITION) {
result = orientation;
} else {
result = EXIF_SEGMENT[position - SEGMENT_START_POSITION] & 0xFF;
}
if (result != -1) {
position++;
}
return result;
}
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
int read;
if (position > ORIENTATION_POSITION) {
read = super.read(buffer, byteOffset, byteCount);
} else if (position == ORIENTATION_POSITION) {
buffer[byteOffset] = orientation;
read = 1;
} else if (position < SEGMENT_START_POSITION) {
read = super.read(buffer, byteOffset, SEGMENT_START_POSITION - position);
} else {
read = Math.min(ORIENTATION_POSITION - position, byteCount);
System.arraycopy(EXIF_SEGMENT, position - SEGMENT_START_POSITION, buffer, byteOffset,
read);
}
if (read > 0) {
position += read;
}
return read;
}
@Override
public long skip(long byteCount) throws IOException {
long skipped = super.skip(byteCount);
if (skipped > 0) {
position += skipped;
}
return skipped;
}
@Override
public void reset() throws IOException {
throw new UnsupportedOperationException();
}
}
......@@ -6,8 +6,10 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.resource.bitmap.ImageHeaderParser;
import java.io.File;
import java.io.FileNotFoundException;
......@@ -22,6 +24,7 @@ import java.io.InputStream;
* {@link android.provider.MediaStore.Video.Thumbnails}.
*/
public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
private static final String TAG = "MediaStoreThumbFetcher";
private static final int MINI_WIDTH = 512;
private static final int MINI_HEIGHT = 384;
private static final ThumbnailStreamOpenerFactory DEFAULT_FACTORY = new ThumbnailStreamOpenerFactory();
......@@ -54,14 +57,35 @@ public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
ThumbnailStreamOpener fetcher = factory.build(mediaStoreUri, width, height);
if (fetcher != null) {
inputStream = fetcher.open(context, mediaStoreUri);
inputStream = openThumbInputStream(fetcher);
}
if (inputStream != null) {
return inputStream;
} else {
return defaultFetcher.loadData(priority);
if (inputStream == null) {
inputStream = defaultFetcher.loadData(priority);
}
return inputStream;
}
private InputStream openThumbInputStream(ThumbnailStreamOpener fetcher) {
InputStream result = null;
try {
result = fetcher.open(context, mediaStoreUri);
} catch (FileNotFoundException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to find thumbnail file", e);
}
}
int orientation = -1;
if (result != null) {
orientation = fetcher.getOrientation(context, mediaStoreUri);
}
if (orientation != -1) {
result = new ExifOrientationStream(result, orientation);
}
return result;
}
@Override
......@@ -111,7 +135,7 @@ public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
}
interface ThumbnailQuery {
Cursor query(Context context, Uri uri);
Cursor queryPath(Context context, Uri uri);
}
static class ThumbnailStreamOpener {
......@@ -128,20 +152,36 @@ public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
this.query = query;
}
public int getOrientation(Context context, Uri uri) {
int orientation = -1;
InputStream is = null;
try {
is = context.getContentResolver().openInputStream(uri);
orientation = new ImageHeaderParser(is).getOrientation();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open uri: " + uri, e);
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
return orientation;
}
public InputStream open(Context context, Uri uri) throws FileNotFoundException {
Uri thumbnailUri = null;
InputStream inputStream = null;
final Cursor cursor = query.query(context, uri);
final Cursor cursor = query.queryPath(context, uri);
try {
if (cursor != null && cursor.moveToFirst()) {
String path = cursor.getString(0);
if (!TextUtils.isEmpty(path)) {
File file = service.get(path);
if (service.exists(file) && service.length(file) > 0) {
thumbnailUri = Uri.fromFile(file);
}
}
thumbnailUri = parseThumbUri(cursor);
}
} finally {
if (cursor != null) {
......@@ -153,29 +193,57 @@ public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
}
return inputStream;
}
private Uri parseThumbUri(Cursor cursor) {
Uri result = null;
String path = cursor.getString(0);
if (!TextUtils.isEmpty(path)) {
File file = service.get(path);
if (service.exists(file) && service.length(file) > 0) {
result = Uri.fromFile(file);
}
}
return result;
}
}
static class ImageThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Images.Thumbnails.DATA,
};
private static final String PATH_SELECTION =
MediaStore.Images.Thumbnails.KIND + " = " + MediaStore.Images.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Images.Thumbnails.IMAGE_ID + " = ?";
@Override
public Cursor query(Context context, Uri uri) {
String id = uri.getLastPathSegment();
return context.getContentResolver().query(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, new String[] {
MediaStore.Images.Thumbnails.DATA
}, MediaStore.Images.Thumbnails.IMAGE_ID + " = ? AND " + MediaStore.Images.Thumbnails.KIND + " = ?",
new String[] { id, String.valueOf(MediaStore.Images.Thumbnails.MINI_KIND) }, null);
public Cursor queryPath(Context context, Uri uri) {
String imageId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { imageId },
null /*sortOrder*/);
}
}
static class VideoThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Video.Thumbnails.DATA
};
private static final String PATH_SELECTION =
MediaStore.Video.Thumbnails.KIND + " = " + MediaStore.Video.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Video.Thumbnails.VIDEO_ID + " = ?";
@Override
public Cursor query(Context context, Uri uri) {
String id = uri.getLastPathSegment();
return context.getContentResolver().query(MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI, new String[] {
MediaStore.Video.Thumbnails.DATA
}, MediaStore.Video.Thumbnails.VIDEO_ID + " = ? AND " + MediaStore.Video.Thumbnails.KIND + " = ?",
new String[] { id, String.valueOf(MediaStore.Video.Thumbnails.MINI_KIND) }, null);
public Cursor queryPath(Context context, Uri uri) {
String videoId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { videoId },
null /*sortOrder*/);
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册