Skip to content

Commit 895e2f7

Browse files
vigneshvgglide-copybara-robot
authored andcommittedFeb 14, 2023
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
1 parent 57248ae commit 895e2f7

File tree

5 files changed

+132
-22
lines changed

5 files changed

+132
-22
lines changed
 

‎integration/avif/src/main/java/com/bumptech/glide/integration/avif/AvifStreamBitmapDecoder.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public Resource<Bitmap> decode(InputStream source, int width, int height, Option
4141

4242
@Override
4343
public boolean handles(InputStream source, Options options) throws IOException {
44-
return ImageType.AVIF.equals(ImageHeaderParserUtils.getType(parsers, source, arrayPool));
44+
ImageType type = ImageHeaderParserUtils.getType(parsers, source, arrayPool);
45+
return type.equals(ImageType.AVIF) || type.equals(ImageType.ANIMATED_AVIF);
4546
}
4647
}

‎library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ enum ImageType {
3434
ANIMATED_WEBP(true),
3535
/** Avif type (may contain alpha). */
3636
AVIF(true),
37+
/** Animated Avif type (may contain alpha). */
38+
ANIMATED_AVIF(true),
3739
/** Unrecognized type. */
3840
UNKNOWN(false);
3941

‎library/src/main/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParser.java

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.bumptech.glide.load.resource.bitmap;
22

3+
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_AVIF;
34
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.ANIMATED_WEBP;
45
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.AVIF;
56
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF;
@@ -129,7 +130,7 @@ private ImageType getType(Reader reader) throws IOException {
129130
if (firstFourBytes != RIFF_HEADER) {
130131
// Check for AVIF (reads up to 32 bytes). If it is a valid AVIF stream, then the
131132
// firstFourBytes will be the size of the FTYP box.
132-
return sniffAvif(reader, /* boxSize= */ firstFourBytes) ? AVIF : UNKNOWN;
133+
return sniffAvif(reader, /* boxSize= */ firstFourBytes);
133134
}
134135

135136
// WebP (reads up to 21 bytes).
@@ -177,34 +178,40 @@ private ImageType getType(Reader reader) throws IOException {
177178
* Check if the bits look like an AVIF Image. AVIF Specification:
178179
* https://aomediacodec.github.io/av1-avif/
179180
*
180-
* @return true if the first few bytes looks like it could be an AVIF Image, false otherwise.
181+
* @return AVIF or ANIMATED_AVIF if the first few bytes look like it could be an AVIF Image or an
182+
* animated AVIF Image respectively, UNKNOWN otherwise.
181183
*/
182-
private boolean sniffAvif(Reader reader, int boxSize) throws IOException {
184+
private ImageType sniffAvif(Reader reader, int boxSize) throws IOException {
183185
int chunkType = (reader.getUInt16() << 16) | reader.getUInt16();
184186
if (chunkType != FTYP_HEADER) {
185-
return false;
187+
return UNKNOWN;
186188
}
187189
// majorBrand.
188190
int brand = (reader.getUInt16() << 16) | reader.getUInt16();
189-
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
190-
return true;
191+
// The overall logic is that, if any of the brands are 'avis', then we can conclude immediately
192+
// that it is an animated AVIF image. Otherwise, we conclude after seeing all the brands that if
193+
// one of them is 'avif', the it is a still AVIF image.
194+
if (brand == AVIS_BRAND) {
195+
return ANIMATED_AVIF;
191196
}
197+
boolean avifBrandSeen = brand == AVIF_BRAND;
192198
// Skip the minor version.
193199
reader.skip(4);
194200
// Check the first five minor brands. While there could theoretically be more than five minor
195201
// brands, it is rare in practice. This way we stop the loop from running several times on a
196202
// blob that just happened to look like an ftyp box.
197203
int sizeRemaining = boxSize - 16;
198-
if (sizeRemaining % 4 != 0) {
199-
return false;
200-
}
201-
for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) {
202-
brand = (reader.getUInt16() << 16) | reader.getUInt16();
203-
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
204-
return true;
204+
if (sizeRemaining % 4 == 0) {
205+
for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) {
206+
brand = (reader.getUInt16() << 16) | reader.getUInt16();
207+
if (brand == AVIS_BRAND) {
208+
return ANIMATED_AVIF;
209+
} else if (brand == AVIF_BRAND) {
210+
avifBrandSeen = true;
211+
}
205212
}
206213
}
207-
return false;
214+
return avifBrandSeen ? AVIF : UNKNOWN;
208215
}
209216

210217
/**

‎library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DefaultImageHeaderParserTest.java

+107-7
Original file line numberDiff line numberDiff line change
@@ -580,22 +580,22 @@ public void run(
580580
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
581581
}
582582
});
583-
// Change the brand from 'avif' to 'avis'.
583+
// Change the major brand from 'avif' to 'avis'. Now, the expected output is ANIMATED_AVIF.
584584
data[11] = 0x73;
585585
runTest(
586586
data,
587587
new ParserTestCase() {
588588
@Override
589589
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
590590
throws IOException {
591-
assertEquals(ImageType.AVIF, parser.getType(is));
591+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
592592
}
593593

594594
@Override
595595
public void run(
596596
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
597597
throws IOException {
598-
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
598+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
599599
}
600600
});
601601
}
@@ -654,22 +654,101 @@ public void run(
654654
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
655655
}
656656
});
657-
// Change the brand from 'avif' to 'avis'.
658-
data[13] = 0x73;
657+
// Change the last minor brand from 'MA1B' to 'avis'. Now, the expected output is ANIMATED_AVIF.
658+
data[24] = 0x61;
659+
data[25] = 0x76;
660+
data[26] = 0x69;
661+
data[27] = 0x73;
659662
runTest(
660663
data,
661664
new ParserTestCase() {
662665
@Override
663666
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
664667
throws IOException {
665-
assertEquals(ImageType.AVIF, parser.getType(is));
668+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
666669
}
667670

668671
@Override
669672
public void run(
670673
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
671674
throws IOException {
672-
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
675+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
676+
}
677+
});
678+
}
679+
680+
@Test
681+
public void testCanParseAvifAndAvisBrandsAsAnimatedAvif() throws IOException {
682+
byte[] data =
683+
new byte[] {
684+
// Box Size.
685+
0x00,
686+
0x00,
687+
0x00,
688+
0x1C,
689+
// ftyp.
690+
0x66,
691+
0x74,
692+
0x79,
693+
0x70,
694+
// avis (major brand).
695+
0x61,
696+
0x76,
697+
0x69,
698+
0x73,
699+
// minor version.
700+
0x00,
701+
0x00,
702+
0x00,
703+
0x00,
704+
// other minor brands (miaf, avif, MA1B).
705+
0x6d,
706+
0x69,
707+
0x61,
708+
0x66,
709+
0x61,
710+
0x76,
711+
0x69,
712+
0x66,
713+
0x4d,
714+
0x41,
715+
0x31,
716+
0x42
717+
};
718+
runTest(
719+
data,
720+
new ParserTestCase() {
721+
@Override
722+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
723+
throws IOException {
724+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
725+
}
726+
727+
@Override
728+
public void run(
729+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
730+
throws IOException {
731+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
732+
}
733+
});
734+
// Change the major brand from 'avis' to 'avif'.
735+
data[11] = 0x66;
736+
// Change the minor brand from 'avif' to 'avis'.
737+
data[23] = 0x73;
738+
runTest(
739+
data,
740+
new ParserTestCase() {
741+
@Override
742+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
743+
throws IOException {
744+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(is));
745+
}
746+
747+
@Override
748+
public void run(
749+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
750+
throws IOException {
751+
assertEquals(ImageType.ANIMATED_AVIF, parser.getType(byteBuffer));
673752
}
674753
});
675754
}
@@ -743,6 +822,27 @@ public void run(
743822
});
744823
}
745824

825+
@Test
826+
public void testCanParseRealAnimatedAvifFile() throws IOException {
827+
byte[] data = Util.readBytes(TestResourceUtil.openResource(getClass(), "animated_avif.avif"));
828+
runTest(
829+
data,
830+
new ParserTestCase() {
831+
@Override
832+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
833+
throws IOException {
834+
assertThat(parser.getType(is)).isEqualTo(ImageType.ANIMATED_AVIF);
835+
}
836+
837+
@Override
838+
public void run(
839+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
840+
throws IOException {
841+
assertThat(parser.getType(byteBuffer)).isEqualTo(ImageType.ANIMATED_AVIF);
842+
}
843+
});
844+
}
845+
746846
@Test
747847
public void testReturnsUnknownTypeForUnknownImageHeaders() throws IOException {
748848
byte[] data = new byte[] {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.