Add support for gain maps that use a 'tmap' derived item. Change-Id: I98e7195b2859c3836d9aed2b2c41da349ba08015
diff --git a/avifinfo.c b/avifinfo.c index 91e690c..77b8b03 100644 --- a/avifinfo.c +++ b/avifinfo.c
@@ -147,6 +147,7 @@ typedef struct { uint8_t tile_item_id; uint8_t parent_item_id; + uint8_t dimg_idx; // Index of this association in the dimg box (0-based). } AvifInfoInternalTile; // Tile item id <-> parent item id associations. typedef struct { @@ -166,17 +167,14 @@ typedef struct { uint8_t has_primary_item; // True if "pitm" was parsed. - // Start location of the primary item id in the stream, in bytes. - uint64_t primary_item_id_location; - // Number of bytes used for the primary item id. - uint8_t primary_item_id_bytes; uint8_t has_alpha; // True if an alpha "auxC" was parsed. - uint8_t has_gainmap; // True if a gain map "auxC" was parsed. // Index of the gain map auxC property. uint8_t gainmap_property_index; uint8_t primary_item_id; AvifInfoFeatures primary_item_features; // Deduced from the data below. uint8_t data_was_skipped; // True if some loops/indices were skipped. + uint8_t tone_mapped_item_id; // Id of the "tmap" box, > 0 if present. + uint8_t iinf_parsed; // True if the "iinf" (item info) box was parsed. uint8_t num_tiles; AvifInfoInternalTile tiles[AVIFINFO_MAX_TILES]; @@ -191,20 +189,6 @@ // Generates the features of a given 'target_item_id' from internal features. static AvifInfoInternalStatus AvifInfoInternalGetItemFeatures( AvifInfoInternalFeatures* f, uint32_t target_item_id, uint32_t tile_depth) { - f->primary_item_features.has_gainmap = f->has_gainmap; - f->primary_item_features.primary_item_id_location = - f->primary_item_id_location; - f->primary_item_features.primary_item_id_bytes = f->primary_item_id_bytes; - - // Find the gain map's item id. - if (f->gainmap_property_index > 0) { - for (uint32_t prop_item = 0; prop_item < f->num_props; ++prop_item) { - if (f->props[prop_item].property_index == f->gainmap_property_index) { - f->primary_item_features.gainmap_item_id = f->props[prop_item].item_id; - } - } - } - for (uint32_t prop_item = 0; prop_item < f->num_props; ++prop_item) { if (f->props[prop_item].item_id != target_item_id) continue; const uint32_t property_index = f->props[prop_item].property_index; @@ -258,6 +242,35 @@ AVIFINFO_CHECK(f->has_primary_item, kNotFound); // Early exit. AVIFINFO_CHECK(f->num_dim_props > 0 && f->num_chan_props, kNotFound); + + // Look for a gain map. + // HEIF scheme: gain map is a hidden input of a derived item. + if (f->tone_mapped_item_id) { + for (uint32_t tile = 0; tile < f->num_tiles; ++tile) { + if (f->tiles[tile].parent_item_id == f->tone_mapped_item_id && + f->tiles[tile].dimg_idx == 1) { + f->primary_item_features.has_gainmap = 1; + f->primary_item_features.gainmap_item_id = f->tiles[tile].tile_item_id; + break; + } + } + } + // Adobe scheme: gain map is an auxiliary item. + if (!f->primary_item_features.has_gainmap && f->gainmap_property_index > 0) { + for (uint32_t prop_item = 0; prop_item < f->num_props; ++prop_item) { + if (f->props[prop_item].property_index == f->gainmap_property_index) { + f->primary_item_features.has_gainmap = 1; + f->primary_item_features.gainmap_item_id = f->props[prop_item].item_id; + break; + } + } + } + // If the gain map has not been found but we haven't read all the relevant + // metadata, we might still find one later and cannot stop now. + if (!f->primary_item_features.has_gainmap && !f->iinf_parsed) { + return kNotFound; + } + AVIFINFO_CHECK_FOUND( AvifInfoInternalGetItemFeatures(f, f->primary_item_id, /*tile_depth=*/0)); @@ -312,7 +325,8 @@ !memcmp(box->type, "meta", 4) || !memcmp(box->type, "pitm", 4) || !memcmp(box->type, "ipma", 4) || !memcmp(box->type, "ispe", 4) || !memcmp(box->type, "pixi", 4) || !memcmp(box->type, "iref", 4) || - !memcmp(box->type, "auxC", 4); + !memcmp(box->type, "auxC", 4) || !memcmp(box->type, "iinf", 4) || + !memcmp(box->type, "infe", 4); if (has_fullbox_header) box_header_size += 4; AVIFINFO_CHECK(box->size >= box_header_size, kInvalid); box->content_size = box->size - box_header_size; @@ -344,7 +358,10 @@ (!memcmp(box->type, "ispe", 4) && box->version <= 0) || (!memcmp(box->type, "pixi", 4) && box->version <= 0) || (!memcmp(box->type, "iref", 4) && box->version <= 1) || - (!memcmp(box->type, "auxC", 4) && box->version <= 0); + (!memcmp(box->type, "auxC", 4) && box->version <= 0) || + (!memcmp(box->type, "iinf", 4) && box->version <= 1) || + (!memcmp(box->type, "infe", 4) && box->version >= 2 && + box->version <= 3); // Instead of considering this file as invalid, skip unparsable boxes. if (!is_parsable) memcpy(box->type, "\0skp", 4); // \0 so not a valid type } @@ -464,7 +481,6 @@ if (strcmp(aux_type, kGainmapStr) == 0) { // Note: It is unlikely but it is possible that this gain map // does not belong to the primary item or a tile. Ignore this issue. - features->has_gainmap = 1; features->gainmap_property_index = box_index; } else if (box.content_size >= kAlphaStrLength && memcmp(aux_type, kAlphaStr, kGainmapStrLength) == 0) { @@ -623,6 +639,7 @@ features->num_tiles < AVIFINFO_MAX_TILES) { features->tiles[features->num_tiles].tile_item_id = to_item_id; features->tiles[features->num_tiles].parent_item_id = from_item_id; + features->tiles[features->num_tiles].dimg_idx = i; ++features->num_tiles; } else { features->data_was_skipped = 1; @@ -646,6 +663,69 @@ //------------------------------------------------------------------------------ +// Parses a 'stream' of an "iinf" box into 'features'. +static AvifInfoInternalStatus ParseIinf(int nesting_level, + AvifInfoInternalStream* stream, + uint32_t num_remaining_bytes, + uint32_t box_version, + uint32_t* num_parsed_boxes, + AvifInfoInternalFeatures* features) { + features->iinf_parsed = 1; + + const uint32_t num_bytes_per_entry_count = box_version == 0 ? 2 : 4; + AVIFINFO_CHECK(num_bytes_per_entry_count <= num_remaining_bytes, kInvalid); + const uint8_t* data; + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, num_bytes_per_entry_count, &data)); + num_remaining_bytes -= num_bytes_per_entry_count; + const uint32_t entry_count = + AvifInfoInternalReadBigEndian(data, num_bytes_per_entry_count); + + for (int i = 0; i < entry_count; ++i) { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox( + nesting_level, stream, num_remaining_bytes, num_parsed_boxes, &box)); + + if (!memcmp(box.type, "infe", 4)) { + // See ISO/IEC 14496-12:2015(E) 8.11.6.2 + uint32_t num_read_bytes = 0; + + const uint32_t num_bytes_per_id = (box.version == 2) ? 2 : 4; + const uint8_t* data; + // item_ID (16 or 32) + item_protection_index (16) + item_type (32). + AVIFINFO_CHECK((num_bytes_per_id + 6) <= num_remaining_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, num_bytes_per_id, &data)); + num_read_bytes += num_bytes_per_id; + const uint32_t item_id = + AvifInfoInternalReadBigEndian(data, num_bytes_per_id); + + // Skip item_protection_index + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, 2)); + num_read_bytes += 2; + + const uint8_t* item_type; + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &item_type)); + num_read_bytes += 4; + if (!memcmp(item_type, "tmap", 4)) { + // Tone Mapped Image: indicates the presence of a gain map. + features->tone_mapped_item_id = item_id; + } + + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - num_read_bytes)); + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + + num_remaining_bytes -= box.size; + if (num_remaining_bytes <= 0) break; + } + AVIFINFO_RETURN(kNotFound); +} + +//------------------------------------------------------------------------------ + // Parses a 'stream' of a "meta" box. It looks for the primary item ID in the // "pitm" box and recurses into other boxes to find its 'features'. static AvifInfoInternalStatus ParseMeta(int nesting_level, @@ -670,8 +750,9 @@ AVIFINFO_CHECK(primary_item_id <= AVIFINFO_MAX_VALUE, kAborted); features->has_primary_item = 1; features->primary_item_id = primary_item_id; - features->primary_item_id_location = primary_item_id_location; - features->primary_item_id_bytes = num_bytes_per_id; + features->primary_item_features.primary_item_id_location = + primary_item_id_location; + features->primary_item_features.primary_item_id_bytes = num_bytes_per_id; AVIFINFO_CHECK_FOUND( AvifInfoInternalSkip(stream, box.content_size - num_bytes_per_id)); } else if (!memcmp(box.type, "iprp", 4)) { @@ -682,6 +763,10 @@ AVIFINFO_CHECK_NOT_FOUND(ParseIref(nesting_level + 1, stream, box.content_size, num_parsed_boxes, features)); + } else if (!memcmp(box.type, "iinf", 4)) { + AVIFINFO_CHECK_NOT_FOUND(ParseIinf(nesting_level + 1, stream, + box.content_size, box.version, + num_parsed_boxes, features)); } else { AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); }
diff --git a/avifinfo.h b/avifinfo.h index fe8f9ab..0a558f0 100644 --- a/avifinfo.h +++ b/avifinfo.h
@@ -40,7 +40,10 @@ uint32_t num_channels; // Likely 1, 2, 3 or 4 channels: // (1 monochrome or 3 colors) + (0 or 1 alpha) uint8_t has_gainmap; // True if a gain map was found. - uint8_t gainmap_item_id; // Id of the gain map item. + // Id of the gain map item. Assumes there is at most one. If there are several + // gain map items (e.g. because the main image is tiled and each tile has an + // independent gain map), then this is one of the ids, arbitrarily chosen. + uint8_t gainmap_item_id; // Start location in bytes of the primary item id, relative to the beginning // of the given payload. The primary item id is a big endian number stored on // bytes primary_item_id_location to
diff --git a/tests/avifinfo_demo.c b/tests/avifinfo_demo.c index b016c94..6417edf 100644 --- a/tests/avifinfo_demo.c +++ b/tests/avifinfo_demo.c
@@ -35,7 +35,7 @@ static int IdentifyAndGetFeatures(const char* file_path) { FILE* file = fopen(file_path, "rb"); if (!file) return 0; - uint8_t data[512]; // Features are probably available within 512 bytes. + uint8_t data[1024]; // Features are probably available within 1024 bytes. const size_t data_size = fread(data, 1, sizeof(data), file); fclose(file); AvifInfoFeatures features; @@ -138,10 +138,20 @@ //------------------------------------------------------------------------------ int main(int argc, char** argv) { - if (argc >= 2 && Identify(argv[1]) && IdentifyAndGetFeatures(argv[1]) && - IdentifyStream(argv[1]) && IdentifyAndGetFeaturesStream(argv[1]) && - IdentifyAndGetFeaturesStreams(argv[1])) { - return 0; + if (argc < 2) { + fprintf(stderr, "Usage: avifinfo_demo [file]...\n"); + return 1; } - return 1; + int res = 0; + for (int i = 1; i < argc; ++i) { + if (Identify(argv[i]) && IdentifyAndGetFeatures(argv[i]) && + IdentifyStream(argv[i]) && IdentifyAndGetFeaturesStream(argv[i]) && + IdentifyAndGetFeaturesStreams(argv[i])) { + printf("%s is valid\n", argv[i]); + } else { + fprintf(stderr, "ERROR: %s is NOT a valid AVIF file\n", argv[1]); + res = 1; + } + } + return res; }
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc index 94f2b3c..e115299 100644 --- a/tests/avifinfo_test.cc +++ b/tests/avifinfo_test.cc
@@ -127,6 +127,40 @@ .primary_item_id_bytes = 2u}); } +TEST(AvifInfoGetTest, WithGainmapTmap) { + const Data input = LoadFile("avifinfo_test_12x34_gainmap_tmap.avif"); + ASSERT_FALSE(input.empty()); + + ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk); + ExpectEqual(f, {.width = 12u, + .height = 34u, + .bit_depth = 10u, + .num_channels = 4u, + .has_gainmap = 1u, + .gainmap_item_id = 4u, + .primary_item_id_location = 96u, + .primary_item_id_bytes = 2u}); + + Data gainmap = input; + ASSERT_TRUE(SetPrimaryItemIdToGainmapId(gainmap)); + ASSERT_EQ(AvifInfoIdentify(gainmap.data(), gainmap.size()), kAvifInfoOk); + AvifInfoFeatures gainmap_f; + ASSERT_EQ(AvifInfoGetFeatures(gainmap.data(), gainmap.size(), &gainmap_f), + kAvifInfoOk); + // Note that num_channels says 4 even though the alpha plane is associated to + // the main image and not the gain map, but libavifinfo does not check this. + ExpectEqual(gainmap_f, {.width = 6u, + .height = 17u, + .bit_depth = 8u, + .num_channels = 4u, + .has_gainmap = 1u, + .gainmap_item_id = 4u, + .primary_item_id_location = 96u, + .primary_item_id_bytes = 2u}); +} + TEST(AvifInfoGetTest, NoPixi10b) { // Same as above but "meta" box size is stored as 64 bits, "av1C" has // 'high_bitdepth' set to true, "pixi" was renamed to "pixy" and "mdat" size