From a82a0d50b31bb187c8e7edacb79c7824302035cf Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Mon, 28 Aug 2023 17:14:24 +0200 Subject: [PATCH] Refactor exif data copier and add tests --- .../plugins/imagepicker/ExifDataCopier.java | 236 +++++++++--------- .../plugins/imagepicker/ImageResizer.java | 7 +- .../imagepicker/ExifDataCopierTest.java | 114 +++++++++ 3 files changed, 234 insertions(+), 123 deletions(-) create mode 100644 packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ExifDataCopierTest.java diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java index 3f1887d98e4d..72080a72cacf 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java @@ -4,134 +4,126 @@ package io.flutter.plugins.imagepicker; -import android.util.Log; import androidx.exifinterface.media.ExifInterface; +import java.io.IOException; 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); - - @SuppressWarnings("deprecation") - List attributes = - Arrays.asList( - ExifInterface.TAG_IMAGE_DESCRIPTION, - ExifInterface.TAG_MAKE, - ExifInterface.TAG_MODEL, - ExifInterface.TAG_SOFTWARE, - ExifInterface.TAG_DATETIME, - ExifInterface.TAG_ARTIST, - ExifInterface.TAG_COPYRIGHT, - ExifInterface.TAG_EXPOSURE_TIME, - ExifInterface.TAG_F_NUMBER, - ExifInterface.TAG_EXPOSURE_PROGRAM, - ExifInterface.TAG_SPECTRAL_SENSITIVITY, - ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, - ExifInterface.TAG_ISO_SPEED_RATINGS, - ExifInterface.TAG_OECF, - ExifInterface.TAG_SENSITIVITY_TYPE, - ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, - ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, - ExifInterface.TAG_ISO_SPEED, - ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, - ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, - ExifInterface.TAG_EXIF_VERSION, - ExifInterface.TAG_DATETIME_ORIGINAL, - ExifInterface.TAG_DATETIME_DIGITIZED, - ExifInterface.TAG_OFFSET_TIME, - ExifInterface.TAG_OFFSET_TIME_ORIGINAL, - ExifInterface.TAG_OFFSET_TIME_DIGITIZED, - ExifInterface.TAG_SHUTTER_SPEED_VALUE, - ExifInterface.TAG_APERTURE_VALUE, - ExifInterface.TAG_BRIGHTNESS_VALUE, - ExifInterface.TAG_EXPOSURE_BIAS_VALUE, - ExifInterface.TAG_MAX_APERTURE_VALUE, - ExifInterface.TAG_SUBJECT_DISTANCE, - ExifInterface.TAG_METERING_MODE, - ExifInterface.TAG_LIGHT_SOURCE, - ExifInterface.TAG_FLASH, - ExifInterface.TAG_FOCAL_LENGTH, - ExifInterface.TAG_MAKER_NOTE, - ExifInterface.TAG_USER_COMMENT, - ExifInterface.TAG_SUBSEC_TIME, - ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, - ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, - ExifInterface.TAG_FLASHPIX_VERSION, - ExifInterface.TAG_FLASH_ENERGY, - ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, - ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, - ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, - ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, - ExifInterface.TAG_EXPOSURE_INDEX, - ExifInterface.TAG_SENSING_METHOD, - ExifInterface.TAG_FILE_SOURCE, - ExifInterface.TAG_SCENE_TYPE, - ExifInterface.TAG_CFA_PATTERN, - ExifInterface.TAG_CUSTOM_RENDERED, - ExifInterface.TAG_EXPOSURE_MODE, - ExifInterface.TAG_WHITE_BALANCE, - ExifInterface.TAG_DIGITAL_ZOOM_RATIO, - ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, - ExifInterface.TAG_SCENE_CAPTURE_TYPE, - ExifInterface.TAG_GAIN_CONTROL, - ExifInterface.TAG_CONTRAST, - ExifInterface.TAG_SATURATION, - ExifInterface.TAG_SHARPNESS, - ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, - ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, - ExifInterface.TAG_IMAGE_UNIQUE_ID, - ExifInterface.TAG_CAMERA_OWNER_NAME, - ExifInterface.TAG_BODY_SERIAL_NUMBER, - ExifInterface.TAG_LENS_SPECIFICATION, - ExifInterface.TAG_LENS_MAKE, - ExifInterface.TAG_LENS_MODEL, - ExifInterface.TAG_LENS_SERIAL_NUMBER, - ExifInterface.TAG_GPS_VERSION_ID, - ExifInterface.TAG_GPS_LATITUDE_REF, - ExifInterface.TAG_GPS_LATITUDE, - ExifInterface.TAG_GPS_LONGITUDE_REF, - ExifInterface.TAG_GPS_LONGITUDE, - ExifInterface.TAG_GPS_ALTITUDE_REF, - ExifInterface.TAG_GPS_ALTITUDE, - ExifInterface.TAG_GPS_TIMESTAMP, - ExifInterface.TAG_GPS_SATELLITES, - ExifInterface.TAG_GPS_STATUS, - ExifInterface.TAG_GPS_MEASURE_MODE, - ExifInterface.TAG_GPS_DOP, - ExifInterface.TAG_GPS_SPEED_REF, - ExifInterface.TAG_GPS_SPEED, - ExifInterface.TAG_GPS_TRACK_REF, - ExifInterface.TAG_GPS_TRACK, - ExifInterface.TAG_GPS_IMG_DIRECTION_REF, - ExifInterface.TAG_GPS_IMG_DIRECTION, - ExifInterface.TAG_GPS_MAP_DATUM, - ExifInterface.TAG_GPS_DEST_LATITUDE_REF, - ExifInterface.TAG_GPS_DEST_LATITUDE, - ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, - ExifInterface.TAG_GPS_DEST_LONGITUDE, - ExifInterface.TAG_GPS_DEST_BEARING_REF, - ExifInterface.TAG_GPS_DEST_BEARING, - ExifInterface.TAG_GPS_DEST_DISTANCE_REF, - ExifInterface.TAG_GPS_DEST_DISTANCE, - ExifInterface.TAG_GPS_PROCESSING_METHOD, - ExifInterface.TAG_GPS_AREA_INFORMATION, - ExifInterface.TAG_GPS_DATESTAMP, - ExifInterface.TAG_GPS_DIFFERENTIAL, - ExifInterface.TAG_GPS_H_POSITIONING_ERROR, - ExifInterface.TAG_INTEROPERABILITY_INDEX, - ExifInterface.TAG_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); + void copyExif(ExifInterface oldExif, ExifInterface newExif) throws IOException { + @SuppressWarnings("deprecation") + List attributes = + Arrays.asList( + ExifInterface.TAG_IMAGE_DESCRIPTION, + ExifInterface.TAG_MAKE, + ExifInterface.TAG_MODEL, + ExifInterface.TAG_SOFTWARE, + ExifInterface.TAG_DATETIME, + ExifInterface.TAG_ARTIST, + ExifInterface.TAG_COPYRIGHT, + ExifInterface.TAG_EXPOSURE_TIME, + ExifInterface.TAG_F_NUMBER, + ExifInterface.TAG_EXPOSURE_PROGRAM, + ExifInterface.TAG_SPECTRAL_SENSITIVITY, + ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, + ExifInterface.TAG_ISO_SPEED_RATINGS, + ExifInterface.TAG_OECF, + ExifInterface.TAG_SENSITIVITY_TYPE, + ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, + ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, + ExifInterface.TAG_ISO_SPEED, + ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, + ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, + ExifInterface.TAG_EXIF_VERSION, + ExifInterface.TAG_DATETIME_ORIGINAL, + ExifInterface.TAG_DATETIME_DIGITIZED, + ExifInterface.TAG_OFFSET_TIME, + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, + ExifInterface.TAG_OFFSET_TIME_DIGITIZED, + ExifInterface.TAG_SHUTTER_SPEED_VALUE, + ExifInterface.TAG_APERTURE_VALUE, + ExifInterface.TAG_BRIGHTNESS_VALUE, + ExifInterface.TAG_EXPOSURE_BIAS_VALUE, + ExifInterface.TAG_MAX_APERTURE_VALUE, + ExifInterface.TAG_SUBJECT_DISTANCE, + ExifInterface.TAG_METERING_MODE, + ExifInterface.TAG_LIGHT_SOURCE, + ExifInterface.TAG_FLASH, + ExifInterface.TAG_FOCAL_LENGTH, + ExifInterface.TAG_MAKER_NOTE, + ExifInterface.TAG_USER_COMMENT, + ExifInterface.TAG_SUBSEC_TIME, + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, + ExifInterface.TAG_FLASHPIX_VERSION, + ExifInterface.TAG_FLASH_ENERGY, + ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, + ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, + ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, + ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, + ExifInterface.TAG_EXPOSURE_INDEX, + ExifInterface.TAG_SENSING_METHOD, + ExifInterface.TAG_FILE_SOURCE, + ExifInterface.TAG_SCENE_TYPE, + ExifInterface.TAG_CFA_PATTERN, + ExifInterface.TAG_CUSTOM_RENDERED, + ExifInterface.TAG_EXPOSURE_MODE, + ExifInterface.TAG_WHITE_BALANCE, + ExifInterface.TAG_DIGITAL_ZOOM_RATIO, + ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, + ExifInterface.TAG_SCENE_CAPTURE_TYPE, + ExifInterface.TAG_GAIN_CONTROL, + ExifInterface.TAG_CONTRAST, + ExifInterface.TAG_SATURATION, + ExifInterface.TAG_SHARPNESS, + ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, + ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, + ExifInterface.TAG_IMAGE_UNIQUE_ID, + ExifInterface.TAG_CAMERA_OWNER_NAME, + ExifInterface.TAG_BODY_SERIAL_NUMBER, + ExifInterface.TAG_LENS_SPECIFICATION, + ExifInterface.TAG_LENS_MAKE, + ExifInterface.TAG_LENS_MODEL, + ExifInterface.TAG_LENS_SERIAL_NUMBER, + ExifInterface.TAG_GPS_VERSION_ID, + ExifInterface.TAG_GPS_LATITUDE_REF, + ExifInterface.TAG_GPS_LATITUDE, + ExifInterface.TAG_GPS_LONGITUDE_REF, + ExifInterface.TAG_GPS_LONGITUDE, + ExifInterface.TAG_GPS_ALTITUDE_REF, + ExifInterface.TAG_GPS_ALTITUDE, + ExifInterface.TAG_GPS_TIMESTAMP, + ExifInterface.TAG_GPS_SATELLITES, + ExifInterface.TAG_GPS_STATUS, + ExifInterface.TAG_GPS_MEASURE_MODE, + ExifInterface.TAG_GPS_DOP, + ExifInterface.TAG_GPS_SPEED_REF, + ExifInterface.TAG_GPS_SPEED, + ExifInterface.TAG_GPS_TRACK_REF, + ExifInterface.TAG_GPS_TRACK, + ExifInterface.TAG_GPS_IMG_DIRECTION_REF, + ExifInterface.TAG_GPS_IMG_DIRECTION, + ExifInterface.TAG_GPS_MAP_DATUM, + ExifInterface.TAG_GPS_DEST_LATITUDE_REF, + ExifInterface.TAG_GPS_DEST_LATITUDE, + ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, + ExifInterface.TAG_GPS_DEST_LONGITUDE, + ExifInterface.TAG_GPS_DEST_BEARING_REF, + ExifInterface.TAG_GPS_DEST_BEARING, + ExifInterface.TAG_GPS_DEST_DISTANCE_REF, + ExifInterface.TAG_GPS_DEST_DISTANCE, + ExifInterface.TAG_GPS_PROCESSING_METHOD, + ExifInterface.TAG_GPS_AREA_INFORMATION, + ExifInterface.TAG_GPS_DATESTAMP, + ExifInterface.TAG_GPS_DIFFERENTIAL, + ExifInterface.TAG_GPS_H_POSITIONING_ERROR, + ExifInterface.TAG_INTEROPERABILITY_INDEX, + ExifInterface.TAG_ORIENTATION); + for (String attribute : attributes) { + setIfNotNull(oldExif, newExif, attribute); } + + newExif.saveAttributes(); } private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) { diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index 6e04ed40a11a..b5024d6780c3 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -11,6 +11,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.SizeFCompat; +import androidx.exifinterface.media.ExifInterface; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -137,7 +138,11 @@ private FileOutputStream createOutputStream(File imageFile) throws IOException { } private void copyExif(String filePathOri, String filePathDest) { - exifDataCopier.copyExif(filePathOri, filePathDest); + try { + exifDataCopier.copyExif(new ExifInterface(filePathOri), new ExifInterface(filePathDest)); + } catch (Exception ex) { + Log.e("ImageResizer", "Error preserving Exif data on selected image: " + ex); + } } private SizeFCompat readFileDimensions(String path) { diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ExifDataCopierTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ExifDataCopierTest.java new file mode 100644 index 000000000000..4e2521e954d8 --- /dev/null +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ExifDataCopierTest.java @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.exifinterface.media.ExifInterface; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ExifDataCopierTest { + @Mock ExifInterface mockOldExif; + @Mock ExifInterface mockNewExif; + + ExifDataCopier exifDataCopier = new ExifDataCopier(); + + AutoCloseable mockCloseable; + + String orientationValue = "Horizontal (normal)"; + String imageWidthValue = "4032"; + String whitePointValue = "0.96419 1 0.82489"; + String colorSpaceValue = "Uncalibrated"; + String exposureTimeValue = "1/9"; + String exposureModeValue = "Auto"; + String exifVersionValue = "0232"; + String makeValue = "Apple"; + String dateTimeOriginalValue = "2023:02:14 18:55:19"; + String offsetTimeValue = "+01:00"; + + @Before + public void setUp() { + mockCloseable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + mockCloseable.close(); + } + + @Test + public void copyExif_copiesOrientationAttribute() throws IOException { + when(mockOldExif.getAttribute(ExifInterface.TAG_ORIENTATION)).thenReturn(orientationValue); + + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif).setAttribute(ExifInterface.TAG_ORIENTATION, orientationValue); + } + + @Test + public void copyExif_doesNotCopyCategory1AttributesExceptForOrientation() throws IOException { + when(mockOldExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).thenReturn(imageWidthValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_WHITE_POINT)).thenReturn(whitePointValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_COLOR_SPACE)).thenReturn(colorSpaceValue); + + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_IMAGE_WIDTH), any()); + verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_WHITE_POINT), any()); + verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_COLOR_SPACE), any()); + } + + @Test + public void copyExif_copiesCategory2Attributes() throws IOException { + when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).thenReturn(exposureTimeValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_MODE)).thenReturn(exposureModeValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_EXIF_VERSION)).thenReturn(exifVersionValue); + + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif).setAttribute(ExifInterface.TAG_EXPOSURE_TIME, exposureTimeValue); + verify(mockNewExif).setAttribute(ExifInterface.TAG_EXPOSURE_MODE, exposureModeValue); + verify(mockNewExif).setAttribute(ExifInterface.TAG_EXIF_VERSION, exifVersionValue); + } + + @Test + public void copyExif_copiesCategory3Attributes() throws IOException { + when(mockOldExif.getAttribute(ExifInterface.TAG_MAKE)).thenReturn(makeValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)) + .thenReturn(dateTimeOriginalValue); + when(mockOldExif.getAttribute(ExifInterface.TAG_OFFSET_TIME)).thenReturn(offsetTimeValue); + + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif).setAttribute(ExifInterface.TAG_MAKE, makeValue); + verify(mockNewExif).setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue); + verify(mockNewExif).setAttribute(ExifInterface.TAG_OFFSET_TIME, offsetTimeValue); + } + + @Test + public void copyExif_doesNotCopyUnsetAttributes() throws IOException { + when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).thenReturn(null); + + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_EXPOSURE_TIME), any()); + } + + @Test + public void copyExif_savesAttributes() throws IOException { + exifDataCopier.copyExif(mockOldExif, mockNewExif); + + verify(mockNewExif).saveAttributes(); + } +}