blob: 75ba4b13aa99983515ecacea99fd7cda0e0a29ae [file] [log] [blame]
/*
* 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 android.media.cts;
import static android.media.ExifInterface.TAG_SUBJECT_AREA;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.os.FileUtils;
import android.os.StrictMode;
import android.platform.test.annotations.AppModeFull;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.test.AndroidTestCase;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@NonMediaMainlineTest
@AppModeFull(reason = "Instant apps cannot access the SD card")
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;
static final String mInpPrefix = WorkDir.getMediaDirString() + "images/";
// This base directory is needed for the files listed below.
// These files will be available for download in Android O release.
// Link: https://source.android.com/compatibility/cts/downloads.html#cts-media-files
private static final String JPEG_WITH_EXIF_BYTE_ORDER_II = "image_exif_byte_order_ii.jpg";
private static final String JPEG_WITH_EXIF_BYTE_ORDER_MM = "image_exif_byte_order_mm.jpg";
private static final String DNG_WITH_EXIF_WITH_XMP = "lg_g4_iso_800.dng";
private static final String JPEG_WITH_EXIF_WITH_XMP = "lg_g4_iso_800.jpg";
private static final String ARW_SONY_RX_100 = "sony_rx_100.arw";
private static final String CR2_CANON_G7X = "canon_g7x.cr2";
private static final String RAF_FUJI_X20 = "fuji_x20.raf";
private static final String NEF_NIKON_1AW1 = "nikon_1aw1.nef";
private static final String NRW_NIKON_P330 = "nikon_p330.nrw";
private static final String ORF_OLYMPUS_E_PL3 = "olympus_e_pl3.orf";
private static final String RW2_PANASONIC_GM5 = "panasonic_gm5.rw2";
private static final String PEF_PENTAX_K5 = "pentax_k5.pef";
private static final String SRW_SAMSUNG_NX3000 = "samsung_nx3000.srw";
private static final String JPEG_VOLANTIS = "volantis.jpg";
private static final String WEBP_WITH_EXIF = "webp_with_exif.webp";
private static final String WEBP_WITHOUT_EXIF_WITH_ANIM_DATA =
"webp_with_anim_without_exif.webp";
private static final String WEBP_WITHOUT_EXIF = "webp_without_exif.webp";
private static final String WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING =
"webp_lossless_without_exif.webp";
private static final String PNG_WITH_EXIF_BYTE_ORDER_II = "png_with_exif_byte_order_ii.png";
private static final String PNG_WITHOUT_EXIF = "png_without_exif.png";
private static final String JPEG_WITH_DATETIME_TAG = "jpeg_with_datetime_tag.jpg";
private static final String[] EXIF_TAGS = {
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_DATETIME_ORIGINAL,
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;
public final boolean isThumbnailCompressed;
public final int thumbnailOffset;
public final int thumbnailLength;
// GPS information.
public final boolean hasLatLong;
public final float latitude;
public final int latitudeOffset;
public final int latitudeLength;
public final float longitude;
public final float altitude;
// Make information
public final boolean hasMake;
public final int makeOffset;
public final int makeLength;
public final String make;
// Values.
public final String model;
public final float aperture;
public final String dateTimeOriginal;
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;
// XMP information.
public final boolean hasXmp;
public final int xmpOffset;
public final int xmpLength;
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) {
int index = 0;
// Reads thumbnail information.
hasThumbnail = typedArray.getBoolean(index++, false);
thumbnailOffset = typedArray.getInt(index++, -1);
thumbnailLength = typedArray.getInt(index++, -1);
thumbnailWidth = typedArray.getInt(index++, 0);
thumbnailHeight = typedArray.getInt(index++, 0);
isThumbnailCompressed = typedArray.getBoolean(index++, false);
// Reads GPS information.
hasLatLong = typedArray.getBoolean(index++, false);
latitudeOffset = typedArray.getInt(index++, -1);
latitudeLength = typedArray.getInt(index++, -1);
latitude = typedArray.getFloat(index++, 0f);
longitude = typedArray.getFloat(index++, 0f);
altitude = typedArray.getFloat(index++, 0f);
// Reads Make information.
hasMake = typedArray.getBoolean(index++, false);
makeOffset = typedArray.getInt(index++, -1);
makeLength = typedArray.getInt(index++, -1);
make = getString(typedArray, index++);
// Reads values.
model = getString(typedArray, index++);
aperture = typedArray.getFloat(index++, 0f);
dateTimeOriginal = getString(typedArray, index++);
exposureTime = typedArray.getFloat(index++, 0f);
flash = typedArray.getFloat(index++, 0f);
focalLength = getString(typedArray, index++);
gpsAltitude = getString(typedArray, index++);
gpsAltitudeRef = getString(typedArray, index++);
gpsDatestamp = getString(typedArray, index++);
gpsLatitude = getString(typedArray, index++);
gpsLatitudeRef = getString(typedArray, index++);
gpsLongitude = getString(typedArray, index++);
gpsLongitudeRef = getString(typedArray, index++);
gpsProcessingMethod = getString(typedArray, index++);
gpsTimestamp = getString(typedArray, index++);
imageLength = typedArray.getInt(index++, 0);
imageWidth = typedArray.getInt(index++, 0);
iso = getString(typedArray, index++);
orientation = typedArray.getInt(index++, 0);
whiteBalance = typedArray.getInt(index++, 0);
// Reads XMP information.
hasXmp = typedArray.getBoolean(index++, false);
xmpOffset = typedArray.getInt(index++, 0);
xmpLength = typedArray.getInt(index++, 0);
typedArray.recycle();
}
}
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));
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 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, boolean assertRanges) {
if (VERBOSE) {
printExifTagsAndValues(verboseTag, exifInterface);
}
// Checks a thumbnail image.
assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
if (expectedValue.hasThumbnail) {
assertNotNull(exifInterface.getThumbnailRange());
if (assertRanges) {
final long[] thumbnailRange = exifInterface.getThumbnailRange();
assertEquals(expectedValue.thumbnailOffset, thumbnailRange[0]);
assertEquals(expectedValue.thumbnailLength, thumbnailRange[1]);
}
testThumbnail(expectedValue, exifInterface);
} else {
assertNull(exifInterface.getThumbnailRange());
assertNull(exifInterface.getThumbnail());
assertNull(exifInterface.getThumbnailBitmap());
assertFalse(exifInterface.isThumbnailCompressed());
}
// Checks GPS information.
float[] latLong = new float[2];
assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
if (expectedValue.hasLatLong) {
assertNotNull(exifInterface.getAttributeRange(ExifInterface.TAG_GPS_LATITUDE));
if (assertRanges) {
final long[] latitudeRange = exifInterface
.getAttributeRange(ExifInterface.TAG_GPS_LATITUDE);
assertEquals(expectedValue.latitudeOffset, latitudeRange[0]);
assertEquals(expectedValue.latitudeLength, latitudeRange[1]);
}
assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
assertTrue(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LATITUDE));
assertTrue(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LONGITUDE));
} else {
assertNull(exifInterface.getAttributeRange(ExifInterface.TAG_GPS_LATITUDE));
assertFalse(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LATITUDE));
assertFalse(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LONGITUDE));
}
assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
// Checks Make information.
String make = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
assertEquals(expectedValue.hasMake, make != null);
if (expectedValue.hasMake) {
assertNotNull(exifInterface.getAttributeRange(ExifInterface.TAG_MAKE));
if (assertRanges) {
final long[] makeRange = exifInterface
.getAttributeRange(ExifInterface.TAG_MAKE);
assertEquals(expectedValue.makeOffset, makeRange[0]);
assertEquals(expectedValue.makeLength, makeRange[1]);
}
assertEquals(expectedValue.make, make.trim());
} else {
assertNull(exifInterface.getAttributeRange(ExifInterface.TAG_MAKE));
assertFalse(exifInterface.hasAttribute(ExifInterface.TAG_MAKE));
}
// 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_ORIGINAL,
expectedValue.dateTimeOriginal);
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);
if (expectedValue.hasXmp) {
assertNotNull(exifInterface.getAttributeRange(ExifInterface.TAG_XMP));
if (assertRanges) {
final long[] xmpRange = exifInterface.getAttributeRange(ExifInterface.TAG_XMP);
assertEquals(expectedValue.xmpOffset, xmpRange[0]);
assertEquals(expectedValue.xmpLength, xmpRange[1]);
}
final String xmp = new String(exifInterface.getAttributeBytes(ExifInterface.TAG_XMP),
StandardCharsets.UTF_8);
// We're only interested in confirming that we were able to extract
// valid XMP data, which must always include this XML tag; a full
// XMP parser is beyond the scope of ExifInterface. See XMP
// Specification Part 1, Section C.2.2 for additional details.
if (!xmp.contains("<rdf:RDF")) {
fail("Invalid XMP: " + xmp);
}
} else {
assertNull(exifInterface.getAttributeRange(ExifInterface.TAG_XMP));
}
}
private void readFromStandaloneDataWithExif(String fileName, int typedArrayResourceId)
throws IOException {
ExpectedValue expectedValue = new ExpectedValue(
getContext().getResources().obtainTypedArray(typedArrayResourceId));
Preconditions.assertTestFileExists(mInpPrefix + fileName);
File imageFile = new File(mInpPrefix, fileName);
String verboseTag = imageFile.getName();
FileInputStream fis = new FileInputStream(imageFile);
// Skip the following marker bytes (0xff, 0xd8, 0xff, 0xe1)
fis.skip(4);
// Read the value of the length of the exif data
short length = readShort(fis);
byte[] exifBytes = new byte[length];
fis.read(exifBytes);
ByteArrayInputStream bin = new ByteArrayInputStream(exifBytes);
ExifInterface exifInterface =
new ExifInterface(bin, ExifInterface.STREAM_TYPE_EXIF_DATA_ONLY);
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, true);
}
private void testExifInterfaceCommon(String fileName, ExpectedValue expectedValue)
throws IOException {
File imageFile = new File(mInpPrefix, fileName);
Preconditions.assertTestFileExists(mInpPrefix + fileName);
String verboseTag = imageFile.getName();
// Creates via path.
ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
assertNotNull(exifInterface);
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, true);
// Creates via file.
exifInterface = new ExifInterface(imageFile);
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, true);
InputStream in = null;
// Creates via InputStream.
try {
in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
exifInterface = new ExifInterface(in);
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, true);
} 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, true);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
} finally {
IoUtils.closeQuietly(fd);
}
}
private void testExifInterfaceRange(String fileName, ExpectedValue expectedValue)
throws IOException {
Preconditions.assertTestFileExists(mInpPrefix + fileName);
File imageFile = new File(mInpPrefix, fileName);
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
if (expectedValue.hasThumbnail) {
in.skip(expectedValue.thumbnailOffset);
byte[] thumbnailBytes = new byte[expectedValue.thumbnailLength];
if (in.read(thumbnailBytes) != expectedValue.thumbnailLength) {
throw new IOException("Failed to read the expected thumbnail length");
}
// TODO: Need a way to check uncompressed thumbnail file
if (expectedValue.isThumbnailCompressed) {
Bitmap thumbnailBitmap = BitmapFactory.decodeByteArray(thumbnailBytes, 0,
thumbnailBytes.length);
assertNotNull(thumbnailBitmap);
assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
}
}
// TODO: Creating a new input stream is a temporary
// workaround for BufferedInputStream#mark/reset not working properly for
// LG_G4_ISO_800_DNG. Need to investigate cause.
in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
if (expectedValue.hasMake) {
in.skip(expectedValue.makeOffset);
byte[] makeBytes = new byte[expectedValue.makeLength];
if (in.read(makeBytes) != expectedValue.makeLength) {
throw new IOException("Failed to read the expected make length");
}
String makeString = new String(makeBytes);
// Remove null bytes
makeString = makeString.replaceAll("\u0000.*", "");
assertEquals(expectedValue.make, makeString.trim());
}
in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
if (expectedValue.hasXmp) {
in.skip(expectedValue.xmpOffset);
byte[] identifierBytes = new byte[expectedValue.xmpLength];
if (in.read(identifierBytes) != expectedValue.xmpLength) {
throw new IOException("Failed to read the expected xmp length");
}
final String xmpIdentifier = "<?xpacket begin=";
assertTrue(new String(identifierBytes, StandardCharsets.UTF_8)
.startsWith(xmpIdentifier));
}
// TODO: Add code for retrieving raw latitude data using offset and length
} finally {
IoUtils.closeQuietly(in);
}
}
private void writeToFilesWithExif(String fileName, int typedArrayResourceId)
throws IOException {
ExpectedValue expectedValue = new ExpectedValue(
getContext().getResources().obtainTypedArray(typedArrayResourceId));
Preconditions.assertTestFileExists(mInpPrefix + fileName);
File srcFile = new File(mInpPrefix, fileName);
File imageFile = clone(srcFile);
String verboseTag = imageFile.getName();
ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
exifInterface.saveAttributes();
exifInterface = new ExifInterface(imageFile.getAbsolutePath());
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, false);
// Test for modifying one attribute.
String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
exifInterface.saveAttributes();
// Check if thumbnail offset and length are properly updated without parsing the data again.
if (expectedValue.hasThumbnail) {
testThumbnail(expectedValue, exifInterface);
}
exifInterface = new ExifInterface(imageFile.getAbsolutePath());
assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
// Check if thumbnail bytes can be retrieved from the new thumbnail range.
if (expectedValue.hasThumbnail) {
testThumbnail(expectedValue, exifInterface);
}
// Restore the backup value.
exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue);
exifInterface.saveAttributes();
exifInterface = new ExifInterface(imageFile.getAbsolutePath());
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, false);
FileDescriptor fd = null;
try {
fd = Os.open(imageFile.getAbsolutePath(), OsConstants.O_RDWR, 0600);
exifInterface = new ExifInterface(fd);
exifInterface.saveAttributes();
Os.lseek(fd, 0, OsConstants.SEEK_SET);
exifInterface = new ExifInterface(fd);
compareWithExpectedValue(exifInterface, expectedValue, verboseTag, false);
// Test for modifying one attribute.
backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
exifInterface.saveAttributes();
// Check if thumbnail offset and length are properly updated without parsing the data
// again.
if (expectedValue.hasThumbnail) {
testThumbnail(expectedValue, exifInterface);
}
Os.lseek(fd, 0, OsConstants.SEEK_SET);
exifInterface = new ExifInterface(fd);
assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
// Check if thumbnail bytes can be retrieved from the new thumbnail range.
if (expectedValue.hasThumbnail) {
testThumbnail(expectedValue, exifInterface);
}
// 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, false);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
} finally {
IoUtils.closeQuietly(fd);
}
imageFile.delete();
}
private void readFromFilesWithExif(String fileName, int typedArrayResourceId)
throws IOException {
ExpectedValue expectedValue = new ExpectedValue(
getContext().getResources().obtainTypedArray(typedArrayResourceId));
testExifInterfaceCommon(fileName, expectedValue);
// Test for checking expected range by retrieving raw data with given offset and length.
testExifInterfaceRange(fileName, expectedValue);
}
private void writeToFilesWithoutExif(String fileName) throws IOException {
// Test for reading from external data storage.
Preconditions.assertTestFileExists(mInpPrefix + fileName);
File imageFile = clone(new File(mInpPrefix, fileName));
ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
exifInterface.saveAttributes();
exifInterface = new ExifInterface(imageFile.getAbsolutePath());
String make = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
assertEquals("abc", make);
imageFile.delete();
}
private void testThumbnail(ExpectedValue expectedValue, ExifInterface exifInterface) {
byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
assertNotNull(thumbnailBytes);
// Note: NEF file (nikon_1aw1.nef) contains uncompressed thumbnail.
Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap();
assertNotNull(thumbnailBitmap);
assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
assertEquals(expectedValue.isThumbnailCompressed, exifInterface.isThumbnailCompressed());
}
@Override
protected void setUp() throws Exception {
super.setUp();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectUnbufferedIo()
.penaltyDeath()
.build());
}
public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
readFromFilesWithExif(JPEG_WITH_EXIF_BYTE_ORDER_II, R.array.jpeg_with_exif_byte_order_ii);
writeToFilesWithExif(JPEG_WITH_EXIF_BYTE_ORDER_II, R.array.jpeg_with_exif_byte_order_ii);
}
public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
readFromFilesWithExif(JPEG_WITH_EXIF_BYTE_ORDER_MM, R.array.jpeg_with_exif_byte_order_mm);
writeToFilesWithExif(JPEG_WITH_EXIF_BYTE_ORDER_MM, R.array.jpeg_with_exif_byte_order_mm);
}
public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
readFromFilesWithExif(DNG_WITH_EXIF_WITH_XMP, R.array.dng_with_exif_with_xmp);
writeToFilesWithExif(DNG_WITH_EXIF_WITH_XMP, R.array.dng_with_exif_with_xmp);
}
public void testReadExifDataFromLgG4Iso800Jpg() throws Throwable {
readFromFilesWithExif(JPEG_WITH_EXIF_WITH_XMP, R.array.jpeg_with_exif_with_xmp);
writeToFilesWithExif(JPEG_WITH_EXIF_WITH_XMP, R.array.jpeg_with_exif_with_xmp);
}
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.
readFromFilesWithExif(JPEG_VOLANTIS, R.array.volantis_jpg);
writeToFilesWithExif(JPEG_VOLANTIS, R.array.volantis_jpg);
}
public void testReadExifDataFromSonyRX100Arw() throws Throwable {
readFromFilesWithExif(ARW_SONY_RX_100, R.array.sony_rx_100_arw);
}
public void testReadExifDataFromCanonG7XCr2() throws Throwable {
readFromFilesWithExif(CR2_CANON_G7X, R.array.canon_g7x_cr2);
}
public void testReadExifDataFromFujiX20Raf() throws Throwable {
readFromFilesWithExif(RAF_FUJI_X20, R.array.fuji_x20_raf);
}
public void testReadExifDataFromNikon1AW1Nef() throws Throwable {
readFromFilesWithExif(NEF_NIKON_1AW1, R.array.nikon_1aw1_nef);
}
public void testReadExifDataFromNikonP330Nrw() throws Throwable {
readFromFilesWithExif(NRW_NIKON_P330, R.array.nikon_p330_nrw);
}
public void testReadExifDataFromOlympusEPL3Orf() throws Throwable {
readFromFilesWithExif(ORF_OLYMPUS_E_PL3, R.array.olympus_e_pl3_orf);
}
public void testReadExifDataFromPanasonicGM5Rw2() throws Throwable {
readFromFilesWithExif(RW2_PANASONIC_GM5, R.array.panasonic_gm5_rw2);
}
public void testReadExifDataFromPentaxK5Pef() throws Throwable {
readFromFilesWithExif(PEF_PENTAX_K5, R.array.pentax_k5_pef);
}
public void testReadExifDataFromSamsungNX3000Srw() throws Throwable {
readFromFilesWithExif(SRW_SAMSUNG_NX3000, R.array.samsung_nx3000_srw);
}
public void testPngFiles() throws Throwable {
readFromFilesWithExif(PNG_WITH_EXIF_BYTE_ORDER_II, R.array.png_with_exif_byte_order_ii);
writeToFilesWithoutExif(PNG_WITHOUT_EXIF);
}
public void testStandaloneData() throws Throwable {
readFromStandaloneDataWithExif(JPEG_WITH_EXIF_BYTE_ORDER_II,
R.array.standalone_data_with_exif_byte_order_ii);
readFromStandaloneDataWithExif(JPEG_WITH_EXIF_BYTE_ORDER_MM,
R.array.standalone_data_with_exif_byte_order_mm);
}
public void testWebpFiles() throws Throwable {
readFromFilesWithExif(WEBP_WITH_EXIF, R.array.webp_with_exif);
writeToFilesWithExif(WEBP_WITH_EXIF, R.array.webp_with_exif);
writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_ANIM_DATA);
writeToFilesWithoutExif(WEBP_WITHOUT_EXIF);
writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING);
}
public void testGetSetDateTime() throws Throwable {
final long expectedDatetimeValue = 1454059947000L;
final String dateTimeValue = "2017:02:02 22:22:22";
final String dateTimeOriginalValue = "2017:01:01 11:11:11";
Preconditions.assertTestFileExists(mInpPrefix + JPEG_WITH_DATETIME_TAG);
File srcFile = new File(mInpPrefix, JPEG_WITH_DATETIME_TAG);
File imageFile = clone(srcFile);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(expectedDatetimeValue, exif.getDateTime());
assertEquals(expectedDatetimeValue, exif.getDateTimeOriginal());
assertEquals(expectedDatetimeValue, exif.getDateTimeDigitized());
assertEquals(expectedDatetimeValue, exif.getGpsDateTime());
exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
exif.saveAttributes();
// Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value.
exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
// Now remove the DATETIME value.
exif.setAttribute(ExifInterface.TAG_DATETIME, null);
exif.saveAttributes();
// When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
imageFile.delete();
}
public void testIsSupportedMimeType() {
try {
ExifInterface.isSupportedMimeType(null);
fail();
} catch (NullPointerException e) {
// expected
}
assertTrue(ExifInterface.isSupportedMimeType("image/jpeg"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-adobe-dng"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-canon-cr2"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-nikon-nef"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-nikon-nrw"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-sony-arw"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-panasonic-rw2"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-olympus-orf"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-pentax-pef"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-samsung-srw"));
assertTrue(ExifInterface.isSupportedMimeType("image/x-fuji-raf"));
assertTrue(ExifInterface.isSupportedMimeType("image/heic"));
assertTrue(ExifInterface.isSupportedMimeType("image/heif"));
assertTrue(ExifInterface.isSupportedMimeType("image/png"));
assertTrue(ExifInterface.isSupportedMimeType("image/webp"));
assertFalse(ExifInterface.isSupportedMimeType("image/gif"));
}
public void testSetAttribute() throws Throwable {
Preconditions.assertTestFileExists(mInpPrefix + JPEG_WITH_EXIF_BYTE_ORDER_MM);
File srcFile = new File(mInpPrefix, JPEG_WITH_EXIF_BYTE_ORDER_MM);
File imageFile = clone(srcFile);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
try {
exif.setAttribute(null, null);
fail();
} catch (NullPointerException e) {
// expected
}
// Test setting tag to null
assertNotNull(exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
assertNull(exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
// Test tags that are converted to rational values for compatibility:
// 1. GpsTimeStamp tag will be converted to rational in setAttribute and converted back to
// timestamp format in getAttribute.
String validGpsTimeStamp = "11:11:11";
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, validGpsTimeStamp);
assertEquals(validGpsTimeStamp, exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
// Check that invalid format is not set
String invalidGpsTimeStamp = "11:11:11:11";
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, invalidGpsTimeStamp);
assertEquals(validGpsTimeStamp, exif.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
// 2. FNumber tag will be converted to rational in setAttribute and converted back to
// double value in getAttribute
String validFNumber = "2.4";
exif.setAttribute(ExifInterface.TAG_F_NUMBER, validFNumber);
assertEquals(validFNumber, exif.getAttribute(ExifInterface.TAG_F_NUMBER));
// Check that invalid format is not set
String invalidFNumber = "invalid format";
exif.setAttribute(ExifInterface.TAG_F_NUMBER, invalidFNumber);
assertEquals(validFNumber, exif.getAttribute(ExifInterface.TAG_F_NUMBER));
// Test writing different types of formats:
// 1. Byte format tag
String gpsVersionId = "2.3.0.0";
exif.setAttribute(ExifInterface.TAG_GPS_VERSION_ID, gpsVersionId);
byte[] setGpsVersionIdBytes =
exif.getAttribute(ExifInterface.TAG_GPS_VERSION_ID).getBytes();
for (int i = 0; i < setGpsVersionIdBytes.length; i++) {
assertEquals(gpsVersionId.getBytes()[i], setGpsVersionIdBytes[i]);
}
// Test TAG_GPS_ALTITUDE_REF, which is an exceptional case since the only valid values are
// "0" and "1".
String gpsAltitudeRef = "1";
exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, gpsAltitudeRef);
assertEquals(gpsAltitudeRef.getBytes()[0],
exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF).getBytes()[0]);
// 2. String format tag
String makeValue = "MakeTest";
exif.setAttribute(ExifInterface.TAG_MAKE, makeValue);
assertEquals(makeValue, exif.getAttribute(ExifInterface.TAG_MAKE));
// Check that the following values are not parsed as rational values
String makeValueWithOneSlash = "Make/Test";
exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithOneSlash);
assertEquals(makeValueWithOneSlash, exif.getAttribute(ExifInterface.TAG_MAKE));
String makeValueWithTwoSlashes = "Make/Test/Test";
exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithTwoSlashes);
assertEquals(makeValueWithTwoSlashes, exif.getAttribute(ExifInterface.TAG_MAKE));
// When a value has a comma, it should be parsed as a string if any of the values before or
// after the comma is a string.
int defaultValue = -1;
String makeValueWithCommaType1 = "Make,2";
exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithCommaType1);
assertEquals(makeValueWithCommaType1, exif.getAttribute(ExifInterface.TAG_MAKE));
// Make sure that it's not stored as an integer value.
assertEquals(defaultValue, exif.getAttributeInt(ExifInterface.TAG_MAKE, defaultValue));
String makeValueWithCommaType2 = "2,Make";
exif.setAttribute(ExifInterface.TAG_MAKE, makeValueWithCommaType2);
assertEquals(makeValueWithCommaType2, exif.getAttribute(ExifInterface.TAG_MAKE));
// Make sure that it's not stored as an integer value.
assertEquals(defaultValue, exif.getAttributeInt(ExifInterface.TAG_MAKE, defaultValue));
// 3. Unsigned short format tag
String isoSpeedRatings = "800";
exif.setAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS, isoSpeedRatings);
assertEquals(isoSpeedRatings, exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS));
// When a value has multiple components, all of them should be of the format that the tag
// supports. Thus, the following values (SHORT,LONG) should not be set since TAG_COMPRESSION
// only allows short values.
assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
String invalidMultipleComponentsValueType1 = "1,65536";
exif.setAttribute(ExifInterface.TAG_COMPRESSION, invalidMultipleComponentsValueType1);
assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
String invalidMultipleComponentsValueType2 = "65536,1";
exif.setAttribute(ExifInterface.TAG_COMPRESSION, invalidMultipleComponentsValueType2);
assertNull(exif.getAttribute(ExifInterface.TAG_COMPRESSION));
// 4. Unsigned long format tag
String validImageWidthValue = "65536"; // max unsigned short value + 1
exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, validImageWidthValue);
assertEquals(validImageWidthValue, exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
String invalidImageWidthValue = "-65536";
exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, invalidImageWidthValue);
assertEquals(validImageWidthValue, exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
// 5. Unsigned rational format tag
String exposureTime = "1/8";
exif.setAttribute(ExifInterface.TAG_APERTURE_VALUE, exposureTime);
assertEquals(exposureTime, exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE));
// 6. Signed rational format tag
String brightnessValue = "-220/100";
exif.setAttribute(ExifInterface.TAG_BRIGHTNESS_VALUE, brightnessValue);
assertEquals(brightnessValue, exif.getAttribute(ExifInterface.TAG_BRIGHTNESS_VALUE));
// 7. Undefined format tag
String userComment = "UserCommentTest";
exif.setAttribute(ExifInterface.TAG_USER_COMMENT, userComment);
assertEquals(userComment, exif.getAttribute(ExifInterface.TAG_USER_COMMENT));
imageFile.delete();
}
public void testGetAttributeForNullAndNonExistentTag() throws Throwable {
// JPEG_WITH_EXIF_BYTE_ORDER_MM does not have a value for TAG_SUBJECT_AREA tag.
Preconditions.assertTestFileExists(mInpPrefix + JPEG_WITH_EXIF_BYTE_ORDER_MM);
File srcFile = new File(mInpPrefix, JPEG_WITH_EXIF_BYTE_ORDER_MM);
File imageFile = clone(srcFile);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
try {
exif.getAttribute(null);
fail();
} catch (NullPointerException e) {
// expected
}
assertNull(exif.getAttribute(TAG_SUBJECT_AREA));
int defaultValue = -1;
try {
exif.getAttributeInt(null, defaultValue);
fail();
} catch (NullPointerException e) {
// expected
}
assertEquals(defaultValue, exif.getAttributeInt(TAG_SUBJECT_AREA, defaultValue));
try {
exif.getAttributeDouble(null, defaultValue);
fail();
} catch (NullPointerException e) {
// expected
}
assertEquals(defaultValue, exif.getAttributeInt(TAG_SUBJECT_AREA, defaultValue));
try {
exif.getAttributeBytes(null);
fail();
} catch (NullPointerException e) {
// expected
}
assertNull(exif.getAttributeBytes(TAG_SUBJECT_AREA));
}
private static File clone(File original) throws IOException {
final File cloned =
File.createTempFile("cts_", +System.nanoTime() + "_" + original.getName());
FileUtils.copyFileOrThrow(original, cloned);
return cloned;
}
private short readShort(InputStream is) throws IOException {
int ch1 = is.read();
int ch2 = is.read();
if ((ch1 | ch2) < 0) {
throw new EOFException();
}
return (short) ((ch1 << 8) + (ch2));
}
}