/* * Copyright (C) 2016 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 com.android.mediaframeworktest.unit; import com.android.mediaframeworktest.R; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.ExifInterface; import android.os.Environment; import android.test.AndroidTestCase; import android.util.Log; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import libcore.io.IoUtils; import libcore.io.Streams; public class ExifInterfaceTest extends AndroidTestCase { private static final String TAG = ExifInterface.class.getSimpleName(); private static final boolean VERBOSE = false; // lots of logging private static final double DIFFERENCE_TOLERANCE = .001; // List of files. 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 String VOLANTIS_JPEG = "volantis.jpg"; 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, R.raw.volantis }; private static final String[] IMAGE_FILENAMES = new String[] { EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG, VOLANTIS_JPEG }; 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 fNumber; 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); fNumber = 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(); } } @Override protected void setUp() throws Exception { for (int i = 0; i < IMAGE_RESOURCES.length; ++i) { String outputPath = new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i]).getAbsolutePath(); try (InputStream inputStream = getContext().getResources().openRawResource( IMAGE_RESOURCES[i])) { try (FileOutputStream outputStream = new FileOutputStream(outputPath)) { Streams.copy(inputStream, outputStream); } } } super.setUp(); } @Override protected 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(); } } super.tearDown(); } private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) { // Prints thumbnail information. if (exifInterface.hasThumbnail()) { byte[] thumbnailBytes = exifInterface.getThumbnail(); if (thumbnailBytes != null) { Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length); Bitmap bitmap = BitmapFactory.decodeByteArray( thumbnailBytes, 0, thumbnailBytes.length); 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.getThumbnail() != 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)); float[] latLong = new float[2]; if (exifInterface.getLatLong(latLong)) { 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 assertDoubleTag(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(); } 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.getThumbnail(); assertNotNull(thumbnailBytes); Bitmap thumbnailBitmap = BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.length); assertNotNull(thumbnailBitmap); assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth()); assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight()); } else { assertNull(exifInterface.getThumbnail()); } // Checks GPS information. float[] latLong = new float[2]; assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong)); 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); assertDoubleTag(exifInterface, ExifInterface.TAG_F_NUMBER, expectedValue.fNumber); assertStringTag(exifInterface, ExifInterface.TAG_DATETIME, expectedValue.datetime); assertDoubleTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime); assertDoubleTag(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(File imageFile, ExpectedValue expectedValue) throws IOException { String verboseTag = imageFile.getName(); // Creates via path. ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath()); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); // Creates from an asset file. InputStream in = null; try { in = mContext.getAssets().open(imageFile.getName()); exifInterface = new ExifInterface(in); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } finally { IoUtils.closeQuietly(in); } // Creates via InputStream. in = null; try { in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath())); exifInterface = new ExifInterface(in); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } finally { IoUtils.closeQuietly(in); } // Creates via FileDescriptor. FileDescriptor fd = null; try { fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDONLY, 0600); exifInterface = new ExifInterface(fd); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } finally { IoUtils.closeQuietly(fd); } } private void testSaveAttributes_withFileName(File imageFile, ExpectedValue expectedValue) throws IOException { 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 testSaveAttributes_withFileDescriptor(File imageFile, ExpectedValue expectedValue) throws IOException { String verboseTag = imageFile.getName(); FileDescriptor fd = null; try { fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600); ExifInterface exifInterface = new ExifInterface(fd); exifInterface.saveAttributes(); Os.lseek(fd, 0, OsConstants.SEEK_SET); exifInterface = new ExifInterface(fd); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); // Test for modifying one attribute. String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE); exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc"); exifInterface.saveAttributes(); Os.lseek(fd, 0, OsConstants.SEEK_SET); exifInterface = new ExifInterface(fd); assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE)); // Restore the backup value. exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue); exifInterface.saveAttributes(); Os.lseek(fd, 0, OsConstants.SEEK_SET); exifInterface = new ExifInterface(fd); compareWithExpectedValue(exifInterface, expectedValue, verboseTag); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } finally { IoUtils.closeQuietly(fd); } } private void testSaveAttributes_withInputStream(File imageFile, ExpectedValue expectedValue) throws IOException { InputStream in = null; try { in = getContext().getAssets().open(imageFile.getName()); ExifInterface exifInterface = new ExifInterface(in); exifInterface.saveAttributes(); } catch (UnsupportedOperationException e) { // Expected. saveAttributes is not supported with an ExifInterface object which was // created with InputStream. return; } finally { IoUtils.closeQuietly(in); } fail("Should not reach here!"); } private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId) throws IOException { ExpectedValue expectedValue = new ExpectedValue( getContext().getResources().obtainTypedArray(typedArrayResourceId)); File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); // Test for reading from various inputs. testExifInterfaceCommon(imageFile, expectedValue); // Test for saving attributes. testSaveAttributes_withFileName(imageFile, expectedValue); testSaveAttributes_withFileDescriptor(imageFile, expectedValue); testSaveAttributes_withInputStream(imageFile, expectedValue); } private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId) throws IOException { ExpectedValue expectedValue = new ExpectedValue( getContext().getResources().obtainTypedArray(typedArrayResourceId)); File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); // Test for reading from various inputs. testExifInterfaceCommon(imageFile, expectedValue); // Since ExifInterface does not support for saving attributes for RAW files, do not test // about writing back in here. } public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable { testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg); } public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable { testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg); } public void testReadExifDataFromLgG4Iso800Dng() throws Throwable { testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng); } 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!"); } } public void testReadExifDataFromVolantisJpg() throws Throwable { // Test if it is possible to parse the volantis generated JPEG smoothly. testExifInterfaceForJpeg(VOLANTIS_JPEG, R.array.volantis_jpg); } }