Extract info related to gain maps and the primary item id.

Change-Id: I53ca31a136e4ed1f78f2b70de56ca5117d3df3cb
diff --git a/README.md b/README.md
index dfb67c6..fe27e5d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # AVIF-info
 
 **libavifinfo** is a standalone library that can be used to extract the width,
-height, bit depth and number of channels from an AVIF payload.
+height, bit depth, number of channels and other metadata from an AVIF payload.
 
 See `avifinfo.h` for details on the API and `avifinfo.c` for the implementation.
 See `avifinfo_test.cc` for usage examples.
@@ -78,6 +78,8 @@
 
 ## PHP implementation
 
+The PHP implementation of libavifinfo is a subset of the C API.
+
 `libavifinfo` was [implemented](https://github.com/php/php-src/pull/7711) into
 **php-src** natively and is available through `getimagesize()` at head. If it is
 not available in the PHP release version you use, you can fallback to
diff --git a/avifinfo.c b/avifinfo.c
index 90e02bf..977ff82 100644
--- a/avifinfo.c
+++ b/avifinfo.c
@@ -94,6 +94,12 @@
 #define AVIFINFO_CHECK_NOT_FOUND(check_status) \
   AVIFINFO_CHECK_STATUS_IS((check_status), kNotFound)
 
+#if defined(AVIFINFO_ENABLE_DEBUG_LOG)
+#define AVIF_DEBUG_LOG(...) printf(__VA_ARGS__)
+#else
+#define AVIF_DEBUG_LOG(...)
+#endif
+
 //------------------------------------------------------------------------------
 // Streamed input struct and helper functions.
 
@@ -102,6 +108,7 @@
   read_stream_t read;       // Used to fetch more bytes from the 'stream'.
   skip_stream_t skip;       // Used to advance the position in the 'stream'.
                             // Fallback to 'read' if 'skip' is null.
+  uint64_t num_read_bytes;  // Number of bytes read or skipped.
 } AvifInfoInternalStream;
 
 // Reads 'num_bytes' from the 'stream'. They are available at '*data'.
@@ -110,6 +117,7 @@
     AvifInfoInternalStream* stream, uint32_t num_bytes, const uint8_t** data) {
   *data = stream->read(stream->stream, num_bytes);
   AVIFINFO_CHECK(*data != NULL, kTruncated);
+  stream->num_read_bytes += num_bytes;
   return kFound;
 }
 
@@ -128,6 +136,7 @@
       return AvifInfoInternalRead(stream, num_bytes, &unused);
     }
     stream->skip(stream->stream, num_bytes);
+    stream->num_read_bytes += num_bytes;
   }
   return kFound;
 }
@@ -157,7 +166,14 @@
 
 typedef struct {
   uint8_t has_primary_item;  // True if "pitm" was parsed.
-  uint8_t has_alpha;         // True if an alpha "auxC" 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.
@@ -175,6 +191,20 @@
 // 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;
@@ -251,8 +281,9 @@
 // 'num_remaining_bytes' is the remaining size of the container of the 'box'
 // (either the file size itself or the content size of the parent of the 'box').
 static AvifInfoInternalStatus AvifInfoInternalParseBox(
-    AvifInfoInternalStream* stream, uint32_t num_remaining_bytes,
-    uint32_t* num_parsed_boxes, AvifInfoInternalBox* box) {
+    int nesting_level, AvifInfoInternalStream* stream,
+    uint32_t num_remaining_bytes, uint32_t* num_parsed_boxes,
+    AvifInfoInternalBox* box) {
   const uint8_t* data;
   // See ISO/IEC 14496-12:2012(E) 4.2
   uint32_t box_header_size = 8;  // box 32b size + 32b type (at least)
@@ -309,6 +340,8 @@
     // Instead of considering this file as invalid, skip unparsable boxes.
     if (!is_parsable) memcpy(box->type, "\0skp", 4);  // \0 so not a valid type
   }
+  AVIF_DEBUG_LOG("%*c", nesting_level * 2, ' ');
+  AVIF_DEBUG_LOG("Box type %.4s size %d\n", box->type, box->size);
   return kFound;
 }
 
@@ -317,15 +350,16 @@
 // Parses a 'stream' of an "ipco" box into 'features'.
 // "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth
 // and number of channels, and "auxC" is used for alpha.
-static AvifInfoInternalStatus ParseIpco(AvifInfoInternalStream* stream,
+static AvifInfoInternalStatus ParseIpco(int nesting_level,
+                                        AvifInfoInternalStream* stream,
                                         uint32_t num_remaining_bytes,
                                         uint32_t* num_parsed_boxes,
                                         AvifInfoInternalFeatures* features) {
   uint32_t box_index = 1;  // 1-based index. Used for iterating over properties.
   do {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
-                                                  num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+        nesting_level, stream, num_remaining_bytes, num_parsed_boxes, &box));
 
     if (!memcmp(box.type, "ispe", 4)) {
       // See ISO/IEC 23008-12:2017(E) 6.5.3.2
@@ -408,21 +442,40 @@
       // at https://aomediacodec.github.io/av1-avif/#auxiliary-images
       const char* kAlphaStr = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
       const uint32_t kAlphaStrLength = 44;  // Includes terminating character.
-      if (box.content_size >= kAlphaStrLength) {
+      const char* kGainmapStr = "urn:com:photo:aux:hdrgainmap";
+      const uint32_t kGainmapStrLength = 29;  // Includes terminating character.
+      uint32_t num_read_bytes = 0;
+      // Check for a gain map or for an alpha plane. Start with the gain map
+      // since the identifier is shorter.
+      if (box.content_size >= kGainmapStrLength) {
         const uint8_t* data;
         AVIFINFO_CHECK_FOUND(
-            AvifInfoInternalRead(stream, kAlphaStrLength, &data));
+            AvifInfoInternalRead(stream, kGainmapStrLength, &data));
+        num_read_bytes = kGainmapStrLength;
         const char* const aux_type = (const char*)data;
-        if (strcmp(aux_type, kAlphaStr) == 0) {
-          // Note: It is unlikely but it is possible that this alpha plane does
-          //       not belong to the primary item or a tile. Ignore this issue.
-          features->has_alpha = 1;
+        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) {
+          // The beginning of the aux type matches the alpha aux type string.
+          // Check the end as well.
+          const uint8_t* data2;
+          const uint32_t kEndLength = kAlphaStrLength - kGainmapStrLength;
+          AVIFINFO_CHECK_FOUND(
+              AvifInfoInternalRead(stream, kEndLength, &data2));
+          num_read_bytes = kAlphaStrLength;
+          if (strcmp((const char*)data2, &kAlphaStr[kGainmapStrLength]) == 0) {
+            // Note: It is unlikely but it is possible that this alpha plane
+            // does not belong to the primary item or a tile. Ignore this issue.
+            features->has_alpha = 1;
+          }
         }
-        AVIFINFO_CHECK_FOUND(
-            AvifInfoInternalSkip(stream, box.content_size - kAlphaStrLength));
-      } else {
-        AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
       }
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalSkip(stream, box.content_size - num_read_bytes));
     } else {
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
@@ -434,18 +487,20 @@
 
 // Parses a 'stream' of an "iprp" box into 'features'. The "ipco" box contain
 // the properties which are linked to items by the "ipma" box.
-static AvifInfoInternalStatus ParseIprp(AvifInfoInternalStream* stream,
+static AvifInfoInternalStatus ParseIprp(int nesting_level,
+                                        AvifInfoInternalStream* stream,
                                         uint32_t num_remaining_bytes,
                                         uint32_t* num_parsed_boxes,
                                         AvifInfoInternalFeatures* features) {
   do {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
-                                                  num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+        nesting_level, stream, num_remaining_bytes, num_parsed_boxes, &box));
 
     if (!memcmp(box.type, "ipco", 4)) {
-      AVIFINFO_CHECK_NOT_FOUND(
-          ParseIpco(stream, box.content_size, num_parsed_boxes, features));
+      AVIFINFO_CHECK_NOT_FOUND(ParseIpco(nesting_level + 1, stream,
+                                         box.content_size, num_parsed_boxes,
+                                         features));
     } else if (!memcmp(box.type, "ipma", 4)) {
       // See ISO/IEC 23008-12:2017(E) 9.3.2
       uint32_t num_read_bytes = 4;
@@ -521,14 +576,15 @@
 // The "dimg" boxes contain links between tiles and their parent items, which
 // can be used to infer bit depth and number of channels for the primary item
 // when the latter does not have these properties.
-static AvifInfoInternalStatus ParseIref(AvifInfoInternalStream* stream,
+static AvifInfoInternalStatus ParseIref(int nesting_level,
+                                        AvifInfoInternalStream* stream,
                                         uint32_t num_remaining_bytes,
                                         uint32_t* num_parsed_boxes,
                                         AvifInfoInternalFeatures* features) {
   do {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
-                                                  num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+        nesting_level, stream, num_remaining_bytes, num_parsed_boxes, &box));
 
     if (!memcmp(box.type, "dimg", 4)) {
       // See ISO/IEC 14496-12:2015(E) 8.11.12.2
@@ -584,18 +640,19 @@
 
 // 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(AvifInfoInternalStream* stream,
+static AvifInfoInternalStatus ParseMeta(int nesting_level,
+                                        AvifInfoInternalStream* stream,
                                         uint32_t num_remaining_bytes,
                                         uint32_t* num_parsed_boxes,
                                         AvifInfoInternalFeatures* features) {
   do {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
-                                                  num_parsed_boxes, &box));
-
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+        nesting_level, stream, num_remaining_bytes, num_parsed_boxes, &box));
     if (!memcmp(box.type, "pitm", 4)) {
       // See ISO/IEC 14496-12:2015(E) 8.11.4.2
       const uint32_t num_bytes_per_id = (box.version == 0) ? 2 : 4;
+      const uint64_t primary_item_id_location = stream->num_read_bytes;
       const uint8_t* data;
       AVIFINFO_CHECK(num_bytes_per_id <= num_remaining_bytes, kInvalid);
       AVIFINFO_CHECK_FOUND(
@@ -605,14 +662,18 @@
       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;
       AVIFINFO_CHECK_FOUND(
           AvifInfoInternalSkip(stream, box.content_size - num_bytes_per_id));
     } else if (!memcmp(box.type, "iprp", 4)) {
-      AVIFINFO_CHECK_NOT_FOUND(
-          ParseIprp(stream, box.content_size, num_parsed_boxes, features));
+      AVIFINFO_CHECK_NOT_FOUND(ParseIprp(nesting_level + 1, stream,
+                                         box.content_size, num_parsed_boxes,
+                                         features));
     } else if (!memcmp(box.type, "iref", 4)) {
-      AVIFINFO_CHECK_NOT_FOUND(
-          ParseIref(stream, box.content_size, num_parsed_boxes, features));
+      AVIFINFO_CHECK_NOT_FOUND(ParseIref(nesting_level + 1, stream,
+                                         box.content_size, num_parsed_boxes,
+                                         features));
     } else {
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
@@ -628,8 +689,9 @@
 static AvifInfoInternalStatus ParseFtyp(AvifInfoInternalStream* stream) {
   AvifInfoInternalBox box;
   uint32_t num_parsed_boxes = 0;
-  AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
-                                                &num_parsed_boxes, &box));
+  const int nesting_level = 0;
+  AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+      nesting_level, stream, AVIFINFO_MAX_SIZE, &num_parsed_boxes, &box));
   AVIFINFO_CHECK(!memcmp(box.type, "ftyp", 4), kInvalid);
   // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
   AVIFINFO_CHECK(box.content_size >= 8, kInvalid);  // major_brand,minor_version
@@ -653,10 +715,12 @@
                                         AvifInfoInternalFeatures* features) {
   while (1) {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
-                                                  num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
+        /*nesting_level=*/0, stream, AVIFINFO_MAX_SIZE, num_parsed_boxes,
+        &box));
     if (!memcmp(box.type, "meta", 4)) {
-      return ParseMeta(stream, box.content_size, num_parsed_boxes, features);
+      return ParseMeta(/*nesting_level=*/1, stream, box.content_size,
+                       num_parsed_boxes, features);
     } else {
       AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
@@ -716,19 +780,20 @@
 // Streamed input API
 
 AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read,
-                                    skip_stream_t skip) {
+                                      skip_stream_t skip) {
   if (read == NULL) return kAvifInfoNotEnoughData;
 
   AvifInfoInternalStream internal_stream;
   internal_stream.stream = stream;
   internal_stream.read = read;
   internal_stream.skip = skip;  // Fallbacks to 'read' if null.
+  internal_stream.num_read_bytes = 0;
   return AvifInfoInternalConvertStatus(ParseFtyp(&internal_stream));
 }
 
 AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read,
-                                    skip_stream_t skip,
-                                    AvifInfoFeatures* features) {
+                                         skip_stream_t skip,
+                                         AvifInfoFeatures* features) {
   if (features != NULL) memset(features, 0, sizeof(*features));
   if (read == NULL) return kAvifInfoNotEnoughData;
 
@@ -736,6 +801,7 @@
   internal_stream.stream = stream;
   internal_stream.read = read;
   internal_stream.skip = skip;  // Fallbacks to 'read' if null.
+  internal_stream.num_read_bytes = 0;
   uint32_t num_parsed_boxes = 0;
   AvifInfoInternalFeatures internal_features;
   memset(&internal_features, AVIFINFO_UNDEFINED, sizeof(internal_features));
diff --git a/avifinfo.h b/avifinfo.h
index 4c898c2..266c714 100644
--- a/avifinfo.h
+++ b/avifinfo.h
@@ -39,6 +39,14 @@
   uint32_t bit_depth;      // Likely 8, 10 or 12 bits per channel per pixel.
   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.
+  // Start location of the primary item id, in bytes.
+  // The primary item id is a big endian number stored on bytes
+  // primary_item_id_location to primary_item_id_location+primary_item_id_bytes.
+  uint64_t primary_item_id_location;
+  // Number of bytes of the primary item id.
+  uint8_t primary_item_id_bytes;
 } AvifInfoFeatures;
 
 //------------------------------------------------------------------------------
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc
index c0cb7c7..f7fcfc2 100644
--- a/tests/avifinfo_test.cc
+++ b/tests/avifinfo_test.cc
@@ -29,9 +29,32 @@
                                                                      : Data();
 }
 
-bool AreEqual(const AvifInfoFeatures& a, const AvifInfoFeatures& b) {
-  return a.width == b.width && a.height == b.height &&
-         a.bit_depth == b.bit_depth && a.num_channels == b.num_channels;
+void WriteBigEndian(uint32_t value, uint32_t num_bytes, uint8_t* input) {
+  for (int i = num_bytes - 1; i >= 0; --i) {
+    input[i] = value & 0xff;
+    value >>= 8;
+  }
+}
+
+bool SetPrimaryItemIdToGainmapId(Data& input) {
+  AvifInfoFeatures f;
+  if (AvifInfoGetFeatures(input.data(), input.size(), &f) != kAvifInfoOk) {
+    return false;
+  }
+  WriteBigEndian(f.gainmap_item_id, f.primary_item_id_bytes,
+                 &input[f.primary_item_id_location]);
+  return true;
+}
+
+void ExpectEqual(const AvifInfoFeatures& actual, const AvifInfoFeatures& expected) {
+  EXPECT_EQ(actual.width, expected.width);
+  EXPECT_EQ(actual.height, expected.height);
+  EXPECT_EQ(actual.bit_depth, expected.bit_depth);
+  EXPECT_EQ(actual.num_channels, expected.num_channels);
+  EXPECT_EQ(actual.has_gainmap, expected.has_gainmap);
+  EXPECT_EQ(actual.gainmap_item_id, expected.gainmap_item_id);
+  EXPECT_EQ(actual.primary_item_id_location, expected.primary_item_id_location);
+  EXPECT_EQ(actual.primary_item_id_bytes, expected.primary_item_id_bytes);
 }
 
 //------------------------------------------------------------------------------
@@ -41,12 +64,70 @@
   const Data input = LoadFile("avifinfo_test_1x1.avif");
   ASSERT_FALSE(input.empty());
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  ExpectEqual(f, {.width = 1u,
+                  .height = 1u,
+                  .bit_depth = 8u,
+                  .num_channels = 3u,
+                  .has_gainmap = 0u,
+                  .primary_item_id_location = 96u,
+                  .primary_item_id_bytes = 2u});
 }
 
+TEST(AvifInfoGetTest, WithAlpha) {
+  const Data input = LoadFile("avifinfo_test_2x2_alpha.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 = 2u,
+                  .height = 2u,
+                  .bit_depth = 8u,
+                  .num_channels = 4u,
+                  .has_gainmap = 0u,
+                  .primary_item_id_location = 96u,
+                  .primary_item_id_bytes = 2u});
+
+}
+
+TEST(AvifInfoGetTest, WithGainmap) {
+  const Data input = LoadFile("avifinfo_test_20x20_gainmap.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 = 20u,
+                  .height = 20u,
+                  .bit_depth = 8u,
+                  .num_channels = 3u,
+                  .has_gainmap = 1u,
+                  .gainmap_item_id = 2u,
+                  .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);
+  // TODO(maryla-uc): find a small test file with a gainmap that is smaller
+  // than the main image.
+  ExpectEqual(gainmap_f, {.width = 20u,
+                          .height = 20u,
+                          .bit_depth = 8u,
+                          .num_channels = 1u,  // the gainmap is monochrome
+                          .has_gainmap = 1u,
+                          .gainmap_item_id = 2u,
+                          .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
@@ -55,10 +136,17 @@
       LoadFile("avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif");
   ASSERT_FALSE(input.empty());
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(f, {1u, 1u, 10u, 3u}));
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  ExpectEqual(f, {.width = 1u,
+                  .height = 1u,
+                  .bit_depth = 10u,
+                  .num_channels = 3u,
+                  .has_gainmap = 0u,
+                  .primary_item_id_location = 104u,
+                  .primary_item_id_bytes = 2u});
+
 }
 
 TEST(AvifInfoGetTest, EnoughBytes) {
@@ -69,17 +157,23 @@
   input.resize(std::search(input.begin(), input.end(), kMdatTag, kMdatTag + 4) -
                input.begin());
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
-  EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+  ExpectEqual(f, {.width = 1u,
+                  .height = 1u,
+                  .bit_depth = 8u,
+                  .num_channels = 3u,
+                  .has_gainmap = 0u,
+                  .primary_item_id_location = 96u,
+                  .primary_item_id_bytes = 2u});
 }
 
 TEST(AvifInfoGetTest, Null) {
   const Data input = LoadFile("avifinfo_test_1x1.avif");
   ASSERT_FALSE(input.empty());
 
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr),
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr),
             kAvifInfoOk);
 }
 
@@ -87,10 +181,10 @@
 // Negative tests
 
 TEST(AvifInfoGetTest, Empty) {
-  EXPECT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData);
+  ASSERT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData);
-  EXPECT_TRUE(AreEqual(f, {0}));
+  ASSERT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData);
+  ExpectEqual(f, {0});
 }
 
 TEST(AvifInfoGetTest, NotEnoughBytes) {
@@ -101,9 +195,9 @@
   input.resize(std::search(input.begin(), input.end(), kIpmaTag, kIpmaTag + 4) -
                input.begin());
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoNotEnoughData);
 }
 
@@ -114,11 +208,11 @@
   const uint8_t kIspeTag[] = {'i', 's', 'p', 'e'};
   std::search(input.begin(), input.end(), kIspeTag, kIspeTag + 4)[0] = 'a';
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoInvalidFile);
-  EXPECT_TRUE(AreEqual(f, {0}));
+  ExpectEqual(f, {0});
 }
 
 TEST(AvifInfoGetTest, MetaBoxIsTooBig) {
@@ -132,11 +226,11 @@
   meta_tag[-1] = 1;  // 32-bit "1" then 4-char "meta" then 64-bit size.
   input.insert(meta_tag + 4, {255, 255, 255, 255, 255, 255, 255, 255});
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
+  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
             kAvifInfoTooComplex);
-  EXPECT_TRUE(AreEqual(f, {0}));
+  ExpectEqual(f, {0});
 }
 
 TEST(AvifInfoGetTest, TooManyBoxes) {
@@ -150,22 +244,22 @@
     input.insert(input.end(), kBox, kBox + kBox[3]);
   }
 
-  EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()),
+  ASSERT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()),
                                 input.size() * 4, &f),
             kAvifInfoTooComplex);
 }
 
 TEST(AvifInfoReadTest, Null) {
-  EXPECT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr,
+  ASSERT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr,
                                    /*skip=*/nullptr),
             kAvifInfoNotEnoughData);
   AvifInfoFeatures f;
-  EXPECT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr,
+  ASSERT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr,
                                       /*skip=*/nullptr, &f),
             kAvifInfoNotEnoughData);
-  EXPECT_TRUE(AreEqual(f, {0}));
+  ExpectEqual(f, {0});
 }
 
 //------------------------------------------------------------------------------
diff --git a/tests/avifinfo_test_20x20_gainmap.avif b/tests/avifinfo_test_20x20_gainmap.avif
new file mode 100644
index 0000000..e012b7a
--- /dev/null
+++ b/tests/avifinfo_test_20x20_gainmap.avif
Binary files differ
diff --git a/tests/avifinfo_test_2x2_alpha.avif b/tests/avifinfo_test_2x2_alpha.avif
new file mode 100644
index 0000000..c009fa9
--- /dev/null
+++ b/tests/avifinfo_test_2x2_alpha.avif
Binary files differ