Expose avifDecoderItemExtent as avifExtent, and add avifDecoderNthImageMaxExtent() streaming helper function
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 930a34a..806fd29 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -747,6 +747,34 @@
avifResult avifDecoderNthImageTiming(const avifDecoder * decoder, uint32_t frameIndex, avifImageTiming * outTiming);
// ---------------------------------------------------------------------------
+// avifExtent
+
+typedef struct avifExtent
+{
+ uint64_t offset;
+ size_t size;
+} avifExtent;
+
+// Streaming data helper - Use this to calculate the maximal AVIF data extent encompassing all AV1
+// sample data needed to decode the Nth image. The offset will be the earliest offset of all required
+// AV1 extents for this frame, and the size will create a range including the last byte of the last AV1
+// sample needed. Note that this extent may include non-sample data, as a frame's sample data
+// may be broken into multiple extents and interleaved with other data, or in non-sequential order.
+//
+// If includeDependentFrameExtents is true (recommended), this extent will also encompass all AV1
+// samples that this frame's sample depends on to decode, from the nearest keyframe up to this Nth
+// frame.
+//
+// If avifDecoderNthImageMaxExtent() returns AVIF_RESULT_OK and the extent's size is 0 bytes, the
+// data for this frame was read as a part of avifDecoderParse() (typically in an idat box inside of
+// a meta box) and no additional data will need to be read to decode this frame, assuming
+// includeDependentFrameExtents is true or this frameIndex is a keyframe.
+avifResult avifDecoderNthImageMaxExtent(const avifDecoder * decoder,
+ uint32_t frameIndex,
+ avifBool includeDependentFrameExtents,
+ avifExtent * outExtent);
+
+// ---------------------------------------------------------------------------
// avifEncoder
struct avifEncoderData;
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 3aae746..3d0cab5 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -13,6 +13,7 @@
// Yes, clamp macros are nasty. Do not use them.
#define AVIF_CLAMP(x, low, high) (((x) < (low))) ? (low) : (((high) < (x)) ? (high) : (x))
#define AVIF_MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define AVIF_MAX(a, b) (((a) > (b)) ? (a) : (b))
// Used by stream related things.
#define CHECK(A) \
diff --git a/src/read.c b/src/read.c
index 239f6cb..f510429 100644
--- a/src/read.c
+++ b/src/read.c
@@ -120,12 +120,7 @@
return NULL;
}
-typedef struct avifDecoderItemExtent
-{
- uint64_t offset;
- size_t size;
-} avifDecoderItemExtent;
-AVIF_ARRAY_DECLARE(avifDecoderItemExtentArray, avifDecoderItemExtent, extent);
+AVIF_ARRAY_DECLARE(avifExtentArray, avifExtent, extent);
// one "item" worth for decoding (all iref, iloc, iprp, etc refer to one of these)
typedef struct avifDecoderItem
@@ -137,7 +132,7 @@
uint32_t idatID; // If non-zero, offset is relative to this idat box (iloc construction_method==1)
avifContentType contentType;
avifPropertyArray properties;
- avifDecoderItemExtentArray extents; // All extent offsets/sizes
+ avifExtentArray extents; // All extent offsets/sizes
avifRWData mergedExtents; // if set, is a single contiguous block of this item's extents (unused when extents.count == 1)
avifBool ownsMergedExtents; // if true, mergedExtents must be freed when this item is destroyed
avifBool partialMergedExtents; // If true, mergedExtents doesn't have all of the item data yet
@@ -521,7 +516,7 @@
avifDecoderItem * item = (avifDecoderItem *)avifArrayPushPtr(&meta->items);
avifArrayCreate(&item->properties, sizeof(avifProperty), 16);
- avifArrayCreate(&item->extents, sizeof(avifDecoderItemExtent), 1);
+ avifArrayCreate(&item->extents, sizeof(avifExtent), 1);
item->id = itemID;
item->meta = meta;
return item;
@@ -625,6 +620,63 @@
avifFree(data);
}
+// This returns the max extent that has to be read in order to decode this item. If
+// the item is stored in an idat, the data has already been read during Parse() and
+// this function will return AVIF_RESULT_OK with a 0-byte extent.
+static avifResult avifDecoderItemMaxExtent(const avifDecoderItem * item, avifExtent * outExtent)
+{
+ if (item->extents.count == 0) {
+ return AVIF_RESULT_TRUNCATED_DATA;
+ }
+
+ if (item->idatID != 0) {
+ // construction_method: idat(1)
+
+ // Find associated idat block
+ for (uint32_t i = 0; i < item->meta->idats.count; ++i) {
+ if (item->meta->idats.idat[i].id == item->idatID) {
+ // Already read from a meta box during Parse()
+ memset(outExtent, 0, sizeof(avifExtent));
+ return AVIF_RESULT_OK;
+ }
+ }
+
+ // no idat box was found in this meta box, bail out
+ return AVIF_RESULT_NO_CONTENT;
+ }
+
+ // construction_method: file(0)
+
+ avifBool firstExtent = AVIF_TRUE;
+ uint64_t minOffset = 0;
+ uint64_t maxOffset = 0;
+ for (uint32_t extentIter = 0; extentIter < item->extents.count; ++extentIter) {
+ avifExtent * extent = &item->extents.extent[extentIter];
+
+ if (extent->size > UINT64_MAX - extent->offset) {
+ return AVIF_RESULT_BMFF_PARSE_FAILED;
+ }
+ const uint64_t endOffset = extent->offset + extent->size;
+
+ if (firstExtent) {
+ firstExtent = AVIF_FALSE;
+ minOffset = extent->offset;
+ maxOffset = endOffset;
+ } else {
+ if (minOffset > extent->offset) {
+ minOffset = extent->offset;
+ }
+ if (maxOffset < endOffset) {
+ maxOffset = endOffset;
+ }
+ }
+ }
+
+ outExtent->offset = minOffset;
+ outExtent->size = maxOffset - minOffset;
+ return AVIF_RESULT_OK;
+}
+
static avifResult avifDecoderItemRead(avifDecoderItem * item, avifIO * io, avifROData * outData, size_t partialByteCount)
{
if (item->mergedExtents.data && !item->partialMergedExtents) {
@@ -684,7 +736,7 @@
uint8_t * front = item->mergedExtents.data;
size_t remainingBytes = totalBytesToRead;
for (uint32_t extentIter = 0; extentIter < item->extents.count; ++extentIter) {
- avifDecoderItemExtent * extent = &item->extents.extent[extentIter];
+ avifExtent * extent = &item->extents.extent[extentIter];
size_t bytesToRead = extent->size;
if (bytesToRead > remainingBytes) {
@@ -1110,7 +1162,7 @@
uint64_t extentLength; // unsigned int(offset_size*8) extent_length;
CHECK(avifROStreamReadUX8(&s, &extentLength, lengthSize));
- avifDecoderItemExtent * extent = (avifDecoderItemExtent *)avifArrayPushPtr(&item->extents);
+ avifExtent * extent = (avifExtent *)avifArrayPushPtr(&item->extents);
if (extentOffset > UINT64_MAX - baseOffset) {
return AVIF_FALSE;
}
@@ -2234,6 +2286,72 @@
return AVIF_RESULT_OK;
}
+// 0-byte extents are ignored/overwritten during the merge, as they are the signal from helper
+// functions that no extent was necessary for this given sample. If both provided extents are
+// >0 bytes, this will set dst to be an extent that bounds both supplied extents.
+static void avifExtentMerge(avifExtent * dst, const avifExtent * src)
+{
+ if (!dst->size) {
+ memcpy(dst, src, sizeof(avifExtent));
+ return;
+ }
+ if (!src->size) {
+ return;
+ }
+
+ const uint64_t minExtent1 = dst->offset;
+ const uint64_t maxExtent1 = dst->offset + dst->size;
+ const uint64_t minExtent2 = src->offset;
+ const uint64_t maxExtent2 = src->offset + src->size;
+ dst->offset = AVIF_MIN(minExtent1, minExtent2);
+ dst->size = AVIF_MAX(maxExtent1, maxExtent2);
+}
+
+avifResult avifDecoderNthImageMaxExtent(const avifDecoder * decoder, uint32_t frameIndex, avifBool includeDependentFrameExtents, avifExtent * outExtent)
+{
+ if (!decoder->data) {
+ // Nothing has been parsed yet
+ return AVIF_RESULT_NO_CONTENT;
+ }
+
+ memset(outExtent, 0, sizeof(avifExtent));
+
+ uint32_t startFrameIndex = includeDependentFrameExtents ? avifDecoderNearestKeyframe(decoder, frameIndex) : frameIndex;
+ uint32_t endFrameIndex = frameIndex;
+ for (uint32_t currentFrameIndex = startFrameIndex; currentFrameIndex <= endFrameIndex; ++currentFrameIndex) {
+ for (unsigned int tileIndex = 0; tileIndex < decoder->data->tiles.count; ++tileIndex) {
+ avifTile * tile = &decoder->data->tiles.tile[tileIndex];
+ if (currentFrameIndex >= tile->input->samples.count) {
+ return AVIF_RESULT_NO_IMAGES_REMAINING;
+ }
+
+ avifDecodeSample * sample = &tile->input->samples.sample[currentFrameIndex];
+ avifExtent sampleExtent;
+ if (sample->itemID) {
+ // The data comes from an item. Let avifDecoderItemMaxExtent() do the heavy lifting.
+
+ avifDecoderItem * item = avifMetaFindItem(decoder->data->meta, sample->itemID);
+ avifResult maxExtentResult = avifDecoderItemMaxExtent(item, &sampleExtent);
+ if (maxExtentResult != AVIF_RESULT_OK) {
+ return maxExtentResult;
+ }
+ } else {
+ // The data likely comes from a sample table. Use the sample position directly.
+
+ sampleExtent.offset = sample->offset;
+ sampleExtent.size = sample->size;
+ }
+
+ if (sampleExtent.size > UINT64_MAX - sampleExtent.offset) {
+ return AVIF_RESULT_BMFF_PARSE_FAILED;
+ }
+
+ avifExtentMerge(outExtent, &sampleExtent);
+ }
+ }
+ return AVIF_RESULT_OK;
+}
+
static avifResult avifDecoderPrepareSample(avifDecoder * decoder, avifDecodeSample * sample, size_t partialByteCount)
{
if (!sample->data.size || sample->partialData) {
diff --git a/tests/aviftest.c b/tests/aviftest.c
index 770dd6a..c560e38 100644
--- a/tests/aviftest.c
+++ b/tests/aviftest.c
@@ -400,21 +400,37 @@
if (parseResult == AVIF_RESULT_OK) {
for (; io->availableBytes <= io->io.sizeHint; ++io->availableBytes) {
- avifResult nextImageResult = avifDecoderNextImage(decoder);
- if (nextImageResult == AVIF_RESULT_WAITING_ON_IO) {
- continue;
- }
- if (nextImageResult != AVIF_RESULT_OK) {
+ avifExtent extent;
+ avifResult extentResult = avifDecoderNthImageMaxExtent(decoder, 0, AVIF_TRUE, &extent);
+ if (extentResult != AVIF_RESULT_OK) {
retCode = 1;
- }
- printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] nextImage returned: %s\n",
- filename,
- io->availableBytes,
- io->io.sizeHint,
- io->io.persistent ? "Persistent" : "NonPersistent",
- decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
- avifResultToString(nextImageResult));
+ printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] extentResult returned: %s\n",
+ filename,
+ io->availableBytes,
+ io->io.sizeHint,
+ io->io.persistent ? "Persistent" : "NonPersistent",
+ decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
+ avifResultToString(extentResult));
+ } else {
+ avifResult nextImageResult = avifDecoderNextImage(decoder);
+ if (nextImageResult == AVIF_RESULT_WAITING_ON_IO) {
+ continue;
+ }
+ if (nextImageResult != AVIF_RESULT_OK) {
+ retCode = 1;
+ }
+
+ printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] nextImage [MaxExtent off %" PRIu64 ", size %zu] returned: %s\n",
+ filename,
+ io->availableBytes,
+ io->io.sizeHint,
+ io->io.persistent ? "Persistent" : "NonPersistent",
+ decoder->ignoreExif ? "IgnoreMetadata" : "Metadata",
+ extent.offset,
+ extent.size,
+ avifResultToString(nextImageResult));
+ }
break;
}
}