diff --git a/picasso/src/main/java/com/squareup/picasso/ExifStreamReader.java b/picasso/src/main/java/com/squareup/picasso/ExifStreamReader.java new file mode 100644 index 0000000000..9a30f1d7b2 --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso/ExifStreamReader.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Adapted for picasso + */ + +package com.squareup.picasso; + +import android.util.Log; +import java.io.IOException; +import java.io.InputStream; + +final class ExifStreamReader { + private static final String TAG = "CameraExif"; + + // Returns the orientation value + public static int getOrientation(InputStream stream) throws IOException { + if (stream == null) { + return 0; + } + MarkableInputStream markStream = new MarkableInputStream(stream); + long mark = markStream.savePosition(65536); + int orientation = getOrientation(markStream, 65536); + markStream.reset(mark); + return orientation; + } + + // Returns the orientation value + static int getOrientation(MarkableInputStream jpegstream, int byteLimit) throws IOException { + if (jpegstream == null) { + return 0; + } + int marker = 0xFF; + int offset = 0; + int length = 0; + // ISO/IEC 10918-1:1993(E) + while ((offset + 3 < byteLimit) && (marker = jpegstream.read() & 0xFF) == 0xFF) { + marker = jpegstream.read() & 0xFF; + offset += 2; + // Check if the marker is a padding byte + if (marker == 0xFF) { + continue; + } + + // Check if the marker is SOI or TEM. + if (marker == 0xD8 || marker == 0x01) { + continue; + } + // Check if the marker is EOI or SOS. + if (marker == 0xD9 || marker == 0xDA) { + break; + } + + // Get the length and check if it is reasonable. + length = pack(jpegstream, 2 , false); + if (length < 2 || offset + length > byteLimit) { + Log.e(TAG, "Invalid length"); + return 0; + } + + // Break if the marker is EXIF in APP1. + int marker2 = 0x45786966; + if (marker == 0xE1 && length >= 8 + && (marker2 = pack(jpegstream, 4, false)) == 0x45786966 + && pack(jpegstream, 2, false) == 0) { + Log.e(TAG, "APP1"); + offset += 8; + length -= 8; + break; + } + // Skip other markers. + Log.e(TAG, "Skipping markers"); + offset += length; + if (marker != 0xE1 || length < 8) { + jpegstream.skip(length - 2); + } else if (marker2 != 0x45786966) { + jpegstream.skip(length - 6); + } else { + jpegstream.skip(length - 8); + } + length = 0; + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + // Identify the byte order. + int tag = pack(jpegstream, 4, false); + if (tag != 0x49492A00 && tag != 0x4D4D002A) { + Log.e(TAG, "Invalid byte order"); + return 0; + } + boolean littleEndian = (tag == 0x49492A00); + // Get the offset and check if it is reasonable. + int count = pack(jpegstream, 4, littleEndian) + 2; + if (count < 10 || count > length) { + Log.e(TAG, "Invalid offset " + count + " , " + length); + return 0; + } + offset += count; + length -= count; + jpegstream.skip(count - 10); + // Get the count and go through all the elements. + count = pack(jpegstream, 2, littleEndian); + while (count-- > 0 && length >= 12) { + // Get the tag and check if it is orientation. + tag = pack(jpegstream, 2, littleEndian); + if (tag == 0x0112) { + jpegstream.skip(6); + int orientation = pack(jpegstream, 2, littleEndian); + switch (orientation) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + return orientation; + default: + break; + } + Log.i(TAG, "Unsupported orientation"); + return 0; + } else { + jpegstream.skip(10); + } + offset += 12; + length -= 12; + } + } + + Log.i(TAG, "Orientation not found"); + return 0; + } + + + private static int pack(MarkableInputStream jpegstream, int length, boolean littleEndian) +throws IOException { + int shiftL = 0, shiftB = 8; + if (littleEndian) { + shiftB = 0; + shiftL = 1; + } + int value = 0; + for (int i = 0; i < length; i++) { + value = ((value << shiftB) | ((jpegstream.read() & 0xFF) << (shiftL * i * 8))); + } + return value; + } +} + diff --git a/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java b/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java index 5d9c677e80..d7eeffa9ab 100644 --- a/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso/NetworkRequestHandler.java @@ -69,6 +69,13 @@ public NetworkRequestHandler(Downloader downloader, Stats stats) { if (loadedFrom == NETWORK && response.getContentLength() > 0) { stats.dispatchDownloadFinished(response.getContentLength()); } + try { + MarkableInputStream markStream = new MarkableInputStream(is); + is = markStream; + int orientation = ExifStreamReader.getOrientation(is); + return new Result(is, loadedFrom, orientation); + } catch (IOException ignored) { + } return new Result(is, loadedFrom); } diff --git a/picasso/src/main/java/com/squareup/picasso/RequestHandler.java b/picasso/src/main/java/com/squareup/picasso/RequestHandler.java index 54798a553b..e5ceb9d882 100644 --- a/picasso/src/main/java/com/squareup/picasso/RequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso/RequestHandler.java @@ -62,6 +62,10 @@ public Result(InputStream stream, Picasso.LoadedFrom loadedFrom) { this(null, checkNotNull(stream, "stream == null"), loadedFrom, 0); } + public Result(InputStream stream, Picasso.LoadedFrom loadedFrom, int exifOrientation) { + this(null, checkNotNull(stream, "stream == null"), loadedFrom, exifOrientation); + } + Result(Bitmap bitmap, InputStream stream, Picasso.LoadedFrom loadedFrom, int exifOrientation) { if (!(bitmap != null ^ stream != null)) { throw new AssertionError(); diff --git a/picasso/src/test/java/com/squareup/picasso/ExifStreamReaderTest.java b/picasso/src/test/java/com/squareup/picasso/ExifStreamReaderTest.java new file mode 100644 index 0000000000..3890b2a187 --- /dev/null +++ b/picasso/src/test/java/com/squareup/picasso/ExifStreamReaderTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.picasso; + +import java.io.IOException; +import java.io.InputStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.ANDROID.assertThat; +import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.entry; +import static org.fest.assertions.api.Assertions.fail; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ExifStreamReaderTest { + @Test + public void ExifReaderTest() throws IOException { + for (int i=1;i<=8;i++) { + InputStream is = getClass().getClassLoader().getResourceAsStream("Portrait_" + i + ".jpg"); + assertThat(ExifStreamReader.getOrientation(is)).isEqualTo(i); + } + } + + @Test + public void NonJpgExifReaderTest() throws IOException { + InputStream is = getClass().getClassLoader().getResourceAsStream("Portrait_5.png"); + assertThat(ExifStreamReader.getOrientation(is)).isEqualTo(0); + } +} diff --git a/picasso/src/test/java/com/squareup/picasso/NetworkRequestHandlerTest.java b/picasso/src/test/java/com/squareup/picasso/NetworkRequestHandlerTest.java index 4da1c4a6f3..be2d548b41 100644 --- a/picasso/src/test/java/com/squareup/picasso/NetworkRequestHandlerTest.java +++ b/picasso/src/test/java/com/squareup/picasso/NetworkRequestHandlerTest.java @@ -149,21 +149,4 @@ public class NetworkRequestHandlerTest { assertThat(result.getStream()).isNull(); } - @Test public void downloaderInputStreamNotDecoded() throws Exception { - final InputStream is = new ByteArrayInputStream(new byte[] { 'a' }); - Downloader bitmapDownloader = new Downloader() { - @Override public Response load(Uri uri, int networkPolicy) throws IOException { - return new Response(is, false, 1); - } - - @Override public void shutdown() { - } - }; - Action action = TestUtils.mockAction(URI_KEY_1, URI_1); - NetworkRequestHandler customNetworkHandler = new NetworkRequestHandler(bitmapDownloader, stats); - - RequestHandler.Result result = customNetworkHandler.load(action.getRequest(), 0); - assertThat(result.getStream()).isSameAs(is); - assertThat(result.getBitmap()).isNull(); - } } diff --git a/picasso/src/test/resources/Portrait_1.jpg b/picasso/src/test/resources/Portrait_1.jpg new file mode 100644 index 0000000000..f5797797e3 Binary files /dev/null and b/picasso/src/test/resources/Portrait_1.jpg differ diff --git a/picasso/src/test/resources/Portrait_2.jpg b/picasso/src/test/resources/Portrait_2.jpg new file mode 100644 index 0000000000..4d9212c209 Binary files /dev/null and b/picasso/src/test/resources/Portrait_2.jpg differ diff --git a/picasso/src/test/resources/Portrait_3.jpg b/picasso/src/test/resources/Portrait_3.jpg new file mode 100644 index 0000000000..2f88b6d222 Binary files /dev/null and b/picasso/src/test/resources/Portrait_3.jpg differ diff --git a/picasso/src/test/resources/Portrait_4.jpg b/picasso/src/test/resources/Portrait_4.jpg new file mode 100644 index 0000000000..cc947dadb1 Binary files /dev/null and b/picasso/src/test/resources/Portrait_4.jpg differ diff --git a/picasso/src/test/resources/Portrait_5.jpg b/picasso/src/test/resources/Portrait_5.jpg new file mode 100644 index 0000000000..b14114bcf1 Binary files /dev/null and b/picasso/src/test/resources/Portrait_5.jpg differ diff --git a/picasso/src/test/resources/Portrait_5.png b/picasso/src/test/resources/Portrait_5.png new file mode 100644 index 0000000000..a0ade56ab4 Binary files /dev/null and b/picasso/src/test/resources/Portrait_5.png differ diff --git a/picasso/src/test/resources/Portrait_6.jpg b/picasso/src/test/resources/Portrait_6.jpg new file mode 100644 index 0000000000..79617a97c9 Binary files /dev/null and b/picasso/src/test/resources/Portrait_6.jpg differ diff --git a/picasso/src/test/resources/Portrait_7.jpg b/picasso/src/test/resources/Portrait_7.jpg new file mode 100644 index 0000000000..fbcde86dac Binary files /dev/null and b/picasso/src/test/resources/Portrait_7.jpg differ diff --git a/picasso/src/test/resources/Portrait_8.jpg b/picasso/src/test/resources/Portrait_8.jpg new file mode 100644 index 0000000000..07beac8d44 Binary files /dev/null and b/picasso/src/test/resources/Portrait_8.jpg differ