Skip to content

Commit

Permalink
Add AVIF sniffing to ExifInterface
Browse files Browse the repository at this point in the history
The exif metadata extraction process for AVIF is similar to HEIC. Adding
AVIF sniffing so that avif files can be used with ExifInterface.

This is ported from ag/13003156 which was for platform ExifInterface,
with SDK check and test added.

Test: ExifInterfaceTest#testAvifFile
Change-Id: I3366902194b409e77b9e4b15d2b18be2b71375c8
  • Loading branch information
zhanghai authored and Tommy-Geenexus committed Aug 15, 2024
1 parent 0855f1a commit c234d47
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -501,22 +501,41 @@ public void testWebpWithoutExifWithLosslessEncodingAndAlpha() throws Throwable {
}

/**
* Support for retrieving EXIF from HEIF was added in SDK 28.
* Support for retrieving EXIF from HEIC was added in SDK 28.
*/
@Test
@LargeTest
public void testHeifFile() throws Throwable {
File imageFile = copyFromResourceToFile(
R.raw.heif_with_exif,
"heif_with_exif.heic"
);
public void testHeicFile() throws Throwable {
File imageFile = copyFromResourceToFile(R.raw.heic_with_exif, "heic_with_exif.heic");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Reading XMP data from HEIF was added in SDK 31.
// Reading XMP data from HEIC was added in SDK 31.
readFromFilesWithExif(
imageFile,
Build.VERSION.SDK_INT >= 31
? ExpectedAttributes.HEIF_WITH_EXIF_API_31_AND_ABOVE
: ExpectedAttributes.HEIF_WITH_EXIF_BELOW_API_31);
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
? ExpectedAttributes.HEIC_WITH_EXIF_API_31_AND_ABOVE
: ExpectedAttributes.HEIC_WITH_EXIF_BELOW_API_31);
} else {
// Make sure that an exception is not thrown and that image length/width tag values
// return default values, not the actual values.
ExifInterfaceExtended exif = new ExifInterfaceExtended(imageFile.getAbsolutePath());
String defaultTagValue = "0";
assertThat(exif.getAttribute(ExifInterfaceExtended.TAG_IMAGE_LENGTH))
.isEqualTo(defaultTagValue);
assertThat(exif.getAttribute(ExifInterfaceExtended.TAG_IMAGE_WIDTH))
.isEqualTo(defaultTagValue);
}
}

/**
* Support for retrieving EXIF from AVIF was added in SDK 31.
*/
@Test
@LargeTest
public void testAvifFile() throws Throwable {
File imageFile = copyFromResourceToFile(R.raw.avif_with_exif, "avif_with_exif.avif");
if (Build.VERSION.SDK_INT >= 31) {
// Reading EXIF and XMP data from AVIF was added in SDK 31.
readFromFilesWithExif(imageFile, ExpectedAttributes.AVIF_WITH_EXIF);
} else {
// Make sure that an exception is not thrown and that image length/width tag values
// return default values, not the actual values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ final class ExpectedAttributes {
.setHasIccProfile(true)
.build();

/** Expected attributes for {@link R.raw#heif_with_exif} when read on a device below API 31. */
public static final ExpectedAttributes HEIF_WITH_EXIF_BELOW_API_31 =
/** Expected attributes for {@link R.raw#heic_with_exif} when read on a device below API 31. */
public static final ExpectedAttributes HEIC_WITH_EXIF_BELOW_API_31 =
new Builder()
.setMake("LGE")
.setMakeOffset(3519)
Expand All @@ -189,13 +189,13 @@ final class ExpectedAttributes {
.build();

/**
* Expected attributes for {@link R.raw#heif_with_exif} when read on a device running API 31 or
* Expected attributes for {@link R.raw#heic_with_exif} when read on a device running API 31 or
* above.
*/
public static final ExpectedAttributes HEIF_WITH_EXIF_API_31_AND_ABOVE =
HEIF_WITH_EXIF_BELOW_API_31
public static final ExpectedAttributes HEIC_WITH_EXIF_API_31_AND_ABOVE =
HEIC_WITH_EXIF_BELOW_API_31
.buildUpon()
.setXmpResourceId(R.raw.heif_xmp)
.setXmpResourceId(R.raw.heic_xmp)
.setXmpOffsetAndLength(3721, 3020)
.build();

Expand Down Expand Up @@ -253,6 +253,16 @@ final class ExpectedAttributes {
.setHasPhotoshopImageResources(false)
.build();

/**
* Expected attributes for {@link R.raw#avif_with_exif}.
*/
public static final ExpectedAttributes AVIF_WITH_EXIF =
HEIC_WITH_EXIF_API_31_AND_ABOVE
.buildUpon()
.setMakeOffset(451)
.setXmpOffsetAndLength(653, 3020)
.build();

public static class Builder {
// Thumbnail information.
private boolean mHasThumbnail;
Expand Down
Binary file added app/src/androidTest/res/raw/avif_with_exif.avif
Binary file not shown.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
/**
* This is a class for reading and writing Exif tags in various image file formats.
*
* <p>Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW,
* RAF.
* <p>Supported for reading: JPEG, PNG, WebP, HEIC, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW,
* RAF, AVIF (on API 31+).
*
* <p>Supported for writing: JPEG, PNG, WebP.
*
Expand Down Expand Up @@ -3175,15 +3175,11 @@ public class ExifInterfaceExtended {
private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;

private static final byte[] HEIF_TYPE_FTYP = new byte[] {
'f', 't', 'y', 'p'
};
private static final byte[] HEIF_BRAND_MIF1 = new byte[] {
'm', 'i', 'f', '1'
};
private static final byte[] HEIF_BRAND_HEIC = new byte[] {
'h', 'e', 'i', 'c'
};
private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'};
private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'};
private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'};
private static final byte[] HEIF_BRAND_AVIF = new byte[] {'a', 'v', 'i', 'f'};
private static final byte[] HEIF_BRAND_AVIS = new byte[] {'a', 'v', 'i', 's'};

// See http://fileformats.archiveteam.org/wiki/Olympus_ORF
private static final short ORF_SIGNATURE_1 = 0x4f52;
Expand Down Expand Up @@ -3682,9 +3678,10 @@ public class ExifInterfaceExtended {
static final int IMAGE_TYPE_RW2 = 10;
@SuppressWarnings("unused")
static final int IMAGE_TYPE_SRW = 11;
static final int IMAGE_TYPE_HEIF = 12;
static final int IMAGE_TYPE_HEIC = 12;
static final int IMAGE_TYPE_PNG = 13;
static final int IMAGE_TYPE_WEBP = 14;
static final int IMAGE_TYPE_AVIF = 15;

static {
WEBP_VP8X_CHUNK_ORDER = new HashMap<>();
Expand Down Expand Up @@ -4428,8 +4425,8 @@ private void loadAttributes(@NonNull InputStream in) {
return;
}
} else {
if (mMimeType == IMAGE_TYPE_HEIF) {
getHeifAttributes(inputStream);
if (mMimeType == IMAGE_TYPE_HEIC || mMimeType == IMAGE_TYPE_AVIF) {
getHeifAttributes(inputStream, mMimeType);
} else if (mMimeType == IMAGE_TYPE_ORF) {
getOrfAttributes(inputStream);
} else if (mMimeType == IMAGE_TYPE_RW2) {
Expand Down Expand Up @@ -5285,17 +5282,24 @@ private int getMimeType(BufferedInputStream in) throws IOException {
in.reset();
if (isJpegFormat(signatureCheckBytes)) {
return IMAGE_TYPE_JPEG;
} else if (isRafFormat(signatureCheckBytes)) {
}
if (isRafFormat(signatureCheckBytes)) {
return IMAGE_TYPE_RAF;
} else if (isHeifFormat(signatureCheckBytes)) {
return IMAGE_TYPE_HEIF;
} else if (isOrfFormat(signatureCheckBytes)) {
}
int heicOrAvifImageType = isHeicOrAvifFormat(signatureCheckBytes);
if (heicOrAvifImageType != IMAGE_TYPE_UNKNOWN) {
return heicOrAvifImageType;
}
if (isOrfFormat(signatureCheckBytes)) {
return IMAGE_TYPE_ORF;
} else if (isRw2Format(signatureCheckBytes)) {
}
if (isRw2Format(signatureCheckBytes)) {
return IMAGE_TYPE_RW2;
} else if (isPngFormat(signatureCheckBytes)) {
}
if (isPngFormat(signatureCheckBytes)) {
return IMAGE_TYPE_PNG;
} else if (isWebpFormat(signatureCheckBytes)) {
}
if (isWebpFormat(signatureCheckBytes)) {
return IMAGE_TYPE_WEBP;
}
// Certain file formats (PEF) are identified in readImageFileDirectory()
Expand Down Expand Up @@ -5333,7 +5337,7 @@ private boolean isRafFormat(byte[] signatureCheckBytes) {
return true;
}

private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
private int isHeicOrAvifFormat(byte[] signatureCheckBytes) throws IOException {
ByteOrderedDataInputStream signatureInputStream = null;
//noinspection TryFinallyCanBeTryWithResources
try {
Expand All @@ -5344,7 +5348,7 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
signatureInputStream.readFully(chunkType);

if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
return false;
return IMAGE_TYPE_UNKNOWN;
}

long chunkDataOffset = 8;
Expand All @@ -5354,7 +5358,7 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
chunkSize = signatureInputStream.readLong();
if (chunkSize < 16) {
// The smallest valid chunk is 16 bytes long in this case.
return false;
return IMAGE_TYPE_UNKNOWN;
}
chunkDataOffset += 8;
}
Expand All @@ -5369,17 +5373,18 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
// It should at least have major brand (4-byte) and minor version (4-byte).
// The rest of the chunk (if any) is a list of (4-byte) compatible brands.
if (chunkDataSize < 8) {
return false;
return IMAGE_TYPE_UNKNOWN;
}

byte[] brand = new byte[4];
boolean isMif1 = false;
boolean isHeic = false;
boolean isAvif = false;
for (long i = 0; i < chunkDataSize / 4; ++i) {
try {
signatureInputStream.readFully(brand);
} catch (EOFException e) {
return false;
return IMAGE_TYPE_UNKNOWN;
}
if (i == 1) {
// Skip this index, it refers to the minorVersion, not a brand.
Expand All @@ -5389,9 +5394,16 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
isMif1 = true;
} else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
isHeic = true;
} else if (Arrays.equals(brand, HEIF_BRAND_AVIF)
|| Arrays.equals(brand, HEIF_BRAND_AVIS)) {
isAvif = true;
}
if (isMif1 && isHeic) {
return true;
if (isMif1) {
if (isHeic) {
return IMAGE_TYPE_HEIC;
} else if (isAvif) {
return IMAGE_TYPE_AVIF;
}
}
}
} catch (Exception e) {
Expand All @@ -5403,7 +5415,7 @@ private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
signatureInputStream.close();
}
}
return false;
return IMAGE_TYPE_UNKNOWN;
}

/**
Expand Down Expand Up @@ -5783,9 +5795,15 @@ private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException
}

// Support for getting MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET and
// MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH was added SDK 28.
private void getHeifAttributes(final SeekableByteOrderedDataInputStream in) throws IOException {
// MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH was added in SDK 28 for HEIC and in SDK 31
// for AVIF.
private void getHeifAttributes(final SeekableByteOrderedDataInputStream in, int imageType)
throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (imageType == IMAGE_TYPE_AVIF && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
throw new UnsupportedOperationException("Reading EXIF from AVIF files "
+ "is supported from SDK 31 and above");
}
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
ExifInterfaceExtendedUtils.Api23Impl.setDataSource(retriever,
Expand Down Expand Up @@ -5952,7 +5970,7 @@ public long getSize() {
retriever.release();
}
} else {
throw new UnsupportedOperationException("Reading EXIF from HEIF files "
throw new UnsupportedOperationException("Reading EXIF from HEIC files "
+ "is supported from SDK 28 and above");
}
}
Expand Down

0 comments on commit c234d47

Please sign in to comment.