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