Skip to content

Commit

Permalink
Support parsing of animated AVIF in DefaultImageHeaderParser
Browse files Browse the repository at this point in the history
This helps us pass animated AVIF images through to the platform's
ImageDecoder similar to animated WebP.

PiperOrigin-RevId: 509647391
  • Loading branch information
vigneshvg authored and glide-copybara-robot committed Feb 14, 2023
1 parent 57248ae commit 895e2f7
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 22 deletions.
Expand Up @@ -41,6 +41,7 @@ public Resource<Bitmap> decode(InputStream source, int width, int height, Option

@Override
public boolean handles(InputStream source, Options options) throws IOException {
return ImageType.AVIF.equals(ImageHeaderParserUtils.getType(parsers, source, arrayPool));
ImageType type = ImageHeaderParserUtils.getType(parsers, source, arrayPool);
return type.equals(ImageType.AVIF) || type.equals(ImageType.ANIMATED_AVIF);
}
}
Expand Up @@ -34,6 +34,8 @@ enum ImageType {
ANIMATED_WEBP(true),
/** Avif type (may contain alpha). */
AVIF(true),
/** Animated Avif type (may contain alpha). */
ANIMATED_AVIF(true),
/** Unrecognized type. */
UNKNOWN(false);

Expand Down
@@ -1,5 +1,6 @@
package com.bumptech.glide.load.resource.bitmap;

import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_AVIF;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_WEBP;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.AVIF;
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF;
Expand Down Expand Up @@ -129,7 +130,7 @@ private ImageType getType(Reader reader) throws IOException {
if (firstFourBytes != RIFF_HEADER) {
// 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;
return sniffAvif(reader, /* boxSize= */ firstFourBytes);
}

// WebP (reads up to 21 bytes).
Expand Down Expand Up @@ -177,34 +178,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.
* @return AVIF or ANIMATED_AVIF if the first few bytes look like it could be an AVIF Image or an
* animated AVIF Image respectively, UNKNOWN otherwise.
*/
private boolean sniffAvif(Reader reader, int boxSize) throws IOException {
private ImageType sniffAvif(Reader reader, int boxSize) throws IOException {
int chunkType = (reader.getUInt16() << 16) | reader.getUInt16();
if (chunkType != FTYP_HEADER) {
return false;
return UNKNOWN;
}
// majorBrand.
int brand = (reader.getUInt16() << 16) | reader.getUInt16();
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
return true;
// The overall logic is that, if any of the brands are 'avis', then we can conclude immediately
// that it is an animated AVIF image. Otherwise, we conclude after seeing all the brands that if
// one of them is 'avif', the it is a still AVIF image.
if (brand == AVIS_BRAND) {
return ANIMATED_AVIF;
}
boolean avifBrandSeen = brand == AVIF_BRAND;
// 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;
if (sizeRemaining % 4 == 0) {
for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) {
brand = (reader.getUInt16() << 16) | reader.getUInt16();
if (brand == AVIS_BRAND) {
return ANIMATED_AVIF;
} else if (brand == AVIF_BRAND) {
avifBrandSeen = true;
}
}
}
return false;
return avifBrandSeen ? AVIF : UNKNOWN;
}

/**
Expand Down
Expand Up @@ -580,22 +580,22 @@ public void run(
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
// Change the brand from 'avif' to 'avis'.
// Change the major brand from 'avif' to 'avis'. Now, the expected output is ANIMATED_AVIF.
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));
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
}

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
}
});
}
Expand Down Expand Up @@ -654,22 +654,101 @@ public void run(
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
}
});
// Change the brand from 'avif' to 'avis'.
data[13] = 0x73;
// Change the last minor brand from 'MA1B' to 'avis'. Now, the expected output is ANIMATED_AVIF.
data[24] = 0x61;
data[25] = 0x76;
data[26] = 0x69;
data[27] = 0x73;
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.AVIF, parser.getType(is));
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
}

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

@Test
public void testCanParseAvifAndAvisBrandsAsAnimatedAvif() throws IOException {
byte[] data =
new byte[] {
// Box Size.
0x00,
0x00,
0x00,
0x1C,
// ftyp.
0x66,
0x74,
0x79,
0x70,
// avis (major brand).
0x61,
0x76,
0x69,
0x73,
// 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.ANIMATED_AVIF, parser.getType(is));
}

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

@Override
public void run(
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
}
});
}
Expand Down Expand Up @@ -743,6 +822,27 @@ public void run(
});
}

@Test
public void testCanParseRealAnimatedAvifFile() throws IOException {
byte[] data = Util.readBytes(TestResourceUtil.openResource(getClass(), "animated_avif.avif"));
runTest(
data,
new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertThat(parser.getType(is)).isEqualTo(ImageType.ANIMATED_AVIF);
}

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

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

0 comments on commit 895e2f7

Please sign in to comment.