From cdf7f479625fdcbe2524e69a151d2b96c8390ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Cotta?= Date: Fri, 19 Jul 2019 02:33:53 -0300 Subject: [PATCH 1/3] fixed image orientation problem --- packages/image_picker/android/build.gradle | 3 +- .../plugins/imagepicker/ExifDataCopier.java | 55 ---------- .../plugins/imagepicker/ExifHandler.java | 101 ++++++++++++++++++ .../imagepicker/ImagePickerDelegate.java | 23 ++-- .../imagepicker/ImagePickerPlugin.java | 2 +- .../plugins/imagepicker/ImageResizer.java | 58 +++++----- .../imagepicker/ImagePickerDelegateTest.java | 8 +- 7 files changed, 158 insertions(+), 92 deletions(-) delete mode 100644 packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java create mode 100644 packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifHandler.java diff --git a/packages/image_picker/android/build.gradle b/packages/image_picker/android/build.gradle index 7de0adc60eb9..d1ecc9944410 100755 --- a/packages/image_picker/android/build.gradle +++ b/packages/image_picker/android/build.gradle @@ -50,5 +50,6 @@ android { } dependencies { - api 'androidx.legacy:legacy-support-v4:1.0.0' + api 'androidx.exifinterface:exifinterface:1.0.0' + api 'androidx.core:core:1.0.2' } diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java deleted file mode 100644 index ce199295cc19..000000000000 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.imagepicker; - -import android.media.ExifInterface; -import android.util.Log; -import java.util.Arrays; -import java.util.List; - -class ExifDataCopier { - void copyExif(String filePathOri, String filePathDest) { - try { - ExifInterface oldExif = new ExifInterface(filePathOri); - ExifInterface newExif = new ExifInterface(filePathDest); - - List attributes = - Arrays.asList( - "FNumber", - "ExposureTime", - "ISOSpeedRatings", - "GPSAltitude", - "GPSAltitudeRef", - "FocalLength", - "GPSDateStamp", - "WhiteBalance", - "GPSProcessingMethod", - "GPSTimeStamp", - "DateTime", - "Flash", - "GPSLatitude", - "GPSLatitudeRef", - "GPSLongitude", - "GPSLongitudeRef", - "Make", - "Model", - "Orientation"); - for (String attribute : attributes) { - setIfNotNull(oldExif, newExif, attribute); - } - - newExif.saveAttributes(); - - } catch (Exception ex) { - Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex); - } - } - - private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) { - if (oldExif.getAttribute(property) != null) { - newExif.setAttribute(property, oldExif.getAttribute(property)); - } - } -} diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifHandler.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifHandler.java new file mode 100644 index 000000000000..218ed427a95b --- /dev/null +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifHandler.java @@ -0,0 +1,101 @@ +package io.flutter.plugins.imagepicker; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import androidx.exifinterface.media.ExifInterface; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class ExifHandler { + + public ExifInterface copyExif(final String filePathOri, String filePathDest) throws IOException { + + ExifInterface newExif = new ExifInterface(filePathDest); + final ExifInterface originalExif = new ExifInterface(filePathOri); + List attributes = + Arrays.asList( + "FNumber", + "ExposureTime", + "ISOSpeedRatings", + "GPSAltitude", + "GPSAltitudeRef", + "FocalLength", + "GPSDateStamp", + "WhiteBalance", + "GPSProcessingMethod", + "GPSTimeStamp", + "DateTime", + "Flash", + "GPSLatitude", + "GPSLatitudeRef", + "GPSLongitude", + "GPSLongitudeRef", + "Make", + "Model", + "Orientation"); + + for (String attribute : attributes) { + setIfNotNull(originalExif, newExif, attribute); + } + + newExif.saveAttributes(); + + return newExif; + } + + private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) { + if (oldExif.getAttribute(property) != null) { + newExif.setAttribute(property, oldExif.getAttribute(property)); + } + } + + // from : https://github.com/google/cameraview/issues/22#issuecomment-363047917 + public Bitmap getNormalOrientationBitmap(String imageAbsolutePath) + throws IOException { + Bitmap bitmap = BitmapFactory.decodeFile(imageAbsolutePath); + ExifInterface ei = new ExifInterface(imageAbsolutePath); + + int orientation = + ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + + Matrix matrix = new Matrix(); + switch (orientation) { + case ExifInterface.ORIENTATION_NORMAL: + return bitmap; + case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: + matrix.setScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_180: + matrix.setRotate(180); + break; + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + matrix.setRotate(180); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_TRANSPOSE: + matrix.setRotate(90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_90: + matrix.setRotate(90); + break; + case ExifInterface.ORIENTATION_TRANSVERSE: + matrix.setRotate(-90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_270: + matrix.setRotate(-90); + break; + } + + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + public void setNormalOrientation(ExifInterface exif) throws IOException { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + String.valueOf(ExifInterface.ORIENTATION_NORMAL)); + exif.saveAttributes(); + } +} diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 2f2522f53c5c..d96666c45767 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -210,7 +210,13 @@ void retrieveLostImage(MethodChannel.Result result) { if (path != null) { Double maxWidth = (Double) resultMap.get(ImagePickerCache.MAP_KEY_MAX_WIDTH); Double maxHeight = (Double) resultMap.get(ImagePickerCache.MAP_KEY_MAX_HEIGHT); - String newPath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight); + String newPath = null; + try { + newPath = imageResizer.normalizeImage(path, maxWidth, maxHeight); + new File(path).delete(); + } catch (IOException e) { + finishWithError("failed_to_read_image", e.getMessage()); + } resultMap.put(ImagePickerCache.MAP_KEY_PATH, newPath); } if (resultMap.isEmpty()) { @@ -509,14 +515,19 @@ private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled if (methodCall != null) { Double maxWidth = methodCall.argument("maxWidth"); Double maxHeight = methodCall.argument("maxHeight"); - String finalImagePath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight); - finishWithSuccess(finalImagePath); + try { + final String finalImagePath = imageResizer.normalizeImage(path, maxWidth, maxHeight); + if (shouldDeleteOriginalIfScaled) { + new File(path).delete(); + } + + finishWithSuccess(finalImagePath); - //delete original file if scaled - if (!finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { - new File(path).delete(); + } catch (IOException e) { + finishWithError("failed_to_read_image", e.getMessage()); } + } else { finishWithSuccess(path); } diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 371a258966c0..450f651d05af 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -43,7 +43,7 @@ public static void registerWith(PluginRegistry.Registrar registrar) { final File externalFilesDirectory = registrar.activity().getExternalFilesDir(Environment.DIRECTORY_PICTURES); - final ExifDataCopier exifDataCopier = new ExifDataCopier(); + final ExifHandler exifDataCopier = new ExifHandler(); final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); final ImagePickerDelegate delegate = new ImagePickerDelegate(registrar.activity(), externalFilesDirectory, imageResizer); diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index f9bd3adb2e36..3f611e0c8a1e 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -5,7 +5,8 @@ package io.flutter.plugins.imagepicker; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; +import android.graphics.Bitmap.CompressFormat; +import androidx.exifinterface.media.ExifInterface; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -13,38 +14,44 @@ class ImageResizer { private final File externalFilesDirectory; - private final ExifDataCopier exifDataCopier; + private final ExifHandler exifHandler; - ImageResizer(File externalFilesDirectory, ExifDataCopier exifDataCopier) { + ImageResizer(File externalFilesDirectory, ExifHandler exifHandler) { this.externalFilesDirectory = externalFilesDirectory; - this.exifDataCopier = exifDataCopier; + this.exifHandler = exifHandler; + } /** - * If necessary, resizes the image located in imagePath and then returns the path for the scaled - * image. + * This method will utilize the Exif information to try to place the image in normal orientation + * and then resize it. * - *

If no resizing is needed, returns the path for the original image. + *

This method will always apply the operations of fix and resize operation. + * returns the path for the scaled image. + * This will create a new image file for every call. */ - String resizeImageIfNeeded(String imagePath, Double maxWidth, Double maxHeight) { - boolean shouldScale = maxWidth != null || maxHeight != null; + String normalizeImage(String imagePath, Double maxWidth, Double maxHeight) throws IOException { - if (!shouldScale) { - return imagePath; - } - try { - File scaledImage = resizedImage(imagePath, maxWidth, maxHeight); - exifDataCopier.copyExif(imagePath, scaledImage.getPath()); + final Bitmap bitmap = exifHandler.getNormalOrientationBitmap(imagePath); + final File file = resizedImage(bitmap, imagePath, maxWidth, maxHeight); + final ExifInterface newExif = exifHandler.copyExif(imagePath, file.getPath()); + + bitmap.recycle(); + + exifHandler.setNormalOrientation(newExif); + + return file.getPath(); - return scaledImage.getPath(); - } catch (IOException e) { - throw new RuntimeException(e); - } } - private File resizedImage(String path, Double maxWidth, Double maxHeight) throws IOException { - Bitmap bmp = BitmapFactory.decodeFile(path); + private File resizedImage( + Bitmap bmp, + String path, + Double maxWidth, + Double maxHeight + ) throws IOException { + double originalWidth = bmp.getWidth() * 1.0; double originalHeight = bmp.getHeight() * 1.0; @@ -85,9 +92,10 @@ private File resizedImage(String path, Double maxWidth, Double maxHeight) throws Bitmap scaledBmp = Bitmap.createScaledBitmap(bmp, width.intValue(), height.intValue(), false); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - boolean saveAsPNG = bmp.hasAlpha(); - scaledBmp.compress( - saveAsPNG ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 100, outputStream); + final boolean saveAsPNG = bmp.hasAlpha(); + final CompressFormat selectedFormat = saveAsPNG ? CompressFormat.PNG : CompressFormat.JPEG; + + scaledBmp.compress(selectedFormat, 100, outputStream); String[] pathParts = path.split("/"); String imageName = pathParts[pathParts.length - 1]; @@ -96,7 +104,7 @@ private File resizedImage(String path, Double maxWidth, Double maxHeight) throws FileOutputStream fileOutput = new FileOutputStream(imageFile); fileOutput.write(outputStream.toByteArray()); fileOutput.close(); - + scaledBmp.recycle(); return imageFile; } } diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 02bb91b7914f..1b87076c9d73 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -60,12 +60,12 @@ public void setUp() { when(mockFileUtils.getPathFromUri(any(Context.class), any(Uri.class))) .thenReturn("pathFromUri"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null)) + when(mockImageResizer.normalizeImage("pathFromUri", null, null)) .thenReturn("originalPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT)) + when(mockImageResizer.normalizeImage("pathFromUri", WIDTH, HEIGHT)) .thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null)).thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT)) + when(mockImageResizer.normalizeImage("pathFromUri", WIDTH, null)).thenReturn("scaledPath"); + when(mockImageResizer.normalizeImage("pathFromUri", null, HEIGHT)) .thenReturn("scaledPath"); mockFileUriResolver = new MockFileUriResolver(); From 58e5ecbd0a62eb4f5b8915e6eda78ecf3da254b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Cotta?= Date: Fri, 19 Jul 2019 02:57:56 -0300 Subject: [PATCH 2/3] fix test --- .../imagepicker/ImagePickerDelegateTest.java | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 1b87076c9d73..d0b3a81e8e0a 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -18,6 +18,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.io.File; +import java.io.IOException; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -51,7 +52,7 @@ public void getFullImagePath(Uri imageUri, ImagePickerDelegate.OnPathReadyListen } @Before - public void setUp() { + public void setUp() throws IOException { MockitoAnnotations.initMocks(this); when(mockActivity.getPackageName()).thenReturn("com.example.test"); @@ -61,7 +62,7 @@ public void setUp() { .thenReturn("pathFromUri"); when(mockImageResizer.normalizeImage("pathFromUri", null, null)) - .thenReturn("originalPath"); + .thenReturn("scaledPath"); when(mockImageResizer.normalizeImage("pathFromUri", WIDTH, HEIGHT)) .thenReturn("scaledPath"); when(mockImageResizer.normalizeImage("pathFromUri", WIDTH, null)).thenReturn("scaledPath"); @@ -282,18 +283,6 @@ public void onActivityResult_WhenPickFromGalleryCanceled_FinishesWithNull() { verifyNoMoreInteractions(mockResult); } - @Test - public void - onActivityResult_WhenImagePickedFromGallery_AndNoResizeNeeded_FinishesWithImagePath() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); - - delegate.onActivityResult( - ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); - - verify(mockResult).success("originalPath"); - verifyNoMoreInteractions(mockResult); - } - @Test public void onActivityResult_WhenImagePickedFromGallery_AndResizeNeeded_FinishesWithScaledImagePath() { @@ -331,16 +320,6 @@ public void onActivityResult_WhenTakeImageWithCameraCanceled_FinishesWithNull() verifyNoMoreInteractions(mockResult); } - @Test - public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_FinishesWithImagePath() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); - - delegate.onActivityResult( - ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); - - verify(mockResult).success("originalPath"); - verifyNoMoreInteractions(mockResult); - } @Test public void From 8da3f6dcdfd4be825f0243d6a4389f3b9c30a6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Cotta?= Date: Fri, 19 Jul 2019 03:02:45 -0300 Subject: [PATCH 3/3] update CHANGELOG.md and pubspec.yaml --- packages/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 44c8cc9dd01e..7176815fc92c 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 + +* Android: Fix image orientation problem (if Exif information exist in the image file) + ## 0.6.0+17 * iOS: Fix a crash when user captures image from the camera with devices under iOS 11. diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index 721e6abdfbeb..b6e9e859ede9 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -6,7 +6,7 @@ authors: - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.0+17 +version: 0.7.0 flutter: plugin: