Streamed input API

Refactor the code to:
- sequentially read all boxes once into AvifInfoInternalFeatures,
- after "ipma" and "iref", check if AvifInfoInternalFeatures contain
  all necessary information to output AvifInfoFeatures.
This allows to fetch just a few bytes at a time from the stream,
in order, in a contiguous manner and never twice the same data.
The drawback is the ~250-byte long AvifInfoInternalFeatures struct
that is stored on the stack to avoid dynamic allocation. It can be
decreased or increased to adjust the variety of handled input AVIFs.

Change-Id: I98ccf4c55706f6c88d65008ccbcbefc782d2be87
diff --git a/avifinfo.c b/avifinfo.c
index 65f0b68..9630732 100644
--- a/avifinfo.c
+++ b/avifinfo.c
@@ -26,7 +26,15 @@
 
 // uint32_t is used everywhere in this file. It is unlikely to be insufficient
 // to parse AVIF headers. Clamp any input to 2^32-1 for simplicity.
-static const uint32_t kAvifInfoInternalMaxSize = UINT32_MAX;
+#define AVIFINFO_MAX_SIZE UINT32_MAX
+// AvifInfoInternalFeatures uses uint8_t to store values and the number of
+// values is clamped to 32 to limit the stack size.
+#define AVIFINFO_MAX_VALUE UINT8_MAX
+#define AVIFINFO_UNDEFINED 0
+// Maximum number of stored associations. Past that, they are skipped.
+#define AVIFINFO_MAX_TILES 16
+#define AVIFINFO_MAX_PROPS 32
+#define AVIFINFO_MAX_FEATURES 8
 
 // Reads an unsigned integer from 'input' with most significant bits first.
 // 'input' must be at least 'num_bytes'-long.
@@ -79,54 +87,192 @@
   AVIFINFO_CHECK_STATUS_IS((check_status), kNotFound)
 
 //------------------------------------------------------------------------------
+// Streamed input struct and helper functions.
+
+typedef struct {
+  void* stream;             // User-defined data.
+  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.
+  uint32_t num_read_bytes;  // Total number of read bytes.
+} AvifInfoInternalStream;
+
+// Reads 'num_bytes' from the 'stream'. They are available at '*data'.
+// 'num_bytes' must be greater than zero.
+static AvifInfoInternalStatus AvifInfoInternalRead(
+    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;
+}
+
+// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero.
+static AvifInfoInternalStatus AvifInfoInternalSkip(
+    AvifInfoInternalStream* stream, uint32_t num_bytes) {
+  // Avoid a call to the user-defined function for nothing.
+  if (num_bytes > 0) {
+    if (stream->skip == NULL) {
+      const uint8_t* unused;
+      while (num_bytes > AVIFINFO_MAX_NUM_READ_BYTES) {
+        AVIFINFO_CHECK_FOUND(
+            AvifInfoInternalRead(stream, AVIFINFO_MAX_NUM_READ_BYTES, &unused));
+        num_bytes -= AVIFINFO_MAX_NUM_READ_BYTES;
+      }
+      return AvifInfoInternalRead(stream, num_bytes, &unused);
+    }
+    stream->skip(stream->stream, num_bytes);
+    stream->num_read_bytes += num_bytes;
+  }
+  return kFound;
+}
+
+//------------------------------------------------------------------------------
+// Features are parsed into temporary property associations.
+
+typedef struct {
+  uint8_t tile_item_id;
+  uint8_t parent_item_id;
+} AvifInfoInternalTile;  // Tile item id <-> parent item id associations.
+
+typedef struct {
+  uint8_t property_index;
+  uint8_t item_id;
+} AvifInfoInternalProp;  // Property index <-> item id associations.
+
+typedef struct {
+  uint8_t property_index;
+  uint32_t width, height;
+} AvifInfoInternalDimProp;  // Property <-> features associations.
+
+typedef struct {
+  uint8_t property_index;
+  uint8_t bit_depth, num_channels;
+} AvifInfoInternalChanProp;  // Property <-> features associations.
+
+typedef struct {
+  uint8_t has_primary_item;  // True if "pitm" was parsed.
+  uint8_t has_alpha;         // True if an alpha "auxC" was parsed.
+  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 num_tiles;
+  AvifInfoInternalTile tiles[AVIFINFO_MAX_TILES];
+  uint8_t num_props;
+  AvifInfoInternalProp props[AVIFINFO_MAX_PROPS];
+  uint8_t num_dim_props;
+  AvifInfoInternalDimProp dim_props[AVIFINFO_MAX_FEATURES];
+  uint8_t num_chan_props;
+  AvifInfoInternalChanProp chan_props[AVIFINFO_MAX_FEATURES];
+} AvifInfoInternalFeatures;
+
+// 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) {
+  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;
+
+    // Retrieve the width and height of the primary item if not already done.
+    if (target_item_id == f->primary_item_id &&
+        (f->primary_item_features.width == AVIFINFO_UNDEFINED ||
+         f->primary_item_features.height == AVIFINFO_UNDEFINED)) {
+      for (uint32_t i = 0; i < f->num_dim_props; ++i) {
+        if (f->dim_props[i].property_index != property_index) continue;
+        f->primary_item_features.width = f->dim_props[i].width;
+        f->primary_item_features.height = f->dim_props[i].height;
+        if (f->primary_item_features.bit_depth != AVIFINFO_UNDEFINED &&
+            f->primary_item_features.num_channels != AVIFINFO_UNDEFINED) {
+          return kFound;
+        }
+        break;
+      }
+    }
+    // Retrieve the bit depth and number of channels of the target item if not
+    // already done.
+    if (f->primary_item_features.bit_depth == AVIFINFO_UNDEFINED ||
+        f->primary_item_features.num_channels == AVIFINFO_UNDEFINED) {
+      for (uint32_t i = 0; i < f->num_chan_props; ++i) {
+        if (f->chan_props[i].property_index != property_index) continue;
+        f->primary_item_features.bit_depth = f->chan_props[i].bit_depth;
+        f->primary_item_features.num_channels = f->chan_props[i].num_channels;
+        if (f->primary_item_features.width != AVIFINFO_UNDEFINED &&
+            f->primary_item_features.height != AVIFINFO_UNDEFINED) {
+          return kFound;
+        }
+        break;
+      }
+    }
+  }
+
+  // Check for the bit_depth and num_channels in a tile if not yet found.
+  for (uint32_t tile = 0; tile < f->num_tiles && tile_depth < 3; ++tile) {
+    if (f->tiles[tile].parent_item_id != target_item_id) continue;
+    AVIFINFO_CHECK_NOT_FOUND(AvifInfoInternalGetItemFeatures(
+        f, f->tiles[tile].tile_item_id, tile_depth + 1));
+  }
+  AVIFINFO_RETURN(kNotFound);
+}
+
+// Generates the 'f->primary_item_features' from the AvifInfoInternalFeatures.
+// Returns kNotFound if there is not enough information.
+static AvifInfoInternalStatus AvifInfoInternalGetPrimaryItemFeatures(
+    AvifInfoInternalFeatures* f) {
+  // Nothing to do without the primary item ID.
+  AVIFINFO_CHECK(f->has_primary_item, kNotFound);
+  // Early exit.
+  AVIFINFO_CHECK(f->num_dim_props > 0 && f->num_chan_props, kNotFound);
+  AVIFINFO_CHECK_FOUND(
+      AvifInfoInternalGetItemFeatures(f, f->primary_item_id, /*tile_depth=*/0));
+
+  // "auxC" is parsed before the "ipma" properties so it is known now, if any.
+  if (f->has_alpha) ++f->primary_item_features.num_channels;
+  return kFound;
+}
+
+//------------------------------------------------------------------------------
 // Box header parsing and various size checks.
 
 typedef struct {
-  uint32_t size;              // In bytes.
-  const uint8_t* type;        // Points to four characters.
-  uint32_t version;           // 0 or actual version if this is a full box.
-  uint32_t flags;             // 0 or actual value if this is a full box.
-  uint32_t content_size;      // 'size' minus the header size.
-  uint32_t content_position;  // Position in bytes of the 'content' of this box
-                              // relative to its container.
-  const uint8_t* content;     // Content bytes of this box (after its header).
+  uint32_t size;          // In bytes.
+  uint8_t type[4];        // Four characters.
+  uint32_t version;       // 0 or actual version if this is a full box.
+  uint32_t flags;         // 0 or actual value if this is a full box.
+  uint32_t content_size;  // 'size' minus the header size.
 } AvifInfoInternalBox;
 
-// Reads the header of a 'box' starting at 'bytes + position'.
-// 'num_bytes' is the number of available 'bytes'.
-// 'max_num_bytes' is the size of the container of the 'box' (either the file
-// itself or the content of the parent of the 'box').
+// Reads the header of a 'box' starting at the beginning of a 'stream'.
+// '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(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    uint32_t position, uint32_t* num_parsed_boxes, AvifInfoInternalBox* box) {
+    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)
-  AVIFINFO_CHECK(position <= kAvifInfoInternalMaxSize - box_header_size,
-                 kAborted);
-  AVIFINFO_CHECK(position + box_header_size <= max_num_bytes, kInvalid);
-  AVIFINFO_CHECK(position + box_header_size <= num_bytes, kTruncated);
-  box->size = AvifInfoInternalReadBigEndian(bytes + position, sizeof(uint32_t));
+  AVIFINFO_CHECK(box_header_size <= num_remaining_bytes, kInvalid);
+  AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data));
+  box->size = AvifInfoInternalReadBigEndian(data, sizeof(uint32_t));
+  memcpy(box->type, data + 4, 4);
   // 'box->size==1' means 64-bit size should be read after the box type.
   // 'box->size==0' means this box extends to all remaining bytes.
   if (box->size == 1) {
     box_header_size += 8;
-    AVIFINFO_CHECK(position + box_header_size <= max_num_bytes, kInvalid);
-    AVIFINFO_CHECK(position + box_header_size <= num_bytes, kTruncated);
+    AVIFINFO_CHECK(box_header_size <= num_remaining_bytes, kInvalid);
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data));
     // Stop the parsing if any box has a size greater than 4GB.
-    AVIFINFO_CHECK(AvifInfoInternalReadBigEndian(bytes + position + 8,
-                                                 sizeof(uint32_t)) == 0,
+    AVIFINFO_CHECK(AvifInfoInternalReadBigEndian(data, sizeof(uint32_t)) == 0,
                    kAborted);
     // Read the 32 least-significant bits.
-    box->size =
-        AvifInfoInternalReadBigEndian(bytes + position + 12, sizeof(uint32_t));
+    box->size = AvifInfoInternalReadBigEndian(data + 4, sizeof(uint32_t));
   } else if (box->size == 0) {
-    box->size = max_num_bytes - position;
+    box->size = num_remaining_bytes;
   }
   AVIFINFO_CHECK(box->size >= box_header_size, kInvalid);
-  AVIFINFO_CHECK(box->size <= kAvifInfoInternalMaxSize - position, kAborted);
-  AVIFINFO_CHECK(position + box->size <= max_num_bytes, kInvalid);
-  AVIFINFO_CHECK(position + box_header_size <= num_bytes, kTruncated);
-  box->type = bytes + position + 4;
+  AVIFINFO_CHECK(box->size <= AVIFINFO_MAX_SIZE - stream->num_read_bytes,
+                 kAborted);
+  AVIFINFO_CHECK(box->size <= num_remaining_bytes, kInvalid);
 
   const int has_fullbox_header =
       !memcmp(box->type, "meta", 4) || !memcmp(box->type, "pitm", 4) ||
@@ -135,10 +281,7 @@
       !memcmp(box->type, "auxC", 4);
   if (has_fullbox_header) box_header_size += 4;
   AVIFINFO_CHECK(box->size >= box_header_size, kInvalid);
-  box->content_position = position + box_header_size;
-  AVIFINFO_CHECK(box->content_position <= num_bytes, kTruncated);
   box->content_size = box->size - box_header_size;
-  box->content = bytes + box->content_position;
   // Avoid timeouts. The maximum number of parsed boxes is arbitrary.
   ++*num_parsed_boxes;
   AVIFINFO_CHECK(*num_parsed_boxes < 4096, kAborted);
@@ -146,8 +289,9 @@
   box->version = 0;
   box->flags = 0;
   if (has_fullbox_header) {
-    box->version = AvifInfoInternalReadBigEndian(bytes + position + 8, 1);
-    box->flags = AvifInfoInternalReadBigEndian(bytes + position + 9, 3);
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
+    box->version = AvifInfoInternalReadBigEndian(data, 1);
+    box->flags = AvifInfoInternalReadBigEndian(data + 1, 3);
     // See AV1 Image File Format (AVIF) 8.1
     // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when
     // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged).
@@ -160,572 +304,448 @@
     if (!memcmp(box->type, "iref", 4)) is_parsable = (box->version <= 1);
     if (!memcmp(box->type, "auxC", 4)) is_parsable = (box->version <= 0);
     // Instead of considering this file as invalid, skip unparsable boxes.
-    if (!is_parsable) box->type = (const uint8_t*)"\0skip";
+    if (!is_parsable) memcpy(box->type, "\0skp", 4);  // \0 so not a valid type
   }
   return kFound;
 }
 
-// Returns kFound if 'min_size' bytes can be read from the 'box.content' now.
-// 'num_bytes' is the number of available bytes of the parent of the 'box'.
-static AvifInfoInternalStatus AccessContent(const AvifInfoInternalBox* box,
-                                            uint32_t num_bytes,
-                                            uint32_t min_size) {
-  AVIFINFO_CHECK(box->content_size >= min_size, kInvalid);
-  AVIFINFO_CHECK(box->content_position + min_size <= num_bytes, kTruncated);
-  return kFound;
-}
-
 //------------------------------------------------------------------------------
-// Search if the file identifies itself as AVIF through an "ftyp" box.
 
-static AvifInfoInternalStatus ParseFileForBrand(const uint8_t* bytes,
-                                                uint32_t num_bytes,
-                                                uint32_t file_size,
-                                                uint32_t* num_parsed_boxes) {
-  uint32_t position = 0;  // Within 'bytes' (that points to first byte of file).
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, file_size, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "ftyp", 4)) {
-      // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
-      AVIFINFO_CHECK(box.content_size >= 8, kInvalid);  // major_brand + version
-      for (uint32_t i = 0; i < box.content_size; i += 4) {
-        AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, i + 4));
-        if (i == 4) continue;  // Skip minor_version.
-        if (!memcmp(box.content + i, "avif", 4) ||
-            !memcmp(box.content + i, "avis", 4)) {
-          return kFound;
-        }
-        AVIFINFO_CHECK(i <= 32 * 4, kAborted);  // Be reasonable.
-      }
-      AVIFINFO_RETURN(kInvalid);  // Only one "ftyp" allowed per file.
-    }
-    position += box.size;
-    // File is valid only if the end of at least one box is at the same position
-    // as the end of the container. Oddities are caught when parsing further.
-  } while (position != file_size);
-  AVIFINFO_RETURN(kInvalid);  // There should be one "ftyp" box.
-}
-
-//------------------------------------------------------------------------------
-// Search the primary item ID through "meta > pitm" boxes.
-
-static AvifInfoInternalStatus ParseMetaForPrimaryItemId(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    uint32_t* num_parsed_boxes, uint32_t* primary_item_id) {
-  uint32_t position = 0;  // Within 'bytes' (first byte of "meta" box content).
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, 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;
-      AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, num_bytes_per_id));
-      *primary_item_id =
-          AvifInfoInternalReadBigEndian(box.content + 0, num_bytes_per_id);
-      return kFound;
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-
-  // According to ISO/IEC 14496-12:2012(E) 8.11.1.1, there is at most one "meta"
-  // per file. No "pitm" until now means never.
-  AVIFINFO_RETURN(kInvalid);
-}
-
-static AvifInfoInternalStatus ParseFileForPrimaryItemId(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t file_size,
-    uint32_t* num_parsed_boxes, uint32_t* primary_item_id) {
-  uint32_t position = 0;  // Within 'bytes' (that points to first byte of file).
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, file_size, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "meta", 4)) {
-      return ParseMetaForPrimaryItemId(
-          box.content, num_bytes - box.content_position, box.content_size,
-          num_parsed_boxes, primary_item_id);
-    }
-    position += box.size;
-  } while (position != file_size);
-  AVIFINFO_RETURN(kInvalid);  // No "meta" is an issue.
-}
-
-//------------------------------------------------------------------------------
-// Search the features of an item given its ID through "meta > iprp" boxes.
-
-static AvifInfoInternalStatus ParseIpcoForFeaturesInProperty(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    uint32_t target_property_index, uint32_t* num_parsed_boxes,
-    AvifInfoFeatures* features) {
-  uint32_t position = 0;
+// 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,
+                                        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(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
+                                                  num_parsed_boxes, &box));
 
-    if (box_index != target_property_index) {
-      // Skip.
-    } else if (features->width == 0 && !memcmp(box.type, "ispe", 4)) {
+    if (!memcmp(box.type, "ispe", 4)) {
       // See ISO/IEC 23008-12:2017(E) 6.5.3.2
-      AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, 4 + 4));
-      features->width = AvifInfoInternalReadBigEndian(box.content + 0, 4);
-      features->height = AvifInfoInternalReadBigEndian(box.content + 4, 4);
-      AVIFINFO_CHECK(features->width != 0 && features->height != 0, kInvalid);
-      return kFound;
-    } else if (features->num_channels == 0 && !memcmp(box.type, "pixi", 4)) {
+      const uint8_t* data;
+      AVIFINFO_CHECK(box.content_size >= 8, kInvalid);
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data));
+      const uint32_t width = AvifInfoInternalReadBigEndian(data + 0, 4);
+      const uint32_t height = AvifInfoInternalReadBigEndian(data + 4, 4);
+      AVIFINFO_CHECK(width != 0 && height != 0, kInvalid);
+      if (features->num_dim_props < AVIFINFO_MAX_FEATURES &&
+          box_index <= AVIFINFO_MAX_VALUE) {
+        features->dim_props[features->num_dim_props].property_index = box_index;
+        features->dim_props[features->num_dim_props].width = width;
+        features->dim_props[features->num_dim_props].height = height;
+        ++features->num_dim_props;
+      } else {
+        features->data_was_skipped = 1;
+      }
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size - 8));
+    } else if (!memcmp(box.type, "pixi", 4)) {
       // See ISO/IEC 23008-12:2017(E) 6.5.6.2
-      AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, 1));
-      features->num_channels =
-          AvifInfoInternalReadBigEndian(box.content + 0, 1);
-      AVIFINFO_CHECK(features->num_channels >= 1, kInvalid);
-      AVIFINFO_CHECK_FOUND(
-          AccessContent(&box, num_bytes, 1 + features->num_channels));
-      features->bit_depth = AvifInfoInternalReadBigEndian(box.content + 1, 1);
-      AVIFINFO_CHECK(features->bit_depth >= 1, kInvalid);
-      for (uint32_t i = 1; i < features->num_channels; ++i) {
-        const uint32_t bit_depth =
-            AvifInfoInternalReadBigEndian(box.content + 1 + i, 1);
+      const uint8_t* data;
+      AVIFINFO_CHECK(box.content_size >= 1, kInvalid);
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data));
+      const uint32_t num_channels = AvifInfoInternalReadBigEndian(data + 0, 1);
+      AVIFINFO_CHECK(num_channels >= 1, kInvalid);
+      AVIFINFO_CHECK(box.content_size >= 1 + num_channels, kInvalid);
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data));
+      const uint32_t bit_depth = AvifInfoInternalReadBigEndian(data, 1);
+      AVIFINFO_CHECK(bit_depth >= 1, kInvalid);
+      for (uint32_t i = 1; i < num_channels; ++i) {
+        AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data));
         // Bit depth should be the same for all channels.
-        AVIFINFO_CHECK(bit_depth == features->bit_depth, kInvalid);
+        AVIFINFO_CHECK(AvifInfoInternalReadBigEndian(data, 1) == bit_depth,
+                       kInvalid);
         AVIFINFO_CHECK(i <= 32, kAborted);  // Be reasonable.
       }
-      return kFound;
-    } else if (features->num_channels == 0 && !memcmp(box.type, "av1C", 4)) {
+      if (features->num_chan_props < AVIFINFO_MAX_FEATURES &&
+          box_index <= AVIFINFO_MAX_VALUE && bit_depth <= AVIFINFO_MAX_VALUE &&
+          num_channels <= AVIFINFO_MAX_VALUE) {
+        features->chan_props[features->num_chan_props].property_index =
+            box_index;
+        features->chan_props[features->num_chan_props].bit_depth = bit_depth;
+        features->chan_props[features->num_chan_props].num_channels =
+            num_channels;
+        ++features->num_chan_props;
+      } else {
+        features->data_was_skipped = 1;
+      }
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalSkip(stream, box.content_size - (1 + num_channels)));
+    } else if (!memcmp(box.type, "av1C", 4)) {
       // See AV1 Codec ISO Media File Format Binding 2.3.1
       // at https://aomediacodec.github.io/av1-isobmff/#av1c
       // Only parse the necessary third byte. Assume that the others are valid.
-      AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, 3));
-      const uint32_t fields = AvifInfoInternalReadBigEndian(box.content + 2, 1);
-      const int high_bitdepth = (fields & 0x40) != 0;
-      const int twelve_bit = (fields & 0x20) != 0;
-      const int monochrome = (fields & 0x10) != 0;
+      const uint8_t* data;
+      AVIFINFO_CHECK(box.content_size >= 3, kInvalid);
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 3, &data));
+      const int high_bitdepth = (data[2] & 0x40) != 0;
+      const int twelve_bit = (data[2] & 0x20) != 0;
+      const int monochrome = (data[2] & 0x10) != 0;
       if (twelve_bit) {
         AVIFINFO_CHECK(high_bitdepth, kInvalid);
       }
-      features->num_channels = monochrome ? 1 : 3;
-      features->bit_depth = high_bitdepth ? twelve_bit ? 12 : 10 : 8;
-      return kFound;
-    }
-    ++box_index;
-    position += box.size;
-  } while (position != max_num_bytes && box_index <= target_property_index);
-  AVIFINFO_RETURN(kNotFound);
-}
-
-static AvifInfoInternalStatus ParseIprpForFeaturesInProperty(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    uint32_t target_property_index, uint32_t* num_parsed_boxes,
-    AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "ipco", 4)) {
-      return ParseIpcoForFeaturesInProperty(
-          box.content, num_bytes - box.content_position, box.content_size,
-          target_property_index, num_parsed_boxes, features);
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kInvalid);  // No "ipco" in "iprp" is an issue.
-}
-
-static AvifInfoInternalStatus ParseIprpForFeatures(const uint8_t* bytes,
-                                                   uint32_t num_bytes,
-                                                   uint32_t max_num_bytes,
-                                                   uint32_t target_item_id,
-                                                   uint32_t* num_parsed_boxes,
-                                                   AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "ipma", 4)) {
-      // See ISO/IEC 23008-12:2017(E) 9.3.2
-      AVIFINFO_CHECK_FOUND(AccessContent(&box, num_bytes, 4));
-      const uint32_t entry_count =
-          AvifInfoInternalReadBigEndian(box.content + 0, 4);
-      uint32_t offset = 4;
-      const uint32_t id_num_bytes = (box.version < 1) ? 2 : 4;
-      const uint32_t index_num_bytes = (box.flags & 1) ? 2 : 1;
-      const uint32_t essential_bit_mask = (box.flags & 1) ? 0x8000 : 0x80;
-
-      for (uint32_t entry = 0; entry < entry_count; ++entry) {
-        AVIFINFO_CHECK_FOUND(
-            AccessContent(&box, num_bytes, offset + id_num_bytes + 1));
-        const uint32_t item_id =
-            AvifInfoInternalReadBigEndian(box.content + offset, id_num_bytes);
-
-        offset += id_num_bytes;
-        const uint32_t association_count =
-            AvifInfoInternalReadBigEndian(box.content + offset, 1);
-        offset += 1;
-
-        for (uint32_t property = 0; property < association_count; ++property) {
-          AVIFINFO_CHECK_FOUND(
-              AccessContent(&box, num_bytes, offset + index_num_bytes));
-          const uint32_t value = AvifInfoInternalReadBigEndian(
-              box.content + offset, index_num_bytes);
-          offset += index_num_bytes;
-
-          if (item_id == target_item_id) {
-            // const int essential = (value & essential_bit_mask);  // Unused.
-            const uint32_t property_index = (value & ~essential_bit_mask);
-
-            // Parse again at the same "iprp" level to find the associated
-            // "ipco" and the "ispe", "pixi" or "av1C" within.
-            const AvifInfoInternalStatus status =
-                ParseIprpForFeaturesInProperty(bytes, num_bytes, max_num_bytes,
-                                               property_index, num_parsed_boxes,
-                                               features);
-            if (status != kFound) {
-              // Stop in case of error, carry on if not found.
-              AVIFINFO_CHECK_NOT_FOUND(status);
-            } else if (features->width != 0 && features->height != 0 &&
-                       features->num_channels != 0 &&
-                       features->bit_depth != 0) {
-              return kFound;  // Found everything. Otherwise carry on.
-            }
-          }
-          AVIFINFO_CHECK(property <= 32, kAborted);  // Be reasonable.
-        }
-        AVIFINFO_CHECK(entry <= 32, kAborted);  // Be reasonable.
+      if (features->num_chan_props < AVIFINFO_MAX_FEATURES &&
+          box_index <= AVIFINFO_MAX_VALUE) {
+        features->chan_props[features->num_chan_props].property_index =
+            box_index;
+        features->chan_props[features->num_chan_props].bit_depth =
+            high_bitdepth ? twelve_bit ? 12 : 10 : 8;
+        features->chan_props[features->num_chan_props].num_channels =
+            monochrome ? 1 : 3;
+        ++features->num_chan_props;
+      } else {
+        features->data_was_skipped = 1;
       }
-
-      // According to ISO/IEC 14496-12:2012(E) 8.11.1.1, there is at most one
-      // "meta" per file. According to ISO/IEC 23008-12:2017(E) 9.3.1, there is
-      // exactly one "ipma" per "iprp" and at most one "iprp" per "meta".
-      // The primary properties shall have been found now.
-      if (features->width != 0 && features->height != 0) {
-        // Exception: The bit depth and number of channels may be referenced
-        //            in a tile and not in the primary item of item type "grid".
-        return kNotFound;  // Continue the search at a higher level.
-      }
-      AVIFINFO_RETURN(kInvalid);
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kInvalid);  // No "ipma" in "iprp" is an issue.
-}
-
-static AvifInfoInternalStatus ParseMetaForFeatures(const uint8_t* bytes,
-                                                   uint32_t num_bytes,
-                                                   uint32_t max_num_bytes,
-                                                   uint32_t target_item_id,
-                                                   uint32_t* num_parsed_boxes,
-                                                   AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "iprp", 4)) {
-      return ParseIprpForFeatures(box.content, num_bytes - box.content_position,
-                                  box.content_size, target_item_id,
-                                  num_parsed_boxes, features);
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kInvalid);  // No "iprp" in "meta" is an issue.
-}
-
-static AvifInfoInternalStatus ParseFileForFeatures(const uint8_t* bytes,
-                                                   uint32_t num_bytes,
-                                                   uint32_t file_size,
-                                                   uint32_t target_item_id,
-                                                   uint32_t* num_parsed_boxes,
-                                                   AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, file_size, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "meta", 4)) {
-      return ParseMetaForFeatures(box.content, num_bytes - box.content_position,
-                                  box.content_size, target_item_id,
-                                  num_parsed_boxes, features);
-    }
-    position += box.size;
-  } while (position != file_size);
-  AVIFINFO_RETURN(kInvalid);  // No "meta" is an issue.
-}
-
-//------------------------------------------------------------------------------
-// Search if a tile contains features through "meta > iref > dimg" boxes.
-
-static AvifInfoInternalStatus ParseIrefForFeaturesInTiles(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    const uint8_t* meta_bytes, uint32_t meta_num_bytes,
-    uint32_t meta_max_num_bytes, uint32_t primary_item_id,
-    uint32_t* num_parsed_boxes, AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "dimg", 4)) {
-      // See ISO/IEC 14496-12:2015(E) 8.11.12.2
-      const uint32_t num_bytes_per_id = (box.version == 0) ? 2 : 4;
-      uint32_t offset = 0;
-      AVIFINFO_CHECK_FOUND(
-          AccessContent(&box, num_bytes, num_bytes_per_id + 2));
-      const uint32_t from_item_id =
-          AvifInfoInternalReadBigEndian(box.content + offset, num_bytes_per_id);
-      offset += num_bytes_per_id;
-      if (from_item_id == primary_item_id) {
-        const uint32_t reference_count =
-            AvifInfoInternalReadBigEndian(box.content + offset, 2);
-        offset += 2;
-        for (uint32_t i = 0; i < reference_count; ++i) {
-          AVIFINFO_CHECK_FOUND(
-              AccessContent(&box, num_bytes, offset + num_bytes_per_id));
-          const uint32_t to_item_id = AvifInfoInternalReadBigEndian(
-              box.content + offset, num_bytes_per_id);
-          offset += num_bytes_per_id;
-          AVIFINFO_CHECK(meta_bytes != NULL && meta_bytes < bytes, kInvalid);
-          AVIFINFO_CHECK(meta_max_num_bytes > 0, kInvalid);
-          // Go up one level: from "dimg" among "iref" to boxes among "meta".
-          AVIFINFO_CHECK_NOT_FOUND(ParseMetaForFeatures(
-              meta_bytes, meta_num_bytes, meta_max_num_bytes, to_item_id,
-              num_parsed_boxes, features));
-          // Trying the first tile should be enough. Check others just in case.
-          AVIFINFO_CHECK(i <= 32, kAborted);  // Be reasonable.
-        }
-      }
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kNotFound);  // No "dimg" in "iref" is not an issue.
-}
-
-static AvifInfoInternalStatus ParseMetaForFeaturesInTiles(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t max_num_bytes,
-    uint32_t primary_item_id, uint32_t* num_parsed_boxes,
-    AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "iref", 4)) {
-      return ParseIrefForFeaturesInTiles(
-          box.content, num_bytes - box.content_position, box.content_size,
-          bytes, num_bytes, max_num_bytes, primary_item_id, num_parsed_boxes,
-          features);
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kNotFound);  // No "iref" in "meta" is not an issue.
-}
-
-static AvifInfoInternalStatus ParseFileForFeaturesInTiles(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t file_size,
-    uint32_t primary_item_id, uint32_t* num_parsed_boxes,
-    AvifInfoFeatures* features) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, file_size, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "meta", 4)) {
-      return ParseMetaForFeaturesInTiles(
-          box.content, num_bytes - box.content_position, box.content_size,
-          primary_item_id, num_parsed_boxes, features);
-    }
-    position += box.size;
-  } while (position != file_size);
-  AVIFINFO_RETURN(kInvalid);  // No "meta" is an issue.
-}
-
-//------------------------------------------------------------------------------
-// Search if there is an alpha layer through "meta > iprp > ipco > auxC" boxes.
-
-static AvifInfoInternalStatus ParseIpcoForAlpha(const uint8_t* bytes,
-                                                uint32_t num_bytes,
-                                                uint32_t max_num_bytes,
-                                                uint32_t* num_parsed_boxes) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "auxC", 4)) {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size - 3));
+    } else if (!memcmp(box.type, "auxC", 4)) {
       // See AV1 Image File Format (AVIF) 4
       // 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) {
-        AVIFINFO_CHECK(box.content_position + kAlphaStrLength <= num_bytes,
-                       kTruncated);
-        const char* const aux_type = (const char*)box.content;
+        const uint8_t* data;
+        AVIFINFO_CHECK_FOUND(
+            AvifInfoInternalRead(stream, kAlphaStrLength, &data));
+        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.
-          return kFound;
+          features->has_alpha = 1;
         }
+        AVIFINFO_CHECK_FOUND(
+            AvifInfoInternalSkip(stream, box.content_size - kAlphaStrLength));
+      } else {
+        AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
       }
+    } else {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kNotFound);  // No "auxC" in "ipco" is not an issue.
+    ++box_index;
+    num_remaining_bytes -= box.size;
+  } while (num_remaining_bytes > 0);
+  AVIFINFO_RETURN(kNotFound);
 }
 
-static AvifInfoInternalStatus ParseIprpForAlpha(const uint8_t* bytes,
-                                                uint32_t num_bytes,
-                                                uint32_t max_num_bytes,
-                                                uint32_t* num_parsed_boxes) {
-  uint32_t position = 0;
+// 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,
+                                        uint32_t num_remaining_bytes,
+                                        uint32_t* num_parsed_boxes,
+                                        AvifInfoInternalFeatures* features) {
   do {
     AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
+    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes,
+                                                  num_parsed_boxes, &box));
 
     if (!memcmp(box.type, "ipco", 4)) {
-      return ParseIpcoForAlpha(box.content, num_bytes - box.content_position,
-                               box.content_size, num_parsed_boxes);
+      AVIFINFO_CHECK_NOT_FOUND(
+          ParseIpco(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;
+      const uint8_t* data;
+      AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid);
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
+      const uint32_t entry_count = AvifInfoInternalReadBigEndian(data, 4);
+      const uint32_t id_num_bytes = (box.version < 1) ? 2 : 4;
+      const uint32_t index_num_bytes = (box.flags & 1) ? 2 : 1;
+      const uint32_t essential_bit_mask = (box.flags & 1) ? 0x8000 : 0x80;
+
+      for (uint32_t entry = 0; entry < entry_count; ++entry) {
+        if (entry >= AVIFINFO_MAX_PROPS ||
+            features->num_props >= AVIFINFO_MAX_PROPS) {
+          features->data_was_skipped = 1;
+          break;
+        }
+        num_read_bytes += id_num_bytes + 1;
+        AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid);
+        AVIFINFO_CHECK_FOUND(
+            AvifInfoInternalRead(stream, id_num_bytes + 1, &data));
+        const uint32_t item_id =
+            AvifInfoInternalReadBigEndian(data, id_num_bytes);
+        const uint32_t association_count =
+            AvifInfoInternalReadBigEndian(data + id_num_bytes, 1);
+
+        uint32_t property;
+        for (property = 0; property < association_count; ++property) {
+          if (property >= AVIFINFO_MAX_PROPS ||
+              features->num_props >= AVIFINFO_MAX_PROPS) {
+            features->data_was_skipped = 1;
+            break;
+          }
+          num_read_bytes += index_num_bytes;
+          AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid);
+          AVIFINFO_CHECK_FOUND(
+              AvifInfoInternalRead(stream, index_num_bytes, &data));
+          const uint32_t value =
+              AvifInfoInternalReadBigEndian(data, index_num_bytes);
+          // const int essential = (value & essential_bit_mask);  // Unused.
+          const uint32_t property_index = (value & ~essential_bit_mask);
+          if (property_index <= AVIFINFO_MAX_VALUE &&
+              item_id <= AVIFINFO_MAX_VALUE) {
+            features->props[features->num_props].property_index =
+                property_index;
+            features->props[features->num_props].item_id = item_id;
+            ++features->num_props;
+          } else {
+            features->data_was_skipped = 1;
+          }
+        }
+        if (property < association_count) break;  // Do not read garbage.
+      }
+
+      // If all features are available now, do not look further.
+      AVIFINFO_CHECK_NOT_FOUND(
+          AvifInfoInternalGetPrimaryItemFeatures(features));
+
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalSkip(stream, box.content_size - num_read_bytes));
+    } else {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
     }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kInvalid);  // No "ipco" in "iprp" is an issue.
-}
-
-static AvifInfoInternalStatus ParseMetaForAlpha(const uint8_t* bytes,
-                                                uint32_t num_bytes,
-                                                uint32_t max_num_bytes,
-                                                uint32_t* num_parsed_boxes) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, max_num_bytes, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "iprp", 4)) {
-      return ParseIprpForAlpha(box.content, num_bytes - box.content_position,
-                               box.content_size, num_parsed_boxes);
-    }
-    position += box.size;
-  } while (position != max_num_bytes);
-  AVIFINFO_RETURN(kInvalid);  // No "iprp" in "meta" is an issue.
-}
-
-static AvifInfoInternalStatus ParseFileForAlpha(const uint8_t* bytes,
-                                                uint32_t num_bytes,
-                                                uint32_t file_size,
-                                                uint32_t* num_parsed_boxes) {
-  uint32_t position = 0;
-  do {
-    AvifInfoInternalBox box;
-    AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(
-        bytes, num_bytes, file_size, position, num_parsed_boxes, &box));
-
-    if (!memcmp(box.type, "meta", 4)) {
-      return ParseMetaForAlpha(box.content, num_bytes - box.content_position,
-                               box.content_size, num_parsed_boxes);
-    }
-    position += box.size;
-  } while (position != file_size);
-  AVIFINFO_RETURN(kInvalid);  // No "meta" is an issue.
+    num_remaining_bytes -= box.size;
+  } while (num_remaining_bytes != 0);
+  AVIFINFO_RETURN(kNotFound);
 }
 
 //------------------------------------------------------------------------------
-// Parsing starting point.
 
-static AvifInfoInternalStatus AvifInfoInternalParseFile(
-    const uint8_t* bytes, uint32_t num_bytes, uint32_t file_size,
-    AvifInfoFeatures* features) {
-  uint32_t num_parsed_boxes = 0;
-  AVIFINFO_CHECK_FOUND(
-      ParseFileForBrand(bytes, num_bytes, file_size, &num_parsed_boxes));
+// Parses a 'stream' of an "iref" box into 'features'.
+// 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,
+                                        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));
 
-  // 'bytes' is an AVIF file. Next step is finding the ID of the primary item.
-  uint32_t primary_item_id;
-  AVIFINFO_CHECK_FOUND(ParseFileForPrimaryItemId(
-      bytes, num_bytes, file_size, &num_parsed_boxes, &primary_item_id));
+    if (!memcmp(box.type, "dimg", 4)) {
+      // See ISO/IEC 14496-12:2015(E) 8.11.12.2
+      const uint32_t num_bytes_per_id = (box.version == 0) ? 2 : 4;
+      uint32_t num_read_bytes = num_bytes_per_id + 2;
+      const uint8_t* data;
+      AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid);
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalRead(stream, num_bytes_per_id + 2, &data));
+      const uint32_t from_item_id =
+          AvifInfoInternalReadBigEndian(data, num_bytes_per_id);
+      const uint32_t reference_count =
+          AvifInfoInternalReadBigEndian(data + num_bytes_per_id, 2);
 
-  // Now find the 'features' of the primary item.
-  AvifInfoInternalStatus status =
-      ParseFileForFeatures(bytes, num_bytes, file_size, primary_item_id,
-                           &num_parsed_boxes, features);
-  if (status == kNotFound) {
-    // It is possible that some of the 'features' are missing for the primary
-    // item. Try to look into tiles in case they are defined there.
-    status = ParseFileForFeaturesInTiles(bytes, num_bytes, file_size,
-                                         primary_item_id, &num_parsed_boxes,
-                                         features);
-  }
-  AVIFINFO_CHECK_FOUND(status);
+      for (uint32_t i = 0; i < reference_count; ++i) {
+        if (i >= AVIFINFO_MAX_TILES) {
+          features->data_was_skipped = 1;
+          break;
+        }
+        num_read_bytes += num_bytes_per_id;
+        AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid);
+        AVIFINFO_CHECK_FOUND(
+            AvifInfoInternalRead(stream, num_bytes_per_id, &data));
+        const uint32_t to_item_id =
+            AvifInfoInternalReadBigEndian(data, num_bytes_per_id);
+        if (from_item_id <= AVIFINFO_MAX_VALUE &&
+            to_item_id <= AVIFINFO_MAX_VALUE &&
+            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->num_tiles;
+        } else {
+          features->data_was_skipped = 1;
+        }
+      }
 
-  // If there is an alpha plane, add 1 to the number of channels.
-  status = ParseFileForAlpha(bytes, num_bytes, file_size, &num_parsed_boxes);
-  if (status == kFound) {
-    ++features->num_channels;
-  } else {
-    AVIFINFO_CHECK_NOT_FOUND(status);
-  }
-  return kFound;
+      // If all features are available now, do not look further.
+      AVIFINFO_CHECK_NOT_FOUND(
+          AvifInfoInternalGetPrimaryItemFeatures(features));
+    } else {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
+    }
+    num_remaining_bytes -= box.size;
+  } while (num_remaining_bytes > 0);
+  AVIFINFO_RETURN(kNotFound);
 }
 
 //------------------------------------------------------------------------------
-// Public API
+
+// 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,
+                                        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));
+
+    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 uint8_t* data;
+      AVIFINFO_CHECK(num_bytes_per_id <= num_remaining_bytes, kInvalid);
+      AVIFINFO_CHECK_FOUND(
+          AvifInfoInternalRead(stream, num_bytes_per_id, &data));
+      const uint32_t primary_item_id =
+          AvifInfoInternalReadBigEndian(data, num_bytes_per_id);
+      AVIFINFO_CHECK(primary_item_id <= AVIFINFO_MAX_VALUE, kAborted);
+      features->has_primary_item = 1;
+      features->primary_item_id = primary_item_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));
+    } else if (!memcmp(box.type, "iref", 4)) {
+      AVIFINFO_CHECK_NOT_FOUND(
+          ParseIref(stream, box.content_size, num_parsed_boxes, features));
+    } else {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
+    }
+    num_remaining_bytes -= box.size;
+  } while (num_remaining_bytes != 0);
+  // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta".
+  AVIFINFO_RETURN(features->data_was_skipped ? kAborted : kInvalid);
+}
+
+//------------------------------------------------------------------------------
+
+// Parses a file 'stream'. The file type is checked through the "ftyp" box and
+// 'features' are extracted through the "meta" box.
+static AvifInfoInternalStatus ParseFile(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));
+    // The first box must be "ftyp" and no other box at root can be "ftyp".
+    AVIFINFO_CHECK((*num_parsed_boxes == 1) == !memcmp(box.type, "ftyp", 4),
+                   kInvalid);
+
+    if (!memcmp(box.type, "ftyp", 4)) {
+      // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
+      AVIFINFO_CHECK(box.content_size >= 8, kInvalid);  // major_brand + version
+      uint8_t is_avif = 0;
+      for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) {
+        const uint8_t* data;
+        AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
+        if (i == 4) continue;  // Skip minor_version.
+        if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) {
+          is_avif = 1;
+          AVIFINFO_CHECK_FOUND(
+              AvifInfoInternalSkip(stream, box.content_size - (i + 4)));
+          break;
+        }
+        AVIFINFO_CHECK(i <= 32 * 4, kAborted);  // Be reasonable.
+      }
+      AVIFINFO_CHECK(is_avif, kInvalid);
+    } else if (!memcmp(box.type, "meta", 4)) {
+      return ParseMeta(stream, box.content_size, num_parsed_boxes, features);
+    } else {
+      AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
+    }
+    num_remaining_bytes -= box.size;
+  } while (num_remaining_bytes != 0);
+  AVIFINFO_RETURN(kInvalid);  // No "meta" no good.
+}
+
+//------------------------------------------------------------------------------
+// Helpers for converting the fixed-size input public API to the streamed one.
+
+typedef struct {
+  const uint8_t* data;
+  size_t data_size;
+} AvifInfoInternalForward;
+
+static const uint8_t* AvifInfoInternalForwardRead(void* stream,
+                                                  size_t num_bytes) {
+  AvifInfoInternalForward* forward = (AvifInfoInternalForward*)stream;
+  if (num_bytes > forward->data_size) return NULL;
+  const uint8_t* data = forward->data;
+  forward->data += num_bytes;
+  forward->data_size -= num_bytes;
+  return data;
+}
+
+static void AvifInfoInternalForwardSkip(void* stream, size_t num_bytes) {
+  AvifInfoInternalForward* forward = (AvifInfoInternalForward*)stream;
+  if (num_bytes > forward->data_size) num_bytes = forward->data_size;
+  forward->data += num_bytes;
+  forward->data_size -= num_bytes;
+}
+
+//------------------------------------------------------------------------------
+// Fixed-size input public API
 
 AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size,
                            AvifInfoFeatures* features) {
   // Consider the file to be of maximum size.
   return AvifInfoGetWithSize(data, data_size, features,
-                             /*file_size=*/kAvifInfoInternalMaxSize);
+                             /*file_size=*/AVIFINFO_MAX_SIZE);
 }
 
 AvifInfoStatus AvifInfoGetWithSize(const uint8_t* data, size_t data_size,
                                    AvifInfoFeatures* features,
                                    size_t file_size) {
-  if (features != NULL) memset(features, 0, sizeof(*features));
-  if (data == NULL) return kAvifInfoNotEnoughData;
-  if (data_size > file_size) data_size = file_size;
+  AvifInfoInternalForward stream;
+  stream.data = data;
+  stream.data_size = data_size;
+  // Forward null 'data' as a null 'stream' to handle it the same way.
+  return AvifInfoReadWithSize(
+      (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead,
+      AvifInfoInternalForwardSkip, features, file_size);
+}
 
-  AvifInfoFeatures parsed_features;
-  memset(&parsed_features, 0, sizeof(parsed_features));
-  const AvifInfoInternalStatus status = AvifInfoInternalParseFile(
-      data,
-      (data_size >= kAvifInfoInternalMaxSize) ? kAvifInfoInternalMaxSize
-                                              : (uint32_t)data_size,
-      (file_size >= kAvifInfoInternalMaxSize) ? kAvifInfoInternalMaxSize
-                                              : (uint32_t)file_size,
-      &parsed_features);
+//------------------------------------------------------------------------------
+// Streamed input API
+
+AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
+                            skip_stream_t skip, AvifInfoFeatures* features) {
+  // Consider the file to be of maximum size.
+  return AvifInfoReadWithSize(stream, read, skip, features,
+                              /*file_size=*/AVIFINFO_MAX_SIZE);
+}
+
+AvifInfoStatus AvifInfoReadWithSize(void* stream, read_stream_t read,
+                                    skip_stream_t skip,
+                                    AvifInfoFeatures* features,
+                                    size_t file_size) {
+  if (features != NULL) memset(features, 0, sizeof(*features));
+  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;
+  const uint32_t size = (file_size >= AVIFINFO_MAX_SIZE) ? AVIFINFO_MAX_SIZE
+                                                         : (uint32_t)file_size;
+  uint32_t num_parsed_boxes = 0;
+  AvifInfoInternalFeatures internal_features;
+  memset(&internal_features, AVIFINFO_UNDEFINED, sizeof(internal_features));
+
+  // Go through all relevant boxes sequentially.
+  const AvifInfoInternalStatus status =
+      ParseFile(&internal_stream, size, &num_parsed_boxes, &internal_features);
 
   if (status == kNotFound) {
-    return (data_size < file_size) ? kAvifInfoNotEnoughData
-                                   : kAvifInfoInvalidFile;
+    return (internal_stream.num_read_bytes < file_size) ? kAvifInfoNotEnoughData
+                                                        : kAvifInfoInvalidFile;
   }
   if (status == kTruncated) return kAvifInfoNotEnoughData;
   if (status == kInvalid) return kAvifInfoInvalidFile;
   if (status == kAborted) return kAvifInfoTooComplex;
   if (features != NULL) {
-    memcpy(features, &parsed_features, sizeof(*features));
+    memcpy(features, &internal_features.primary_item_features,
+           sizeof(*features));
   }
   return kAvifInfoOk;
 }
-
-//------------------------------------------------------------------------------
-
-#undef AVIFINFO_RETURN
-#undef AVIFINFO_CHECK
-#undef AVIFINFO_CHECK_STATUS_IS
-#undef AVIFINFO_CHECK_FOUND
-#undef AVIFINFO_CHECK_NOT_FOUND
diff --git a/avifinfo.h b/avifinfo.h
index ea4d9d7..414f52e 100644
--- a/avifinfo.h
+++ b/avifinfo.h
@@ -41,6 +41,10 @@
                            //   (1 monochrome or 3 colors) + (0 or 1 alpha)
 } AvifInfoFeatures;
 
+//------------------------------------------------------------------------------
+// Fixed-size input API
+// Use this API if a raw byte array of fixed size is available as input.
+
 // Parses the AVIF 'data' and extracts its 'features'.
 // 'data' can be partial but must point to the beginning of the AVIF file.
 // The 'features' can be parsed in the first 450 bytes of most AVIF files.
@@ -55,11 +59,38 @@
                                    size_t file_size);
 
 //------------------------------------------------------------------------------
+// Streamed input API
+// Use this API if the input bytes must be fetched and/or if the AVIF payload
+// size is unknown. Implement the two function signatures below and pass them to
+// AvifInfoRead*() with a 'stream', which can be anything (file, struct etc.).
+
+// Reads 'num_bytes' from the 'stream'.
+// The position in the 'stream' must be advanced by 'num_bytes'.
+// Returns a pointer to the 'num_bytes' or null if it cannot be fulfilled.
+// The returned data must remain valid until the next read.
+typedef const uint8_t* (*read_stream_t)(void* stream, size_t num_bytes);
+// Advances the position in the 'stream' by 'num_bytes'.
+typedef void (*skip_stream_t)(void* stream, size_t num_bytes);
+
+// Maximum number of bytes requested per read. There is no limit per skip.
+#define AVIFINFO_MAX_NUM_READ_BYTES 64
+
+// Same as AvifInfoGet*() but takes a 'stream' as input. AvifInfoRead*() does
+// not access the 'stream' directly but passes it as is to 'read' and 'skip'.
+// 'read' cannot be null. If 'skip' is null, 'read' is called instead.
+AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
+                            skip_stream_t skip, AvifInfoFeatures* features);
+AvifInfoStatus AvifInfoReadWithSize(void* stream, read_stream_t read,
+                                    skip_stream_t skip,
+                                    AvifInfoFeatures* features,
+                                    size_t file_size);
+
+//------------------------------------------------------------------------------
 
 // If needed, avifinfo.h and avifinfo.c can be merged into a single file:
 //   1. Replace this block comment by the content of avifinfo.c
 //   2. Discard #include "./avifinfo.h" and move other includes to the top
-//   3. Mark AvifInfoGet*() declarations and definitions as static
+//   3. Mark AvifInfo*() declarations and definitions as static
 // This procedure can be useful when only one translation unit uses avifinfo,
 // whether it includes the merged .h or the merged code is inserted into a file.
 
diff --git a/tests/avifinfo_fuzz.cc b/tests/avifinfo_fuzz.cc
index 6e9cf27..bc9cb0b 100644
--- a/tests/avifinfo_fuzz.cc
+++ b/tests/avifinfo_fuzz.cc
@@ -13,6 +13,38 @@
 
 #include "avifinfo.h"
 
+//------------------------------------------------------------------------------
+// Stream definition.
+
+typedef struct {
+  const uint8_t* data;
+  size_t data_size;
+} StreamData;
+
+static const uint8_t* StreamRead(void* stream, size_t num_bytes) {
+  if (stream == NULL) abort();
+  if (num_bytes < 1 || num_bytes > AVIFINFO_MAX_NUM_READ_BYTES) abort();
+
+  StreamData* stream_data = (StreamData*)stream;
+  if (num_bytes > stream_data->data_size) return NULL;
+  const uint8_t* data = stream_data->data;
+  stream_data->data += num_bytes;
+  stream_data->data_size -= num_bytes;
+  return data;
+}
+
+static void StreamSkip(void* stream, size_t num_bytes) {
+  if (stream == NULL) abort();
+  if (num_bytes < 1) abort();
+
+  StreamData* stream_data = (StreamData*)stream;
+  if (num_bytes > stream_data->data_size) num_bytes = stream_data->data_size;
+  stream_data->data += num_bytes;
+  stream_data->data_size -= num_bytes;
+}
+
+//------------------------------------------------------------------------------
+
 // Test a random bitstream of random size, whether it is valid or not.
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t data_size) {
   AvifInfoStatus previous_status = kAvifInfoNotEnoughData;
@@ -22,9 +54,10 @@
   // for a given size and a status that is not kAvifInfoNotEnoughData, any
   // bigger size (of the same data) should return the same status and features.
   for (size_t size = 0; size < data_size; ++size) {
+    StreamData stream = {data, size};
     AvifInfoFeatures features;
-    const AvifInfoStatus status =
-        AvifInfoGetWithSize(data, size, &features, data_size);
+    const AvifInfoStatus status = AvifInfoReadWithSize(
+        &stream, StreamRead, StreamSkip, &features, data_size);
 
     if (previous_status != kAvifInfoNotEnoughData &&
         status != previous_status) {
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc
index fe59b14..381a8e8 100644
--- a/tests/avifinfo_test.cc
+++ b/tests/avifinfo_test.cc
@@ -192,6 +192,17 @@
             kAvifInfoTooComplex);
 }
 
+TEST(AvifInfoReadTest, Null) {
+  AvifInfoFeatures features;
+  EXPECT_EQ(AvifInfoRead(/*stream=*/nullptr, /*read=*/nullptr, /*skip=*/nullptr,
+                         &features),
+            kAvifInfoNotEnoughData);
+  EXPECT_EQ(features.width, 0u);
+  EXPECT_EQ(features.height, 0u);
+  EXPECT_EQ(features.bit_depth, 0u);
+  EXPECT_EQ(features.num_channels, 0u);
+}
+
 //------------------------------------------------------------------------------
 
 }  // namespace