From 895e2f7bb2b060cb88d460d0660a68ca0d9dec6c Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Date: Tue, 14 Feb 2023 14:58:06 -0800 Subject: [PATCH] Support parsing of animated AVIF in DefaultImageHeaderParser This helps us pass animated AVIF images through to the platform's ImageDecoder similar to animated WebP. PiperOrigin-RevId: 509647391 --- .../avif/AvifStreamBitmapDecoder.java | 3 +- .../glide/load/ImageHeaderParser.java | 2 + .../bitmap/DefaultImageHeaderParser.java | 35 +++--- .../bitmap/DefaultImageHeaderParserTest.java | 114 ++++++++++++++++-- .../src/test/resources/animated_avif.avif | Bin 0 -> 4063 bytes 5 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 library/test/src/test/resources/animated_avif.avif 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 0000000000000000000000000000000000000000..0ea6dd171829092c2f3857ace6db5656568912b0 GIT binary patch literal 4063 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