Skip to content

Commit

Permalink
Refactor exif data copier and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertOdrowaz committed Aug 28, 2023
1 parent 8dd1bf9 commit a82a0d5
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit a82a0d5

Please sign in to comment.