Skip to content

Commit

Permalink
Add AVIF sniffing to DefaultImageHeaderParser
Browse files Browse the repository at this point in the history
Android 12 supports AVIF.

PiperOrigin-RevId: 413818906
  • Loading branch information
sjudd authored and glide-copybara-robot committed Dec 3, 2021
1 parent 1c0a45f commit 3fd8e77
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum ImageType {
WEBP_A(true),
/** WebP type without alpha. */
WEBP(false),
/** Avif type (may contain alpha). */
AVIF(true),
/** Unrecognized type. */
UNKNOWN(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bumptech.glide.load.resource.bitmap;

import static com.bumptech.glide.load.ImageHeaderParser.ImageType.AVIF;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.JPEG;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG;
Expand Down Expand Up @@ -54,6 +55,13 @@ public final class DefaultImageHeaderParser implements ImageHeaderParser {
private static final int VP8_HEADER_TYPE_LOSSLESS = 0x0000004C;
private static final int WEBP_EXTENDED_ALPHA_FLAG = 1 << 4;
private static final int WEBP_LOSSLESS_ALPHA_FLAG = 1 << 3;
// Avif-related
// "ftyp"
private static final int FTYP_HEADER = 0x66747970;
// "avif"
private static final int AVIF_BRAND = 0x61766966;
// "avis"
private static final int AVIS_BRAND = 0x61766973;

@NonNull
@Override
Expand Down Expand Up @@ -116,12 +124,14 @@ private ImageType getType(Reader reader) throws IOException {
}
}

// WebP (reads up to 21 bytes).
// See https://developers.google.com/speed/webp/docs/riff_container for details.
if (firstFourBytes != RIFF_HEADER) {
return UNKNOWN;
// Check for AVIF (reads up to 32 bytes). If it is a valid AVIF stream, then the
// firstFourBytes will be the size of the FTYP box.
return sniffAvif(reader, /* boxSize= */ firstFourBytes) ? AVIF : UNKNOWN;
}

// WebP (reads up to 21 bytes).
// See https://developers.google.com/speed/webp/docs/riff_container for details.
// Bytes 4 - 7 contain length information. Skip these.
reader.skip(4);
final int thirdFourBytes = (reader.getUInt16() << 16) | reader.getUInt16();
Expand Down Expand Up @@ -155,6 +165,40 @@ private ImageType getType(Reader reader) throws IOException {
}
}

/**
* Check if the bits look like an AVIF Image. AVIF Specification:
* https://aomediacodec.github.io/av1-avif/
*
* @return true if the first few bytes looks like it could be an AVIF Image, false otherwise.
*/
private boolean sniffAvif(Reader reader, int boxSize) throws IOException {
int chunkType = (reader.getUInt16() << 16) | reader.getUInt16();
if (chunkType != FTYP_HEADER) {
return false;
}
// majorBrand.
int brand = (reader.getUInt16() << 16) | reader.getUInt16();
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
return true;
}
// Skip the minor version.
reader.skip(4);
// Check the first five minor brands. While there could theoretically be more than five minor
// brands, it is rare in practice. This way we stop the loop from running several times on a
// blob that just happened to look like an ftyp box.
int sizeRemaining = boxSize - 16;
if (sizeRemaining % 4 != 0) {
return false;
}
for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) {
brand = (reader.getUInt16() << 16) | reader.getUInt16();
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
return true;
}
}
return false;
}

/**
* Parse the orientation from the image header. If it doesn't handle this image type (or this is
* not an image) it will return a default value rather than throwing an exception.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import androidx.annotation.NonNull;
import com.bumptech.glide.load.ImageHeaderParser;
Expand Down Expand Up @@ -284,6 +285,223 @@ public void run(
});
}

@Test
public void testCanParseAvifMajorBrand() throws IOException {
byte[] data =
new byte[] {
// Box Size.
0x00,
0x00,
0x00,
0x1C,
// ftyp.
0x66,
0x74,
0x79,
0x70,
// avif (major brand).
0x61,
0x76,
0x69,
0x66,
// minor version.
0x00,
0x00,
0x00,
0x00,
// other minor brands (mif1, miaf, MA1B).
0x6d,
0x69,
0x66,
0x31,
0x6d,
0x69,
0x61,
0x66,
0x4d,
0x41,
0x31,
0x42
};
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
// Change the brand from 'avif' to 'avis'.
data[11] = 0x73;
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
}

@Test
public void testCanParseAvifMinorBrand() throws IOException {
byte[] data =
new byte[] {
// Box Size.
0x00,
0x00,
0x00,
0x1C,
// ftyp.
0x66,
0x74,
0x79,
0x70,
// mif1 (major brand).
0x6d,
0x69,
0x66,
0x31,
// minor version.
0x00,
0x00,
0x00,
0x00,
// other minor brands (miaf, avif, MA1B).
0x6d,
0x69,
0x61,
0x66,
0x61,
0x76,
0x69,
0x66,
0x4d,
0x41,
0x31,
0x42
};
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
// Change the brand from 'avif' to 'avis'.
data[13] = 0x73;
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
}

@Test
public void testCannotParseAvifMoreThanFiveMinorBrands() throws IOException {
byte[] data =
new byte[] {
// Box Size.
0x00,
0x00,
0x00,
0x28,
// ftyp.
0x66,
0x74,
0x79,
0x70,
// mif1 (major brand).
0x6d,
0x69,
0x66,
0x31,
// minor version.
0x00,
0x00,
0x00,
0x00,
// more than five minor brands with the sixth one being avif (mif1, miaf, MA1B, mif1,
// miab, avif).
0x6d,
0x69,
0x66,
0x31,
0x6d,
0x69,
0x61,
0x66,
0x4d,
0x41,
0x31,
0x42,
0x6d,
0x69,
0x66,
0x31,
0x6d,
0x69,
0x61,
0x66,
0x61,
0x76,
0x69,
0x66,
};
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertNotEquals(ImageType.AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertNotEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
}

@Test
public void testReturnsUnknownTypeForUnknownImageHeaders() throws IOException {
byte[] data = new byte[] {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
Expand Down

0 comments on commit 3fd8e77

Please sign in to comment.