diff --git a/src/read.c b/src/read.c index cf4752b0ce..297c7aa789 100644 --- a/src/read.c +++ b/src/read.c @@ -71,6 +71,9 @@ static const char * avifGetConfigurationPropertyName(avifCodecType codecType) // --------------------------------------------------------------------------- // Box data structures +typedef uint8_t avifBrand[4]; +AVIF_ARRAY_DECLARE(avifBrandArray, avifBrand, brand); + // ftyp typedef struct avifFileType { @@ -862,6 +865,7 @@ typedef struct avifDecoderData avifCodec * codec; avifCodec * codecAlpha; uint8_t majorBrand[4]; // From the file's ftyp, used by AVIF_DECODER_SOURCE_AUTO + avifBrandArray compatibleBrands; // From the file's ftyp avifDiagnostics * diag; // Shallow copy; owned by avifDecoder const avifSampleTable * sourceSampleTable; // NULL unless (source == AVIF_DECODER_SOURCE_TRACKS), owned by an avifTrack avifBool cicpSet; // True if avifDecoder's image has had its CICP set correctly yet. @@ -1022,6 +1026,7 @@ static void avifDecoderDataDestroy(avifDecoderData * data) avifArrayDestroy(&data->tracks); avifDecoderDataClearTiles(data); avifArrayDestroy(&data->tiles); + avifArrayDestroy(&data->compatibleBrands); avifFree(data); } @@ -4086,12 +4091,17 @@ static avifResult avifParse(avifDecoder * decoder) avifDecoderData * data = decoder->data; avifBool ftypSeen = AVIF_FALSE; avifBool metaSeen = AVIF_FALSE; + avifBool metaIsSizeZero = AVIF_FALSE; avifBool moovSeen = AVIF_FALSE; avifBool needsMeta = AVIF_FALSE; avifBool needsMoov = AVIF_FALSE; #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) avifBool miniSeen = AVIF_FALSE; avifBool needsMini = AVIF_FALSE; +#endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + avifBool needsTmap = AVIF_FALSE; + avifBool tmapSeen = AVIF_FALSE; #endif avifFileType ftyp = {}; @@ -4130,6 +4140,7 @@ static avifResult avifParse(avifDecoder * decoder) } else if (!memcmp(header.type, "meta", 4)) { isMeta = AVIF_TRUE; isNonSkippableVariableLengthBox = AVIF_TRUE; + metaIsSizeZero = header.isSizeZeroBox; } else if (!memcmp(header.type, "moov", 4)) { isMoov = AVIF_TRUE; isNonSkippableVariableLengthBox = AVIF_TRUE; @@ -4186,6 +4197,12 @@ static avifResult avifParse(avifDecoder * decoder) AVIF_CHECKERR(avifFileTypeIsCompatible(&ftyp), AVIF_RESULT_INVALID_FTYP); ftypSeen = AVIF_TRUE; memcpy(data->majorBrand, ftyp.majorBrand, 4); // Remember the major brand for future AVIF_DECODER_SOURCE_AUTO decisions + if (ftyp.compatibleBrandsCount > 0) { + AVIF_CHECKERR(avifArrayCreate(&data->compatibleBrands, sizeof(avifBrand), ftyp.compatibleBrandsCount), + AVIF_RESULT_OUT_OF_MEMORY); + memcpy(data->compatibleBrands.brand, ftyp.compatibleBrands, sizeof(avifBrand) * ftyp.compatibleBrandsCount); + data->compatibleBrands.count = ftyp.compatibleBrandsCount; + } needsMeta = avifFileTypeHasBrand(&ftyp, "avif"); needsMoov = avifFileTypeHasBrand(&ftyp, "avis"); #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) @@ -4203,6 +4220,12 @@ static avifResult avifParse(avifDecoder * decoder) AVIF_RESULT_BMFF_PARSE_FAILED); } #endif // AVIF_ENABLE_EXPERIMENTAL_MINI +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + needsTmap = avifFileTypeHasBrand(&ftyp, "tmap"); + if (needsTmap) { + needsMeta = AVIF_TRUE; + } +#endif } else if (isMeta) { AVIF_CHECKERR(!metaSeen, AVIF_RESULT_BMFF_PARSE_FAILED); #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) @@ -4210,6 +4233,16 @@ static avifResult avifParse(avifDecoder * decoder) #endif AVIF_CHECKRES(avifParseMetaBox(data->meta, boxOffset, boxContents.data, boxContents.size, data->diag)); metaSeen = AVIF_TRUE; + +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) { + if (!memcmp(data->meta->items.item[itemIndex]->type, "tmap", 4)) { + tmapSeen = AVIF_TRUE; + break; + } + } +#endif + #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) } else if (isMini) { AVIF_CHECKERR(!metaSeen, AVIF_RESULT_BMFF_PARSE_FAILED); @@ -4239,11 +4272,14 @@ static avifResult avifParse(avifDecoder * decoder) // * If the brand 'avif' is present, require a meta box // * If the brand 'avis' is present, require a moov box // * If AVIF_ENABLE_EXPERIMENTAL_MINI is defined and the brand 'mif3' is present, require a mini box + avifBool sawEverythingNeeded = ftypSeen && (!needsMeta || metaSeen) && (!needsMoov || moovSeen); #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) - if (ftypSeen && (!needsMeta || metaSeen) && (!needsMoov || moovSeen) && (!needsMini || miniSeen)) { -#else - if (ftypSeen && (!needsMeta || metaSeen) && (!needsMoov || moovSeen)) { + sawEverythingNeeded = sawEverythingNeeded && (!needsMini || miniSeen); #endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + sawEverythingNeeded = sawEverythingNeeded && (!needsTmap || tmapSeen); +#endif + if (sawEverythingNeeded) { return AVIF_RESULT_OK; } } @@ -4253,6 +4289,13 @@ static avifResult avifParse(avifDecoder * decoder) if ((needsMeta && !metaSeen) || (needsMoov && !moovSeen)) { return AVIF_RESULT_TRUNCATED_DATA; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + if (needsTmap && !tmapSeen) { + return metaIsSizeZero ? AVIF_RESULT_TRUNCATED_DATA : AVIF_RESULT_BMFF_PARSE_FAILED; + } +#else + (void)metaIsSizeZero; +#endif #if defined(AVIF_ENABLE_EXPERIMENTAL_MINI) if (needsMini && !miniSeen) { return AVIF_RESULT_TRUNCATED_DATA; @@ -4311,6 +4354,18 @@ avifBool avifPeekCompatibleFileType(const avifROData * input) return avifFileTypeIsCompatible(&ftyp); } +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) +static avifBool avifBrandArrayHasBrand(avifBrandArray * brands, const char * brand) +{ + for (uint32_t brandIndex = 0; brandIndex < brands->count; ++brandIndex) { + if (!memcmp(brands->brand[brandIndex], brand, 4)) { + return AVIF_TRUE; + } + } + return AVIF_FALSE; +} +#endif + // --------------------------------------------------------------------------- avifDecoder * avifDecoderCreate(void) @@ -5412,7 +5467,17 @@ avifResult avifDecoderReset(avifDecoder * decoder) } #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) - if (decoder->enableParsingGainMapMetadata) { + // Section 10.2.6 of 23008-12:2024/AMD 1:2024(E): + // 'tmap' brand + // This brand enables file players to identify and decode HEIF files containing tone-map derived image + // items. When present, this brand shall be among the brands included in the compatible_brands + // array of the FileTypeBox. + // + // If the file contains a 'tmap' item but doesn't have the 'tmap' brand, it is technically invalid. + // However, we don't report any error because in order to do detect this case consistently, we would + // need to remove the early exit in avifParse() to check if a 'tmap' item might be present + // further down the file. Instead, we simply ignore tmap items in files that lack the 'tmap' brand. + if (decoder->enableParsingGainMapMetadata && avifBrandArrayHasBrand(&data->compatibleBrands, "tmap")) { avifDecoderItem * toneMappedImageItem; avifDecoderItem * gainMapItem; avifCodecType gainMapCodecType; diff --git a/src/stream.c b/src/stream.c index ac89fed3dd..565baaf480 100644 --- a/src/stream.c +++ b/src/stream.c @@ -245,7 +245,7 @@ avifBool avifROStreamReadBoxHeaderPartial(avifROStream * stream, avifBoxHeader * // Section 4.2.2 of ISO/IEC 14496-12. // if size is 0, then this box shall be in a top-level box (i.e. not contained in another // box), and be the last box in its 'file', and its payload extends to the end of that - // enclosing 'file'. This is normally only used for a MediaDataBox. + // enclosing 'file'. This is normally only used for a MediaDataBox ('mdat'). if (!topLevel) { avifDiagnosticsPrintf(stream->diag, "%s: Non-top-level box with size 0", stream->diagContext); return AVIF_FALSE; diff --git a/tests/data/README.md b/tests/data/README.md index d1a693946c..824e2e6c3b 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -501,7 +501,7 @@ exiftool "-icc_profile<=p3.icc" paris_exif_xmp_icc_gainmap_bigendian.jpg License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) Source: generated with a modified libavif at https://github.com/maryla-uc/libavif/tree/weirdgainmaps -by running `./tests/avifgainmaptest --gtest_filter=GainMapTest.CreateGainMapImages ../tests/data/`  +by running `./tests/avifgainmaptest --gtest_filter=GainMapTest.CreateGainMapImages ../tests/data/` Contains a 4x3 color grid, a 4x3 alpha grid, and a 2x2 gain map grid. @@ -592,7 +592,7 @@ HDR image using the PQ transfer curve. Contains a gain map in [Adobe's format](https://helpx.adobe.com/camera-raw/using/gain-map.html) that is not recognized by libavif and ignored by the tests. -### File [seine_sdr_gainmap_srgb.jpg](seine_sdr_gainmap_srgb.avif) +### File [seine_sdr_gainmap_srgb.jpg](seine_sdr_gainmap_srgb.jpg) ![](seine_sdr_gainmap_srgb.jpg) @@ -626,12 +626,22 @@ with AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP and AVIF_ENABLE_LIBXML2 then: SDR image with a gain map to allow tone mapping to HDR. +### File [seine_sdr_gainmap_notmapbrand.avif](seine_sdr_gainmap_notmapbrand.avif) + +![](seine_sdr_gainmap_notmapbrand.avif) + +License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) + +Source : same as seine_sdr_gainmap_srgb.avif before commit 10b7232 + +An image with a `tmap` item (i.e. a gain map) but no 'tmap' brand in the `ftyp` box. + ### File [seine_sdr_gainmap_big_srgb.avif](seine_sdr_gainmap_big_srgb.avif) ![](seine_sdr_gainmap_big_srgb.avif) -Source : modified version of `seine_sdr_gainmap_srgb.avif` with an upscaled gain map, generated using libavif's API. -See `CreateTestImages` in `avifgainmaptest.cc` (set kUpdateTestImages to update images). +Source : generated by running `./tests/avifgainmaptest --gtest_filter=GainMapTest.CreateGainMapImages ../tests/data/` +after changing `kUpdateTestImages` to true in the `avifgainmaptest.cc`. SDR image with a gain map to allow tone mapping to HDR. The gain map's width and height are doubled compared to the base image. This is an atypical image just for testing. Typically, the gain map would be either the same size or smaller as the base image. @@ -642,8 +652,8 @@ This is an atypical image just for testing. Typically, the gain map would be eit License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) -Source : created from `seine_hdr_srgb.avif` (for the base image) and `seine_sdr_gainmap_srgb.avif` (for the gain map) with libavif's API. -See `CreateTestImages` in `avifgainmaptest.cc` (set kUpdateTestImages to update images). +Source : generated by running `./tests/avifgainmaptest --gtest_filter=GainMapTest.CreateGainMapImages ../tests/data/` +after changing `kUpdateTestImages` to true in the `avifgainmaptest.cc`. HDR image with a gain map to allow tone mapping to SDR. @@ -653,8 +663,8 @@ HDR image with a gain map to allow tone mapping to SDR. License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) -Source : modified version of `seine_hdr_gainmap_srgb.avif` with a downscaled gain map, generated using libavif's API. -See `CreateTestImages` in `avifgainmaptest.cc` (set kUpdateTestImages to update images). +Source : generated by running `./tests/avifgainmaptest --gtest_filter=GainMapTest.CreateGainMapImages ../tests/data/` +after changing `kUpdateTestImages` to true in the `avifgainmaptest.cc`. SDR image with a gain map to allow tone mapping to HDR. The gain map's width and height are halved compared to the base image. diff --git a/tests/data/seine_sdr_gainmap_notmapbrand.avif b/tests/data/seine_sdr_gainmap_notmapbrand.avif new file mode 100644 index 0000000000..b716742bd2 Binary files /dev/null and b/tests/data/seine_sdr_gainmap_notmapbrand.avif differ diff --git a/tests/gtest/avifgainmaptest.cc b/tests/gtest/avifgainmaptest.cc index d79537b81c..98c64369dc 100644 --- a/tests/gtest/avifgainmaptest.cc +++ b/tests/gtest/avifgainmaptest.cc @@ -937,6 +937,23 @@ TEST(GainMapTest, ExtraBytesAfterGainMapMetadataSupporterWriterVersion) { AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE); } +TEST(GainMapTest, DecodeInvalidFtyp) { + const std::string path = + std::string(data_path) + "seine_sdr_gainmap_notmapbrand.avif"; + ImagePtr decoded(avifImageCreateEmpty()); + ASSERT_NE(decoded, nullptr); + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + decoder->enableDecodingGainMap = true; + decoder->enableParsingGainMapMetadata = true; + + ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()), + AVIF_RESULT_OK); + // The gain map is ignored because the 'tmap' brand is not present. + EXPECT_EQ(decoder->gainMapPresent, false); + ASSERT_EQ(decoded->gainMap, nullptr); +} + #define EXPECT_FRACTION_NEAR(numerator, denominator, expected) \ EXPECT_NEAR(std::abs((double)numerator / denominator), expected, \ expected * 0.001);