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