diff --git a/integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java b/integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java index 99c79cb11c..2e1ec10a4d 100644 --- a/integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java +++ b/integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java @@ -41,6 +41,7 @@ public Resource 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); } } diff --git a/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java b/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java index da7beabd5c..006a07624b 100644 --- a/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java +++ b/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java @@ -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); diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java index 386865d607..458de79181 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java @@ -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; @@ -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). @@ -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; } /** diff --git a/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java b/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java index 8bd3e48d14..302c3f903c 100644 --- a/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java +++ b/library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java @@ -580,7 +580,7 @@ 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, @@ -588,14 +588,14 @@ public void run( @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)); } }); } @@ -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)); } }); } @@ -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}; diff --git a/library/test/src/test/resources/animated_avif.avif b/library/test/src/test/resources/animated_avif.avif new file mode 100644 index 0000000000..0ea6dd1718 Binary files /dev/null and b/library/test/src/test/resources/animated_avif.avif differ