Skip to content

Commit 3fd8e77

Browse files
sjuddglide-copybara-robot
authored andcommittedDec 3, 2021
Add AVIF sniffing to DefaultImageHeaderParser
Android 12 supports AVIF. PiperOrigin-RevId: 413818906
1 parent 1c0a45f commit 3fd8e77

File tree

3 files changed

+267
-3
lines changed

3 files changed

+267
-3
lines changed
 

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

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ enum ImageType {
3030
WEBP_A(true),
3131
/** WebP type without alpha. */
3232
WEBP(false),
33+
/** Avif type (may contain alpha). */
34+
AVIF(true),
3335
/** Unrecognized type. */
3436
UNKNOWN(false);
3537

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

+47-3
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.AVIF;
34
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.GIF;
45
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.JPEG;
56
import static com.bumptech.glide.load.ImageHeaderParser.ImageType.PNG;
@@ -54,6 +55,13 @@ public final class DefaultImageHeaderParser implements ImageHeaderParser {
5455
private static final int VP8_HEADER_TYPE_LOSSLESS = 0x0000004C;
5556
private static final int WEBP_EXTENDED_ALPHA_FLAG = 1 << 4;
5657
private static final int WEBP_LOSSLESS_ALPHA_FLAG = 1 << 3;
58+
// Avif-related
59+
// "ftyp"
60+
private static final int FTYP_HEADER = 0x66747970;
61+
// "avif"
62+
private static final int AVIF_BRAND = 0x61766966;
63+
// "avis"
64+
private static final int AVIS_BRAND = 0x61766973;
5765

5866
@NonNull
5967
@Override
@@ -116,12 +124,14 @@ private ImageType getType(Reader reader) throws IOException {
116124
}
117125
}
118126

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

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

168+
/**
169+
* Check if the bits look like an AVIF Image. AVIF Specification:
170+
* https://aomediacodec.github.io/av1-avif/
171+
*
172+
* @return true if the first few bytes looks like it could be an AVIF Image, false otherwise.
173+
*/
174+
private boolean sniffAvif(Reader reader, int boxSize) throws IOException {
175+
int chunkType = (reader.getUInt16() << 16) | reader.getUInt16();
176+
if (chunkType != FTYP_HEADER) {
177+
return false;
178+
}
179+
// majorBrand.
180+
int brand = (reader.getUInt16() << 16) | reader.getUInt16();
181+
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
182+
return true;
183+
}
184+
// Skip the minor version.
185+
reader.skip(4);
186+
// Check the first five minor brands. While there could theoretically be more than five minor
187+
// brands, it is rare in practice. This way we stop the loop from running several times on a
188+
// blob that just happened to look like an ftyp box.
189+
int sizeRemaining = boxSize - 16;
190+
if (sizeRemaining % 4 != 0) {
191+
return false;
192+
}
193+
for (int i = 0; i < 5 && sizeRemaining > 0; ++i, sizeRemaining -= 4) {
194+
brand = (reader.getUInt16() << 16) | reader.getUInt16();
195+
if (brand == AVIF_BRAND || brand == AVIS_BRAND) {
196+
return true;
197+
}
198+
}
199+
return false;
200+
}
201+
158202
/**
159203
* Parse the orientation from the image header. If it doesn't handle this image type (or this is
160204
* not an image) it will return a default value rather than throwing an exception.

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

+218
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.google.common.truth.Truth.assertThat;
44
import static org.junit.Assert.assertEquals;
5+
import static org.junit.Assert.assertNotEquals;
56

67
import androidx.annotation.NonNull;
78
import com.bumptech.glide.load.ImageHeaderParser;
@@ -284,6 +285,223 @@ public void run(
284285
});
285286
}
286287

288+
@Test
289+
public void testCanParseAvifMajorBrand() throws IOException {
290+
byte[] data =
291+
new byte[] {
292+
// Box Size.
293+
0x00,
294+
0x00,
295+
0x00,
296+
0x1C,
297+
// ftyp.
298+
0x66,
299+
0x74,
300+
0x79,
301+
0x70,
302+
// avif (major brand).
303+
0x61,
304+
0x76,
305+
0x69,
306+
0x66,
307+
// minor version.
308+
0x00,
309+
0x00,
310+
0x00,
311+
0x00,
312+
// other minor brands (mif1, miaf, MA1B).
313+
0x6d,
314+
0x69,
315+
0x66,
316+
0x31,
317+
0x6d,
318+
0x69,
319+
0x61,
320+
0x66,
321+
0x4d,
322+
0x41,
323+
0x31,
324+
0x42
325+
};
326+
runTest(
327+
data,
328+
new ParserTestCase() {
329+
@Override
330+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
331+
throws IOException {
332+
assertEquals(ImageType.AVIF, parser.getType(is));
333+
}
334+
335+
@Override
336+
public void run(
337+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
338+
throws IOException {
339+
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
340+
}
341+
});
342+
// Change the brand from 'avif' to 'avis'.
343+
data[11] = 0x73;
344+
runTest(
345+
data,
346+
new ParserTestCase() {
347+
@Override
348+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
349+
throws IOException {
350+
assertEquals(ImageType.AVIF, parser.getType(is));
351+
}
352+
353+
@Override
354+
public void run(
355+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
356+
throws IOException {
357+
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
358+
}
359+
});
360+
}
361+
362+
@Test
363+
public void testCanParseAvifMinorBrand() throws IOException {
364+
byte[] data =
365+
new byte[] {
366+
// Box Size.
367+
0x00,
368+
0x00,
369+
0x00,
370+
0x1C,
371+
// ftyp.
372+
0x66,
373+
0x74,
374+
0x79,
375+
0x70,
376+
// mif1 (major brand).
377+
0x6d,
378+
0x69,
379+
0x66,
380+
0x31,
381+
// minor version.
382+
0x00,
383+
0x00,
384+
0x00,
385+
0x00,
386+
// other minor brands (miaf, avif, MA1B).
387+
0x6d,
388+
0x69,
389+
0x61,
390+
0x66,
391+
0x61,
392+
0x76,
393+
0x69,
394+
0x66,
395+
0x4d,
396+
0x41,
397+
0x31,
398+
0x42
399+
};
400+
runTest(
401+
data,
402+
new ParserTestCase() {
403+
@Override
404+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
405+
throws IOException {
406+
assertEquals(ImageType.AVIF, parser.getType(is));
407+
}
408+
409+
@Override
410+
public void run(
411+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
412+
throws IOException {
413+
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
414+
}
415+
});
416+
// Change the brand from 'avif' to 'avis'.
417+
data[13] = 0x73;
418+
runTest(
419+
data,
420+
new ParserTestCase() {
421+
@Override
422+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
423+
throws IOException {
424+
assertEquals(ImageType.AVIF, parser.getType(is));
425+
}
426+
427+
@Override
428+
public void run(
429+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
430+
throws IOException {
431+
assertEquals(ImageType.AVIF, parser.getType(byteBuffer));
432+
}
433+
});
434+
}
435+
436+
@Test
437+
public void testCannotParseAvifMoreThanFiveMinorBrands() throws IOException {
438+
byte[] data =
439+
new byte[] {
440+
// Box Size.
441+
0x00,
442+
0x00,
443+
0x00,
444+
0x28,
445+
// ftyp.
446+
0x66,
447+
0x74,
448+
0x79,
449+
0x70,
450+
// mif1 (major brand).
451+
0x6d,
452+
0x69,
453+
0x66,
454+
0x31,
455+
// minor version.
456+
0x00,
457+
0x00,
458+
0x00,
459+
0x00,
460+
// more than five minor brands with the sixth one being avif (mif1, miaf, MA1B, mif1,
461+
// miab, avif).
462+
0x6d,
463+
0x69,
464+
0x66,
465+
0x31,
466+
0x6d,
467+
0x69,
468+
0x61,
469+
0x66,
470+
0x4d,
471+
0x41,
472+
0x31,
473+
0x42,
474+
0x6d,
475+
0x69,
476+
0x66,
477+
0x31,
478+
0x6d,
479+
0x69,
480+
0x61,
481+
0x66,
482+
0x61,
483+
0x76,
484+
0x69,
485+
0x66,
486+
};
487+
runTest(
488+
data,
489+
new ParserTestCase() {
490+
@Override
491+
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
492+
throws IOException {
493+
assertNotEquals(ImageType.AVIF, parser.getType(is));
494+
}
495+
496+
@Override
497+
public void run(
498+
DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
499+
throws IOException {
500+
assertNotEquals(ImageType.AVIF, parser.getType(byteBuffer));
501+
}
502+
});
503+
}
504+
287505
@Test
288506
public void testReturnsUnknownTypeForUnknownImageHeaders() throws IOException {
289507
byte[] data = new byte[] {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};

0 commit comments

Comments
 (0)
Please sign in to comment.