Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lightweight EXIF orientation for network JPG/TIFF #1031

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions picasso/src/main/java/com/squareup/picasso/ExifStreamReader.java
Original file line number Diff line number Diff line change
@@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't using a MarkableInputStream here irrelevant, since it's local to ExifStreamReader only and will be discarded at the end of the call. The InputStream provided by the caller will still have been consumed and is independent of MarkableInputStream. Instead, I think you want to find a way to maintain the original InputStream (perhaps best done by the caller?)

I believe this is the root cause of the issue I was having using your branch, where images don't load up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MarkableInputStream is used to allow us to reset the Inputstream i.e. not have it consumed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to build JohnWowUs:IFStream-EXIF branch, and have the same issue. It would be great, if you can fix it and merge into picasso.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JohnWowUs I see what you mean; I was under the impression that the MarkableInputStream maintains its own marker and resetting it just resets MarkableInputStream, but after looking at the code, I see that it also tries to reset the original InputStream.

However, it seems that it does not work (hence, why I'm seeing failures in decoding the images). I believe it happens when the downloader is UrlConnectionDownloader, since that downloader uses the InputStream provided by openConnection(), which doesn't seem resettable.

Can you also try it on your end to confirm?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use OkHttpDownloader. Also I use NetworkInterceptor, wich replaces original InputStream with new one, because original stream is encrypted and I need to decrypt it.

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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public NetworkRequestHandler(Downloader downloader, Stats stats) {
if (loadedFrom == NETWORK && response.getContentLength() > 0) {
stats.dispatchDownloadFinished(response.getContentLength());
}
try {
int orientation = ExifStreamReader.getOrientation(is);
return new Result(is, loadedFrom, orientation);
} catch (IOException ignored) {
}
return new Result(is, loadedFrom);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Binary file added picasso/src/test/resources/Portrait_1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_6.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_7.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added picasso/src/test/resources/Portrait_8.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.