Skip to content

Commit

Permalink
Ignore gain maps in files that don't have the 'tmap' brand.
Browse files Browse the repository at this point in the history
When the 'tmap' brand is present, keep parsing until the 'tmap' item
is found.
  • Loading branch information
maryla-uc committed Aug 20, 2024
1 parent 14d8e3c commit b7dc6cf
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 50 deletions.
150 changes: 109 additions & 41 deletions src/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1022,6 +1026,7 @@ static void avifDecoderDataDestroy(avifDecoderData * data)
avifArrayDestroy(&data->tracks);
avifDecoderDataClearTiles(data);
avifArrayDestroy(&data->tiles);
avifArrayDestroy(&data->compatibleBrands);
avifFree(data);
}

Expand Down Expand Up @@ -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 = {};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -4203,13 +4220,29 @@ 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)
AVIF_CHECKERR(!miniSeen, AVIF_RESULT_BMFF_PARSE_FAILED);
#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);
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -5406,46 +5461,59 @@ avifResult avifDecoderReset(avifDecoder * decoder)
}

#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
avifDecoderItem * toneMappedImageItem;
const avifResult findGainMapResult = avifDecoderFindGainMapItem(decoder,
mainItems[AVIF_ITEM_COLOR],
&toneMappedImageItem,
&mainItems[AVIF_ITEM_GAIN_MAP],
&codecType[AVIF_ITEM_GAIN_MAP]);
if (!decoder->enableDecodingGainMap) {
// When ignoring the gain map, we still report whether one is present or not,
// but do not fail if there was any error with the gain map.
if (findGainMapResult != AVIF_RESULT_OK) {
// Only ignore reproducible errors (caused by the bitstream and not by the environment).
AVIF_CHECKERR(findGainMapResult != AVIF_RESULT_OUT_OF_MEMORY, findGainMapResult);
// Clear diagnostic message.
avifDiagnosticsClearError(data->diag);
}
decoder->gainMapPresent = (mainItems[AVIF_ITEM_GAIN_MAP] != NULL);
// We also ignore the actual item and don't decode it.
mainItems[AVIF_ITEM_GAIN_MAP] = NULL;
} else {
AVIF_CHECKRES(findGainMapResult);
}
if (toneMappedImageItem != NULL && decoder->enableParsingGainMapMetadata) {
// Read the gain map's metadata.
avifROData tmapData;
AVIF_CHECKRES(avifDecoderItemRead(toneMappedImageItem, decoder->io, &tmapData, 0, 0, data->diag));
if (decoder->image->gainMap == NULL) {
decoder->image->gainMap = avifGainMapCreate();
AVIF_CHECKERR(decoder->image->gainMap, AVIF_RESULT_OUT_OF_MEMORY);
}
const avifResult tmapParsingRes =
avifParseToneMappedImageBox(&decoder->image->gainMap->metadata, tmapData.data, tmapData.size, data->diag);
if (tmapParsingRes == AVIF_RESULT_NOT_IMPLEMENTED) {
// Forget about the gain map.
toneMappedImageItem = NULL;
// 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 contians 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 (avifBrandArrayHasBrand(&data->compatibleBrands, "tmap")) {
avifDecoderItem * toneMappedImageItem;
const avifResult findGainMapResult = avifDecoderFindGainMapItem(decoder,
mainItems[AVIF_ITEM_COLOR],
&toneMappedImageItem,
&mainItems[AVIF_ITEM_GAIN_MAP],
&codecType[AVIF_ITEM_GAIN_MAP]);
if (!decoder->enableDecodingGainMap) {
// When ignoring the gain map, we still report whether one is present or not,
// but do not fail if there was any error with the gain map.
if (findGainMapResult != AVIF_RESULT_OK) {
// Only ignore reproducible errors (caused by the bitstream and not by the environment).
AVIF_CHECKERR(findGainMapResult != AVIF_RESULT_OUT_OF_MEMORY, findGainMapResult);
// Clear diagnostic message.
avifDiagnosticsClearError(data->diag);
}
decoder->gainMapPresent = (mainItems[AVIF_ITEM_GAIN_MAP] != NULL);
// We also ignore the actual item and don't decode it.
mainItems[AVIF_ITEM_GAIN_MAP] = NULL;
avifGainMapDestroy(decoder->image->gainMap);
decoder->image->gainMap = NULL;
decoder->gainMapPresent = AVIF_FALSE;
} else {
AVIF_CHECKRES(tmapParsingRes);
AVIF_CHECKRES(findGainMapResult);
}
if (toneMappedImageItem != NULL && decoder->enableParsingGainMapMetadata) {
// Read the gain map's metadata.
avifROData tmapData;
AVIF_CHECKRES(avifDecoderItemRead(toneMappedImageItem, decoder->io, &tmapData, 0, 0, data->diag));
if (decoder->image->gainMap == NULL) {
decoder->image->gainMap = avifGainMapCreate();
AVIF_CHECKERR(decoder->image->gainMap, AVIF_RESULT_OUT_OF_MEMORY);
}
const avifResult tmapParsingRes =
avifParseToneMappedImageBox(&decoder->image->gainMap->metadata, tmapData.data, tmapData.size, data->diag);
if (tmapParsingRes == AVIF_RESULT_NOT_IMPLEMENTED) {
// Forget about the gain map.
toneMappedImageItem = NULL;
mainItems[AVIF_ITEM_GAIN_MAP] = NULL;
avifGainMapDestroy(decoder->image->gainMap);
decoder->image->gainMap = NULL;
decoder->gainMapPresent = AVIF_FALSE;
} else {
AVIF_CHECKRES(tmapParsingRes);
}
}
}
#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
Expand Down
2 changes: 1 addition & 1 deletion src/stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 18 additions & 8 deletions tests/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand All @@ -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.

Expand All @@ -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.

Expand Down
Binary file added tests/data/seine_sdr_gainmap_notmapbrand.avif
Binary file not shown.
17 changes: 17 additions & 0 deletions tests/gtest/avifgainmaptest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,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);
Expand Down

0 comments on commit b7dc6cf

Please sign in to comment.