diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java new file mode 100644 index 0000000000..ba5862a863 --- /dev/null +++ b/src/com/android/gallery3d/data/Exif.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2011 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. + */ + +package com.android.gallery3d.data; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; + +public class Exif { + private static final String TAG = "CameraExif"; + + public static int getOrientation(InputStream is) { + if (is == null) { + return 0; + } + + byte[] buf = new byte[8]; + int length = 0; + + // ISO/IEC 10918-1:1993(E) + while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { + int marker = buf[1] & 0xFF; + + // Check if the marker is a padding. + 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) { + return 0; + } + + // Get the length and check if it is reasonable. + if (!read(is, buf, 2)) { + return 0; + } + length = pack(buf, 0, 2, false); + if (length < 2) { + Log.e(TAG, "Invalid length"); + return 0; + } + length -= 2; + + // Break if the marker is EXIF in APP1. + if (marker == 0xE1 && length >= 6) { + if (!read(is, buf, 6)) return 0; + length -= 6; + if (pack(buf, 0, 4, false) == 0x45786966 && + pack(buf, 4, 2, false) == 0) { + break; + } + } + + // Skip other markers. + try { + is.skip(length); + } catch (IOException ex) { + return 0; + } + length = 0; + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + int offset = 0; + byte[] jpeg = new byte[length]; + if (!read(is, jpeg, length)) { + return 0; + } + + // 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; + } + + private static boolean read(InputStream is, byte[] buf, int length) { + try { + return is.read(buf, 0, length) == length; + } catch (IOException ex) { + return false; + } + } +} diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index b49b54445f..6debdd87a8 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -813,7 +813,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void addAttachmentImage(Uri imageUri) { try { - attachmentManager.setImage(imageUri); + attachmentManager.setImage(masterSecret, imageUri); } catch (IOException | BitmapDecodingException e) { Log.w(TAG, e); attachmentManager.clear(); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index af8098dbe3..9e641e9974 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -510,14 +510,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { @Override protected Bitmap doInBackground(Void... voids) { if (avatarUri != null) { - InputStream inputStream; try { - inputStream = getApplicationContext().getContentResolver().openInputStream(avatarUri); + avatarBmp = BitmapUtil.getScaledCircleCroppedBitmap(getApplicationContext(), avatarUri, AVATAR_SIZE); } catch (FileNotFoundException e) { Log.w(TAG, e); return null; } - avatarBmp = BitmapUtil.getScaledCircleCroppedBitmap(BitmapFactory.decodeStream(inputStream), AVATAR_SIZE); } return avatarBmp; } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 92d174111d..877e16f2ba 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -30,6 +30,7 @@ import android.widget.ImageView; import android.widget.Toast; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.BitmapDecodingException; import java.io.IOException; @@ -61,8 +62,8 @@ public class AttachmentManager { attachmentListener.onAttachmentChanged(); } - public void setImage(Uri image) throws IOException, BitmapDecodingException { - setMedia(new ImageSlide(context, image), 345, 261); + public void setImage(MasterSecret masterSecret, Uri image) throws IOException, BitmapDecodingException { + setMedia(new ImageSlide(context, masterSecret, image), 345, 261); } public void setVideo(Uri video) throws IOException, MediaTooLargeException { diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 533d13de41..83afb06bb3 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -60,8 +60,8 @@ public class ImageSlide extends Slide { super(context, masterSecret, part); } - public ImageSlide(Context context, Uri uri) throws IOException, BitmapDecodingException { - super(context, constructPartFromUri(context, uri)); + public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException { + super(context, constructPartFromUri(context, masterSecret, uri)); } @Override @@ -77,11 +77,9 @@ public class ImageSlide extends Slide { } try { - InputStream measureStream = getPartDataInputStream(); - InputStream dataStream = getPartDataInputStream(); - - thumbnail = new BitmapDrawable(context.getResources(), BitmapUtil.createScaledBitmap(measureStream, dataStream, maxWidth, maxHeight)); - thumbnailCache.put(part.getDataUri(), new SoftReference(thumbnail)); + thumbnail = new BitmapDrawable(context.getResources(), + BitmapUtil.createScaledBitmap(context, masterSecret, getUri(), maxWidth, maxHeight)); + thumbnailCache.put(part.getDataUri(), new SoftReference<>(thumbnail)); return thumbnail; } catch (FileNotFoundException e) { @@ -183,11 +181,11 @@ public class ImageSlide extends Slide { return SmilUtil.createMediaElement("img", document, new String(getPart().getName())); } - private static PduPart constructPartFromUri(Context context, Uri uri) + private static PduPart constructPartFromUri(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException { PduPart part = new PduPart(); - byte[] data = BitmapUtil.createScaledBytes(context, uri, 1280, 1280, MAX_MESSAGE_SIZE); + byte[] data = BitmapUtil.createScaledBytes(context, masterSecret, uri, 1280, 1280, MAX_MESSAGE_SIZE); part.setData(data); part.setDataUri(uri); diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index 3ec53d1a28..6e1e44af21 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -4,6 +4,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; @@ -13,9 +14,15 @@ import android.util.Log; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import com.android.gallery3d.data.Exif; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.mms.PartAuthority; + public class BitmapUtil { private static final String TAG = BitmapUtil.class.getSimpleName(); @@ -23,20 +30,15 @@ public class BitmapUtil { private static final int MIN_COMPRESSION_QUALITY = 50; private static final int MAX_COMPRESSION_ATTEMPTS = 4; - public static byte[] createScaledBytes(Context context, Uri uri, int maxWidth, - int maxHeight, int maxSize) + public static byte[] createScaledBytes(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, int maxSize) throws IOException, BitmapDecodingException { Bitmap bitmap; try { - bitmap = createScaledBitmap(context.getContentResolver().openInputStream(uri), - context.getContentResolver().openInputStream(uri), - maxWidth, maxHeight, false); + bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, false); } catch(OutOfMemoryError oome) { Log.w(TAG, "OutOfMemoryError when scaling precisely, doing rough scale to save memory instead"); - bitmap = createScaledBitmap(context.getContentResolver().openInputStream(uri), - context.getContentResolver().openInputStream(uri), - maxWidth, maxHeight, true); + bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, true); } int quality = MAX_COMPRESSION_QUALITY; int attempts = 0; @@ -56,6 +58,37 @@ public class BitmapUtil { else throw new IOException("Unable to scale image below: " + baos.size()); } + public static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight) + throws BitmapDecodingException, FileNotFoundException + { + return createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, false); + } + + private static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, boolean constrainedMemory) + throws FileNotFoundException, BitmapDecodingException + { + return createScaledBitmap(PartAuthority.getPartStream(context, masterSecret, uri), + PartAuthority.getPartStream(context, masterSecret, uri), + PartAuthority.getPartStream(context, masterSecret, uri), + maxWidth, maxHeight, constrainedMemory); + } + + private static Bitmap createScaledBitmap(InputStream measure, InputStream orientationStream, InputStream data, + int maxWidth, int maxHeight, boolean constrainedMemory) + throws BitmapDecodingException + { + Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight, constrainedMemory); + return fixOrientation(bitmap, orientationStream); + } + + private static Bitmap createScaledBitmap(InputStream measure, InputStream data, int maxWidth, int maxHeight, + boolean constrainedMemory) + throws BitmapDecodingException + { + final BitmapFactory.Options options = getImageDimensions(measure); + return createScaledBitmap(data, maxWidth, maxHeight, options, constrainedMemory); + } + public static Bitmap createScaledBitmap(InputStream measure, InputStream data, float scale) throws BitmapDecodingException { @@ -66,21 +99,12 @@ public class BitmapUtil { return createScaledBitmap(data, outWidth, outHeight, options, false); } - public static Bitmap createScaledBitmap(InputStream measure, InputStream data, - int maxWidth, int maxHeight) + public static Bitmap createScaledBitmap(InputStream measure, InputStream data, int maxWidth, int maxHeight) throws BitmapDecodingException { return createScaledBitmap(measure, data, maxWidth, maxHeight, false); } - public static Bitmap createScaledBitmap(InputStream measure, InputStream data, - int maxWidth, int maxHeight, boolean constrainedMemory) - throws BitmapDecodingException - { - final BitmapFactory.Options options = getImageDimensions(measure); - return createScaledBitmap(data, maxWidth, maxHeight, options, constrainedMemory); - } - private static Bitmap createScaledBitmap(InputStream data, int maxWidth, int maxHeight, BitmapFactory.Options options, boolean constrainedMemory) throws BitmapDecodingException @@ -141,6 +165,24 @@ public class BitmapUtil { } } + private static Bitmap fixOrientation(Bitmap bitmap, InputStream orientationStream) { + final int orientation = Exif.getOrientation(orientationStream); + + if (orientation != 0) { + return rotateBitmap(bitmap, orientation); + } else { + return bitmap; + } + } + + private static Bitmap rotateBitmap(Bitmap bitmap, int angle) { + Matrix matrix = new Matrix(); + matrix.postRotate(angle); + Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + if (rotated != bitmap) bitmap.recycle(); + return rotated; + } + private static BitmapFactory.Options getImageDimensions(InputStream inputStream) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; @@ -160,6 +202,14 @@ public class BitmapUtil { return getScaledCircleCroppedBitmap(bitmap, srcSize); } + public static Bitmap getScaledCircleCroppedBitmap(Context context, Uri uri, int destSize) throws FileNotFoundException { + InputStream dataStream = context.getContentResolver().openInputStream(uri); + InputStream orientationStream = context.getContentResolver().openInputStream(uri); + Bitmap bitmap = BitmapFactory.decodeStream(dataStream); + + return getScaledCircleCroppedBitmap(fixOrientation(bitmap, orientationStream), destSize); + } + public static Bitmap getScaledCircleCroppedBitmap(Bitmap bitmap, int destSize) { if (bitmap == null) return null; Bitmap output = Bitmap.createBitmap(destSize, destSize, Bitmap.Config.ARGB_8888);