21

I am using Picasso to load images from the web in my application. I have noticed that some images are shown rotated by 90degrees although when I open the image in my browser I see it correctly positioned. I assume that these images have EXIF data. Is there any way to instruct Picasso to ignore EXIF?

Panos
  • 7,227
  • 13
  • 60
  • 95
  • That's odd. I didn't realize Picasso paid attention to EXIF. Do you have any example images that you can link to? – CommonsWare Jan 13 '15 at 21:51
  • Yes. It is odd and not expected. I can't give you my current image as it comes from a private server. But after checking it's EXIF online I see this: Resolution : 3264 x 2448 Orientation : rotate 90 ======= IPTC data : ======= So it is confirmed that the EXIF is responsible for that. – Panos Jan 13 '15 at 21:55
  • 1
    Check line #162 https://github.com/square/picasso/blob/c8e79ce78c26e6ecdf3a0cf0a2efecd95f5ac4d7/picasso/src/main/java/com/squareup/picasso/BitmapHunter.java – Distwo Jan 13 '15 at 22:00
  • It would be nice if there was an option to disable it. – Panos Jan 13 '15 at 22:04
  • I'm not familiar with this API yet, but if there is no way to disable it, you should open a request for that cause that definitely something needed. – Distwo Jan 14 '15 at 00:19
  • @CommonsWare , will that picture do it? http://files.parsetfss.com/da393543-4c5b-4303-9227-491994a9f182/tfss-55ea9699-dd4d-451c-8421-1a41af5a4a20-photo_20150428_161247_XtgKPdVVkz.jpg – Stephane Maarek Apr 28 '15 at 16:23
  • @Stephane: Well, per Distwo's comment, Picasso definitely is paying attention to EXIF headers. – CommonsWare Apr 28 '15 at 16:29
  • @CommonsWare: Apparently not for network images (I have no clue why): https://github.com/square/picasso/issues/846 – Stephane Maarek Apr 28 '15 at 16:34

3 Answers3

2

As we know, Picasso supports EXIF from local storage, this is done via Android inner Utils. Providing the same functionality can't be done easy due to ability to use custom Http loading libraries. My solution is simple: we must override caching and apply Exif rotation before item is cached.

    OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(chain -> {
            Response originalResponse = chain.proceed(chain.request());
            byte[] body = originalResponse.body().bytes();
            ResponseBody newBody = ResponseBody
                .create(originalResponse.body().contentType(), ImageUtils.processImage(body));
            return originalResponse.newBuilder().body(newBody).build();
        })
        .cache(cache)
        .build();

Here we add NetworkInterceptor that can transform request and response before it gets cached.

public class ImageUtils {

    public static byte[] processImage(byte[] originalImg) {
        int orientation = Exif.getOrientation(originalImg);
        if (orientation != 0) {
            Bitmap bmp = BitmapFactory.decodeByteArray(originalImg, 0, originalImg.length);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            rotateImage(orientation, bmp).compress(Bitmap.CompressFormat.PNG, 100, stream);

            return stream.toByteArray();
        }
        return originalImg;
    }

    private static Bitmap rotateImage(int angle, Bitmap bitmapSrc) {
        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        return Bitmap.createBitmap(bitmapSrc, 0, 0,
                bitmapSrc.getWidth(), bitmapSrc.getHeight(), matrix, true);
    }
}

Exif transformation:

public class Exif {
    private static final String TAG = "Exif";

    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
    public static int getOrientation(byte[] jpeg) {
        if (jpeg == null) {
            return 0;
        }

        int offset = 0;
        int length = 0;

        // ISO/IEC 10918-1:1993(E)
        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
            int marker = jpeg[offset] & 0xFF;

            // Check if the marker is a padding.
            if (marker == 0xFF) {
                continue;
            }
            offset++;

            // 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(jpeg, offset, 2, false);
            if (length < 2 || offset + length > jpeg.length) {
                Log.e(TAG, "Invalid length");
                return 0;
            }

            // Break if the marker is EXIF in APP1.
            if (marker == 0xE1 && length >= 8 &&
                    pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
                    pack(jpeg, offset + 6, 2, false) == 0) {
                offset += 8;
                length -= 8;
                break;
            }

            // Skip other markers.
            offset += length;
            length = 0;
        }

        // JEITA CP-3451 Exif Version 2.2
        if (length > 8) {
            // Identify the byte order.
            int tag = pack(jpeg, offset, 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(jpeg, offset + 4, 4, littleEndian) + 2;
            if (count < 10 || count > length) {
                Log.e(TAG, "Invalid offset");
                return 0;
            }
            offset += count;
            length -= count;

            // Get the count and go through all the elements.
            count = pack(jpeg, offset - 2, 2, littleEndian);
            while (count-- > 0 && length >= 12) {
                // Get the tag and check if it is orientation.
                tag = pack(jpeg, offset, 2, littleEndian);
                if (tag == 0x0112) {
                    // We do not really care about type and count, do we?
                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
                    switch (orientation) {
                        case 1:
                            return 0;
                        case 3:
                            return 180;
                        case 6:
                            return 90;
                        case 8:
                            return 270;
                    }
                    Log.i(TAG, "Unsupported orientation");
                    return 0;
                }
                offset += 12;
                length -= 12;
            }
        }

        Log.i(TAG, "Orientation not found");
        return 0;
    }

    private static int pack(byte[] bytes, int offset, int length,
                            boolean littleEndian) {
        int step = 1;
        if (littleEndian) {
            offset += length - 1;
            step = -1;
        }

        int value = 0;
        while (length-- > 0) {
            value = (value << 8) | (bytes[offset] & 0xFF);
            offset += step;
        }
        return value;
    }
}

This solution is experimental and must be tested for leaks and probably improved. In most cases Samsung and iOs devices return 90 DEG rotation and this solution works. Other cases also must be tested.

beresfordt
  • 5,088
  • 10
  • 35
  • 43
Ph0en1x
  • 21
  • 2
2

based on @ph0en1x response this version use google exif library and kotlin: add this interceptor to okhttpclient used by picasso

addNetworkInterceptor {
    val response = it.proceed(it.request())
    val body = response.body
    if (body?.contentType()?.type == "image") {
        val bytes = body.bytes()
        val degrees = bytes.inputStream().use { input ->
            when (ExifInterface(input).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
                ExifInterface.ORIENTATION_ROTATE_270 -> 270
                ExifInterface.ORIENTATION_ROTATE_180 -> 180
                ExifInterface.ORIENTATION_ROTATE_90 -> 90
                else -> 0
            }
        }
        if (degrees != 0) {
            val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
            ByteArrayOutputStream().use { output ->
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, Matrix().apply { postRotate(degrees.toFloat()) }, true)
                    .compress(Bitmap.CompressFormat.PNG, 100, output)
                response.newBuilder().body(output.toByteArray().toResponseBody(body.contentType())).build()
            }
        } else
            response.newBuilder().body(bytes.toResponseBody(body.contentType())).build()
    } else
        response
}
Alessandro Scarozza
  • 4,273
  • 6
  • 31
  • 39
0

Can you post the image you're using? because as this thread said, exif orientation for images loaded from web is ignored(only content provider and local files).

I also try to display this image in picasso 2.5.2, the real orientation of the image is facing rightside(the bottom code in image is facing right). The exif orientation, is 90deg clockwise. Try open it in chrome(chrome is honoring exif rotation), the image will be faced down(bottom code in image is facing down).

fchristysen
  • 208
  • 1
  • 10