Skip to content

Commit 4512e4d

Browse files
[image_picker] Copy exif tags in categories II and III (flutter#4738)
Added all exif tags from Category II and Category III (excluding ones that don't have consts in `ExifInterface` WaterDepth etc.). Those tags include data not related to the structure of the file itself so resizing definitely doesn't invalidate them and thus they should be copied over. I've also switched from list of raw strings to consts provided be the `ExifInterface` I'm not sure show to handle `TAG_ISO_SPEED_RATINGS` deprecation in this case. The const is deprecated and the tag itself is deprecated in the standard however it would be reasonable to copy it as well to handle legacy files which include it. It was also being copied in the previous versions of this package. *List which issues are fixed by this PR. You must list at least one issue.* flutter#132827 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
1 parent 8d553e3 commit 4512e4d

File tree

5 files changed

+251
-37
lines changed

5 files changed

+251
-37
lines changed

packages/image_picker/image_picker_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.8
2+
3+
* Adds additional category II and III exif tags to be copied during photo resize.
4+
15
## 0.8.7+5
26

37
* Adds pub topics to package metadata.

packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java

Lines changed: 126 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,138 @@
44

55
package io.flutter.plugins.imagepicker;
66

7-
import android.util.Log;
87
import androidx.exifinterface.media.ExifInterface;
8+
import java.io.IOException;
99
import java.util.Arrays;
1010
import java.util.List;
1111

1212
class ExifDataCopier {
13-
void copyExif(String filePathOri, String filePathDest) {
14-
try {
15-
ExifInterface oldExif = new ExifInterface(filePathOri);
16-
ExifInterface newExif = new ExifInterface(filePathDest);
17-
18-
List<String> attributes =
19-
Arrays.asList(
20-
"FNumber",
21-
"ExposureTime",
22-
"ISOSpeedRatings",
23-
"GPSAltitude",
24-
"GPSAltitudeRef",
25-
"FocalLength",
26-
"GPSDateStamp",
27-
"WhiteBalance",
28-
"GPSProcessingMethod",
29-
"GPSTimeStamp",
30-
"DateTime",
31-
"Flash",
32-
"GPSLatitude",
33-
"GPSLatitudeRef",
34-
"GPSLongitude",
35-
"GPSLongitudeRef",
36-
"Make",
37-
"Model",
38-
"Orientation");
39-
for (String attribute : attributes) {
40-
setIfNotNull(oldExif, newExif, attribute);
41-
}
42-
43-
newExif.saveAttributes();
44-
45-
} catch (Exception ex) {
46-
Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex);
13+
/**
14+
* Copies all exif data not related to image structure and orientation tag. Data not related to
15+
* image structure consists of category II (Shooting condition related metadata) and category III
16+
* (Metadata storing other information) tags. Category I tags are not copied because they may be
17+
* invalidated as a result of resizing. The exception is the orientation tag which is known to not
18+
* be invalidated and is crucial for proper display of the image.
19+
*
20+
* <p>The categories mentioned refer to standard "CIPA DC-008-Translation-2012 Exchangeable image
21+
* file format for digital still cameras: Exif Version 2.3"
22+
* https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf. Version 2.3 has been chosen because
23+
* {@code ExifInterface} is based on it.
24+
*/
25+
void copyExif(ExifInterface oldExif, ExifInterface newExif) throws IOException {
26+
@SuppressWarnings("deprecation")
27+
List<String> attributes =
28+
Arrays.asList(
29+
ExifInterface.TAG_IMAGE_DESCRIPTION,
30+
ExifInterface.TAG_MAKE,
31+
ExifInterface.TAG_MODEL,
32+
ExifInterface.TAG_SOFTWARE,
33+
ExifInterface.TAG_DATETIME,
34+
ExifInterface.TAG_ARTIST,
35+
ExifInterface.TAG_COPYRIGHT,
36+
ExifInterface.TAG_EXPOSURE_TIME,
37+
ExifInterface.TAG_F_NUMBER,
38+
ExifInterface.TAG_EXPOSURE_PROGRAM,
39+
ExifInterface.TAG_SPECTRAL_SENSITIVITY,
40+
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
41+
ExifInterface.TAG_ISO_SPEED_RATINGS,
42+
ExifInterface.TAG_OECF,
43+
ExifInterface.TAG_SENSITIVITY_TYPE,
44+
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
45+
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
46+
ExifInterface.TAG_ISO_SPEED,
47+
ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
48+
ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
49+
ExifInterface.TAG_EXIF_VERSION,
50+
ExifInterface.TAG_DATETIME_ORIGINAL,
51+
ExifInterface.TAG_DATETIME_DIGITIZED,
52+
ExifInterface.TAG_OFFSET_TIME,
53+
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
54+
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
55+
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
56+
ExifInterface.TAG_APERTURE_VALUE,
57+
ExifInterface.TAG_BRIGHTNESS_VALUE,
58+
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
59+
ExifInterface.TAG_MAX_APERTURE_VALUE,
60+
ExifInterface.TAG_SUBJECT_DISTANCE,
61+
ExifInterface.TAG_METERING_MODE,
62+
ExifInterface.TAG_LIGHT_SOURCE,
63+
ExifInterface.TAG_FLASH,
64+
ExifInterface.TAG_FOCAL_LENGTH,
65+
ExifInterface.TAG_MAKER_NOTE,
66+
ExifInterface.TAG_USER_COMMENT,
67+
ExifInterface.TAG_SUBSEC_TIME,
68+
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
69+
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
70+
ExifInterface.TAG_FLASHPIX_VERSION,
71+
ExifInterface.TAG_FLASH_ENERGY,
72+
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
73+
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
74+
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
75+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
76+
ExifInterface.TAG_EXPOSURE_INDEX,
77+
ExifInterface.TAG_SENSING_METHOD,
78+
ExifInterface.TAG_FILE_SOURCE,
79+
ExifInterface.TAG_SCENE_TYPE,
80+
ExifInterface.TAG_CFA_PATTERN,
81+
ExifInterface.TAG_CUSTOM_RENDERED,
82+
ExifInterface.TAG_EXPOSURE_MODE,
83+
ExifInterface.TAG_WHITE_BALANCE,
84+
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
85+
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
86+
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
87+
ExifInterface.TAG_GAIN_CONTROL,
88+
ExifInterface.TAG_CONTRAST,
89+
ExifInterface.TAG_SATURATION,
90+
ExifInterface.TAG_SHARPNESS,
91+
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
92+
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
93+
ExifInterface.TAG_IMAGE_UNIQUE_ID,
94+
ExifInterface.TAG_CAMERA_OWNER_NAME,
95+
ExifInterface.TAG_BODY_SERIAL_NUMBER,
96+
ExifInterface.TAG_LENS_SPECIFICATION,
97+
ExifInterface.TAG_LENS_MAKE,
98+
ExifInterface.TAG_LENS_MODEL,
99+
ExifInterface.TAG_LENS_SERIAL_NUMBER,
100+
ExifInterface.TAG_GPS_VERSION_ID,
101+
ExifInterface.TAG_GPS_LATITUDE_REF,
102+
ExifInterface.TAG_GPS_LATITUDE,
103+
ExifInterface.TAG_GPS_LONGITUDE_REF,
104+
ExifInterface.TAG_GPS_LONGITUDE,
105+
ExifInterface.TAG_GPS_ALTITUDE_REF,
106+
ExifInterface.TAG_GPS_ALTITUDE,
107+
ExifInterface.TAG_GPS_TIMESTAMP,
108+
ExifInterface.TAG_GPS_SATELLITES,
109+
ExifInterface.TAG_GPS_STATUS,
110+
ExifInterface.TAG_GPS_MEASURE_MODE,
111+
ExifInterface.TAG_GPS_DOP,
112+
ExifInterface.TAG_GPS_SPEED_REF,
113+
ExifInterface.TAG_GPS_SPEED,
114+
ExifInterface.TAG_GPS_TRACK_REF,
115+
ExifInterface.TAG_GPS_TRACK,
116+
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
117+
ExifInterface.TAG_GPS_IMG_DIRECTION,
118+
ExifInterface.TAG_GPS_MAP_DATUM,
119+
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
120+
ExifInterface.TAG_GPS_DEST_LATITUDE,
121+
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
122+
ExifInterface.TAG_GPS_DEST_LONGITUDE,
123+
ExifInterface.TAG_GPS_DEST_BEARING_REF,
124+
ExifInterface.TAG_GPS_DEST_BEARING,
125+
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
126+
ExifInterface.TAG_GPS_DEST_DISTANCE,
127+
ExifInterface.TAG_GPS_PROCESSING_METHOD,
128+
ExifInterface.TAG_GPS_AREA_INFORMATION,
129+
ExifInterface.TAG_GPS_DATESTAMP,
130+
ExifInterface.TAG_GPS_DIFFERENTIAL,
131+
ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
132+
ExifInterface.TAG_INTEROPERABILITY_INDEX,
133+
ExifInterface.TAG_ORIENTATION);
134+
for (String attribute : attributes) {
135+
setIfNotNull(oldExif, newExif, attribute);
47136
}
137+
138+
newExif.saveAttributes();
48139
}
49140

50141
private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) {

packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import androidx.annotation.NonNull;
1212
import androidx.annotation.Nullable;
1313
import androidx.core.util.SizeFCompat;
14+
import androidx.exifinterface.media.ExifInterface;
1415
import java.io.ByteArrayOutputStream;
1516
import java.io.File;
1617
import java.io.FileOutputStream;
@@ -137,7 +138,11 @@ private FileOutputStream createOutputStream(File imageFile) throws IOException {
137138
}
138139

139140
private void copyExif(String filePathOri, String filePathDest) {
140-
exifDataCopier.copyExif(filePathOri, filePathDest);
141+
try {
142+
exifDataCopier.copyExif(new ExifInterface(filePathOri), new ExifInterface(filePathDest));
143+
} catch (Exception ex) {
144+
Log.e("ImageResizer", "Error preserving Exif data on selected image: " + ex);
145+
}
141146
}
142147

143148
private SizeFCompat readFileDimensions(String path) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.imagepicker;
6+
7+
import static org.mockito.ArgumentMatchers.any;
8+
import static org.mockito.ArgumentMatchers.eq;
9+
import static org.mockito.Mockito.never;
10+
import static org.mockito.Mockito.verify;
11+
import static org.mockito.Mockito.when;
12+
13+
import androidx.exifinterface.media.ExifInterface;
14+
import java.io.IOException;
15+
import org.junit.After;
16+
import org.junit.Before;
17+
import org.junit.Test;
18+
import org.mockito.Mock;
19+
import org.mockito.MockitoAnnotations;
20+
21+
public class ExifDataCopierTest {
22+
@Mock ExifInterface mockOldExif;
23+
@Mock ExifInterface mockNewExif;
24+
25+
ExifDataCopier exifDataCopier = new ExifDataCopier();
26+
27+
AutoCloseable mockCloseable;
28+
29+
String orientationValue = "Horizontal (normal)";
30+
String imageWidthValue = "4032";
31+
String whitePointValue = "0.96419 1 0.82489";
32+
String colorSpaceValue = "Uncalibrated";
33+
String exposureTimeValue = "1/9";
34+
String exposureModeValue = "Auto";
35+
String exifVersionValue = "0232";
36+
String makeValue = "Apple";
37+
String dateTimeOriginalValue = "2023:02:14 18:55:19";
38+
String offsetTimeValue = "+01:00";
39+
40+
@Before
41+
public void setUp() {
42+
mockCloseable = MockitoAnnotations.openMocks(this);
43+
}
44+
45+
@After
46+
public void tearDown() throws Exception {
47+
mockCloseable.close();
48+
}
49+
50+
@Test
51+
public void copyExif_copiesOrientationAttribute() throws IOException {
52+
when(mockOldExif.getAttribute(ExifInterface.TAG_ORIENTATION)).thenReturn(orientationValue);
53+
54+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
55+
56+
verify(mockNewExif).setAttribute(ExifInterface.TAG_ORIENTATION, orientationValue);
57+
}
58+
59+
@Test
60+
public void copyExif_doesNotCopyCategory1AttributesExceptForOrientation() throws IOException {
61+
when(mockOldExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).thenReturn(imageWidthValue);
62+
when(mockOldExif.getAttribute(ExifInterface.TAG_WHITE_POINT)).thenReturn(whitePointValue);
63+
when(mockOldExif.getAttribute(ExifInterface.TAG_COLOR_SPACE)).thenReturn(colorSpaceValue);
64+
65+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
66+
67+
verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_IMAGE_WIDTH), any());
68+
verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_WHITE_POINT), any());
69+
verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_COLOR_SPACE), any());
70+
}
71+
72+
@Test
73+
public void copyExif_copiesCategory2Attributes() throws IOException {
74+
when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).thenReturn(exposureTimeValue);
75+
when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_MODE)).thenReturn(exposureModeValue);
76+
when(mockOldExif.getAttribute(ExifInterface.TAG_EXIF_VERSION)).thenReturn(exifVersionValue);
77+
78+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
79+
80+
verify(mockNewExif).setAttribute(ExifInterface.TAG_EXPOSURE_TIME, exposureTimeValue);
81+
verify(mockNewExif).setAttribute(ExifInterface.TAG_EXPOSURE_MODE, exposureModeValue);
82+
verify(mockNewExif).setAttribute(ExifInterface.TAG_EXIF_VERSION, exifVersionValue);
83+
}
84+
85+
@Test
86+
public void copyExif_copiesCategory3Attributes() throws IOException {
87+
when(mockOldExif.getAttribute(ExifInterface.TAG_MAKE)).thenReturn(makeValue);
88+
when(mockOldExif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL))
89+
.thenReturn(dateTimeOriginalValue);
90+
when(mockOldExif.getAttribute(ExifInterface.TAG_OFFSET_TIME)).thenReturn(offsetTimeValue);
91+
92+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
93+
94+
verify(mockNewExif).setAttribute(ExifInterface.TAG_MAKE, makeValue);
95+
verify(mockNewExif).setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
96+
verify(mockNewExif).setAttribute(ExifInterface.TAG_OFFSET_TIME, offsetTimeValue);
97+
}
98+
99+
@Test
100+
public void copyExif_doesNotCopyUnsetAttributes() throws IOException {
101+
when(mockOldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).thenReturn(null);
102+
103+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
104+
105+
verify(mockNewExif, never()).setAttribute(eq(ExifInterface.TAG_EXPOSURE_TIME), any());
106+
}
107+
108+
@Test
109+
public void copyExif_savesAttributes() throws IOException {
110+
exifDataCopier.copyExif(mockOldExif, mockNewExif);
111+
112+
verify(mockNewExif).saveAttributes();
113+
}
114+
}

packages/image_picker/image_picker_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
55

6-
version: 0.8.7+5
6+
version: 0.8.8
77

88
environment:
99
sdk: ">=2.19.0 <4.0.0"

0 commit comments

Comments
 (0)