/* * Copyright (C) 2017 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 android.support.media; import static android.support.test.InstrumentationRegistry.getContext; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.fail; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.os.Environment; import android.support.exifinterface.test.R; import android.support.test.filters.LargeTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.util.Log; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Test {@link ExifInterface}. */ @RunWith(AndroidJUnit4.class) public class ExifInterfaceTest { private static final String TAG = ExifInterface.class.getSimpleName(); private static final boolean VERBOSE = false; // lots of logging private static final double DIFFERENCE_TOLERANCE = .001; private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg"; private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg"; private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng"; private static final int[] IMAGE_RESOURCES = new int[] { R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800}; private static final String[] IMAGE_FILENAMES = new String[] { EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG}; private static final String TEST_TEMP_FILE_NAME = "testImage"; private static final double DELTA = 1e-8; private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8; private static final double[] TEST_LATITUDE_VALID_VALUES = new double[] {0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737}; private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[] {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352}; private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001, 263.34763236326, -1e5, 347.32525, -176.346347754}; private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001, 263.34763236326, -1e10, 347.325252623, -4000.346323236}; private static final String[] EXIF_TAGS = { ExifInterface.TAG_MAKE, ExifInterface.TAG_MODEL, ExifInterface.TAG_F_NUMBER, ExifInterface.TAG_DATETIME, ExifInterface.TAG_EXPOSURE_TIME, ExifInterface.TAG_FLASH, ExifInterface.TAG_FOCAL_LENGTH, ExifInterface.TAG_GPS_ALTITUDE, ExifInterface.TAG_GPS_ALTITUDE_REF, ExifInterface.TAG_GPS_DATESTAMP, ExifInterface.TAG_GPS_LATITUDE, ExifInterface.TAG_GPS_LATITUDE_REF, ExifInterface.TAG_GPS_LONGITUDE, ExifInterface.TAG_GPS_LONGITUDE_REF, ExifInterface.TAG_GPS_PROCESSING_METHOD, ExifInterface.TAG_GPS_TIMESTAMP, ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.TAG_ISO_SPEED_RATINGS, ExifInterface.TAG_ORIENTATION, ExifInterface.TAG_WHITE_BALANCE }; private static class ExpectedValue { // Thumbnail information. public final boolean hasThumbnail; public final int thumbnailWidth; public final int thumbnailHeight; // GPS information. public final boolean hasLatLong; public final float latitude; public final float longitude; public final float altitude; // Values. public final String make; public final String model; public final float aperture; public final String datetime; public final float exposureTime; public final float flash; public final String focalLength; public final String gpsAltitude; public final String gpsAltitudeRef; public final String gpsDatestamp; public final String gpsLatitude; public final String gpsLatitudeRef; public final String gpsLongitude; public final String gpsLongitudeRef; public final String gpsProcessingMethod; public final String gpsTimestamp; public final int imageLength; public final int imageWidth; public final String iso; public final int orientation; public final int whiteBalance; private static String getString(TypedArray typedArray, int index) { String stringValue = typedArray.getString(index); if (stringValue == null || stringValue.equals("")) { return null; } return stringValue.trim(); } public ExpectedValue(TypedArray typedArray) { // Reads thumbnail information. hasThumbnail = typedArray.getBoolean(0, false); thumbnailWidth = typedArray.getInt(1, 0); thumbnailHeight = typedArray.getInt(2, 0); // Reads GPS information. hasLatLong = typedArray.getBoolean(3, false); latitude = typedArray.getFloat(4, 0f); longitude = typedArray.getFloat(5, 0f); altitude = typedArray.getFloat(6, 0f); // Reads values. make = getString(typedArray, 7); model = getString(typedArray, 8); aperture = typedArray.getFloat(9, 0f); datetime = getString(typedArray, 10); exposureTime = typedArray.getFloat(11, 0f); flash = typedArray.getFloat(12, 0f); focalLength = getString(typedArray, 13); gpsAltitude = getString(typedArray, 14); gpsAltitudeRef = getString(typedArray, 15); gpsDatestamp = getString(typedArray, 16); gpsLatitude = getString(typedArray, 17); gpsLatitudeRef = getString(typedArray, 18); gpsLongitude = getString(typedArray, 19); gpsLongitudeRef = getString(typedArray, 20); gpsProcessingMethod = getString(typedArray, 21); gpsTimestamp = getString(typedArray, 22); imageLength = typedArray.getInt(23, 0); imageWidth = typedArray.getInt(24, 0); iso = getString(typedArray, 25); orientation = typedArray.getInt(26, 0); whiteBalance = typedArray.getInt(27, 0); typedArray.recycle(); } } @Before public void setUp() throws Exception { for (int i = 0; i < IMAGE_RESOURCES.length; ++i) { String outputPath = new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i]) .getAbsolutePath(); InputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = getContext().getResources().openRawResource(IMAGE_RESOURCES[i]); outputStream = new FileOutputStream(outputPath); copy(inputStream, outputStream); } finally { closeQuietly(inputStream); closeQuietly(outputStream); } } } @After public void tearDown() throws Exception { for (int i = 0; i < IMAGE_RESOURCES.length; ++i) { String imageFilePath = new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i]) .getAbsolutePath(); File imageFile = new File(imageFilePath); if (imageFile.exists()) { imageFile.delete(); } } } @Test @LargeTest public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable { testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg); } @Test @LargeTest public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable { testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg); } @Test @LargeTest public void testReadExifDataFromLgG4Iso800Dng() throws Throwable { testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng); } @Test @SmallTest public void testDoNotFailOnCorruptedImage() throws Throwable { // To keep the compatibility with old versions of ExifInterface, even on a corrupted image, // it shouldn't raise any exceptions except an IOException when unable to open a file. byte[] bytes = new byte[1024]; try { new ExifInterface(new ByteArrayInputStream(bytes)); // Always success } catch (IOException e) { fail("Should not reach here!"); } } @Test @SmallTest public void testSetLatLong_withValidValues() throws IOException { for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { ExifInterface exif = createTestExifInterface(); exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]); double[] latLong = exif.getLatLong(); assertNotNull(latLong); assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA); assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA); } } @Test @SmallTest public void testSetLatLong_withInvalidLatitude() throws IOException { for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { ExifInterface exif = createTestExifInterface(); try { exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]); fail(); } catch (IllegalArgumentException e) { // expected } assertNull(exif.getLatLong()); assertLatLongValuesAreNotSet(exif); } } @Test @SmallTest public void testSetLatLong_withInvalidLongitude() throws IOException { for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { ExifInterface exif = createTestExifInterface(); try { exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]); fail(); } catch (IllegalArgumentException e) { // expected } assertNull(exif.getLatLong()); assertLatLongValuesAreNotSet(exif); } } private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) { // Prints thumbnail information. if (exifInterface.hasThumbnail()) { byte[] thumbnailBytes = exifInterface.getThumbnailBytes(); if (thumbnailBytes != null) { Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length); Bitmap bitmap = exifInterface.getThumbnailBitmap(); if (bitmap == null) { Log.e(TAG, fileName + " Corrupted thumbnail!"); } else { Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", " + bitmap.getHeight()); } } else { Log.e(TAG, fileName + " Unexpected result: No thumbnails were found. " + "A thumbnail is expected."); } } else { if (exifInterface.getThumbnailBytes() != null) { Log.e(TAG, fileName + " Unexpected result: A thumbnail was found. " + "No thumbnail is expected."); } else { Log.v(TAG, fileName + " No thumbnail"); } } // Prints GPS information. Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0)); double[] latLong = exifInterface.getLatLong(); if (latLong != null) { Log.v(TAG, fileName + " Latitude = " + latLong[0]); Log.v(TAG, fileName + " Longitude = " + latLong[1]); } else { Log.v(TAG, fileName + " No latlong data"); } // Prints values. for (String tagKey : EXIF_TAGS) { String tagValue = exifInterface.getAttribute(tagKey); Log.v(TAG, fileName + " Key{" + tagKey + "} = '" + tagValue + "'"); } } private void assertIntTag(ExifInterface exifInterface, String tag, int expectedValue) { int intValue = exifInterface.getAttributeInt(tag, 0); assertEquals(expectedValue, intValue); } private void assertFloatTag(ExifInterface exifInterface, String tag, float expectedValue) { double doubleValue = exifInterface.getAttributeDouble(tag, 0.0); assertEquals(expectedValue, doubleValue, DIFFERENCE_TOLERANCE); } private void assertStringTag(ExifInterface exifInterface, String tag, String expectedValue) { String stringValue = exifInterface.getAttribute(tag); if (stringValue != null) { stringValue = stringValue.trim(); } stringValue = (stringValue == "") ? null : stringValue; assertEquals(expectedValue, stringValue); } private void compareWithExpectedValue(ExifInterface exifInterface, ExpectedValue expectedValue, String verboseTag) { if (VERBOSE) { printExifTagsAndValues(verboseTag, exifInterface); } // Checks a thumbnail image. assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail()); if (expectedValue.hasThumbnail) { byte[] thumbnailBytes = exifInterface.getThumbnailBytes(); assertNotNull(thumbnailBytes); Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap(); assertNotNull(thumbnailBitmap); assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth()); assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight()); } else { assertNull(exifInterface.getThumbnail()); } // Checks GPS information. double[] latLong = exifInterface.getLatLong(); assertEquals(expectedValue.hasLatLong, latLong != null); if (expectedValue.hasLatLong) { assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE); assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE); } assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE); // Checks values. assertStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make); assertStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model); assertFloatTag(exifInterface, ExifInterface.TAG_F_NUMBER, expectedValue.aperture); assertStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime); assertFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime); assertFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash); assertStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength); assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude); assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF, expectedValue.gpsAltitudeRef); assertStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP, expectedValue.gpsDatestamp); assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude); assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF, expectedValue.gpsLatitudeRef); assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE, expectedValue.gpsLongitude); assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF, expectedValue.gpsLongitudeRef); assertStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD, expectedValue.gpsProcessingMethod); assertStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP, expectedValue.gpsTimestamp); assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength); assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth); assertStringTag(exifInterface, ExifInterface.TAG_ISO_SPEED_RATINGS, expectedValue.iso); assertIntTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation); assertIntTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE, expectedValue.whiteBalance); } private void testExifInterfaceCommon(String fileName, ExpectedValue expectedValue) throws IOException { File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); String verboseTag = imageFile.getName(); // Creates via path. ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath()); assertNotNull(exifInterface); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); InputStream in = null; // Creates via InputStream. try { in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath())); exifInterface = new ExifInterface(in); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } finally { closeQuietly(in); } } private void testSaveAttributes_withFileName(String fileName, ExpectedValue expectedValue) throws IOException { File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); String verboseTag = imageFile.getName(); ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath()); exifInterface.saveAttributes(); exifInterface = new ExifInterface(imageFile.getAbsolutePath()); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); // Test for modifying one attribute. String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE); exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc"); exifInterface.saveAttributes(); exifInterface = new ExifInterface(imageFile.getAbsolutePath()); assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE)); // Restore the backup value. exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue); exifInterface.saveAttributes(); exifInterface = new ExifInterface(imageFile.getAbsolutePath()); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId) throws IOException { ExpectedValue expectedValue = new ExpectedValue( getContext().getResources().obtainTypedArray(typedArrayResourceId)); // Test for reading from external data storage. testExifInterfaceCommon(fileName, expectedValue); // Test for saving attributes. testSaveAttributes_withFileName(fileName, expectedValue); } private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId) throws IOException { ExpectedValue expectedValue = new ExpectedValue( getContext().getResources().obtainTypedArray(typedArrayResourceId)); // Test for reading from external data storage. testExifInterfaceCommon(fileName, expectedValue); // Since ExifInterface does not support for saving attributes for RAW files, do not test // about writing back in here. } private void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (RuntimeException rethrown) { throw rethrown; } catch (Exception ignored) { } } } private int copy(InputStream in, OutputStream out) throws IOException { int total = 0; byte[] buffer = new byte[8192]; int c; while ((c = in.read(buffer)) != -1) { total += c; out.write(buffer, 0, c); } return total; } private void assertLatLongValuesAreNotSet(ExifInterface exif) { assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)); assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF)); } private ExifInterface createTestExifInterface() throws IOException { File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg"); image.deleteOnExit(); return new ExifInterface(image.getAbsolutePath()); } }