Generalized ipco property deduplication
diff --git a/src/write.c b/src/write.c index 6a2474a..eff6d2c 100644 --- a/src/write.c +++ b/src/write.c
@@ -181,6 +181,79 @@ } // --------------------------------------------------------------------------- +// avifItemPropertyDedup - Provides ipco deduplication + +typedef struct avifItemProperty +{ + uint8_t index; + size_t offset; + size_t size; +} avifItemProperty; +AVIF_ARRAY_DECLARE(avifItemPropertyArray, avifItemProperty, property); + +typedef struct avifItemPropertyDedup +{ + avifItemPropertyArray properties; + avifRWStream s; // Temporary stream for each new property, checked against already-written boxes for deduplications + avifRWData buffer; // Temporary storage for 's' + uint8_t nextIndex; // 1-indexed, incremented every time another unique property is finished +} avifItemPropertyDedup; + +static avifItemPropertyDedup * avifItemPropertyDedupCreate(void) +{ + avifItemPropertyDedup * dedup = (avifItemPropertyDedup *)avifAlloc(sizeof(avifItemPropertyDedup)); + memset(dedup, 0, sizeof(avifItemPropertyDedup)); + avifArrayCreate(&dedup->properties, sizeof(avifItemProperty), 8); + avifRWDataRealloc(&dedup->buffer, 2048); // This will resize automatically (if necessary) + return dedup; +} + +static void avifItemPropertyDedupDestroy(avifItemPropertyDedup * dedup) +{ + avifArrayDestroy(&dedup->properties); + avifRWDataFree(&dedup->buffer); + avifFree(dedup); +} + +// Resets the dedup's temporary write stream in preparation for a single item property's worth of writing +static void avifItemPropertyDedupStart(avifItemPropertyDedup * dedup) +{ + avifRWStreamStart(&dedup->s, &dedup->buffer); +} + +// This compares the newly written item property (in the dedup's temporary storage buffer) to +// already-written properties (whose offsets/sizes in outputStream are recorded in the dedup). If a +// match is found, the previous item's index is used. If this new property is unique, it is +// assigned the next available property index, written to the output stream, and its offset/size in +// the output stream is recorded in the dedup for future comparisons. +// +// This function always returns a valid 1-indexed property index for usage in a property association +// (ipma) box later. If the most recent property was a duplicate of a previous property, the return +// value will be the index of the original property, otherwise it will be the index of the newly +// created property. +static uint8_t avifItemPropertyDedupFinish(avifItemPropertyDedup * dedup, avifRWStream * outputStream) +{ + const size_t newPropertySize = avifRWStreamOffset(&dedup->s); + + for (size_t i = 0; i < dedup->properties.count; ++i) { + avifItemProperty * property = &dedup->properties.property[i]; + if ((property->size == newPropertySize) && + !memcmp(&outputStream->raw->data[property->offset], dedup->buffer.data, newPropertySize)) { + // We've already written this exact property, reuse it + return property->index; + } + } + + // Write a new property, and remember its location in the output stream for future deduplication + avifItemProperty * property = (avifItemProperty *)avifArrayPushPtr(&dedup->properties); + property->index = ++dedup->nextIndex; // preincrement so the first new index is 1 (as ipma is 1-indexed) + property->size = newPropertySize; + property->offset = avifRWStreamOffset(outputStream); + avifRWStreamWrite(outputStream, dedup->buffer.data, newPropertySize); + return property->index; +} + +// --------------------------------------------------------------------------- avifEncoder * avifEncoderCreate(void) { @@ -213,20 +286,48 @@ avifCodecSpecificOptionsSet(encoder->csOptions, key, value); } -static void avifEncoderWriteColorProperties(avifRWStream * s, const avifImage * imageMetadata, struct ipmaArray * ipma, uint8_t * itemPropertyIndex) +// This function is used in two codepaths: +// * writing color *item* properties +// * writing color *track* properties +// +// Item properties must have property associations with them and can be deduplicated (by reusing +// these associations), so this function leverages the ipma and dedup arguments to do this. +// +// Track properties, however, are implicitly associated by the track in which they are contained, so +// there is no need to build a property association box (ipma), and no way to deduplicate/reuse a +// property. In this case, the ipma and dedup properties should/will be set to NULL, and this +// function will avoid using them. +static void avifEncoderWriteColorProperties(avifRWStream * outputStream, + const avifImage * imageMetadata, + struct ipmaArray * ipma, + avifItemPropertyDedup * dedup) { + avifRWStream * s = outputStream; + if (dedup) { + assert(ipma); + + // Use the dedup's temporary stream for box writes + s = &dedup->s; + } + if (imageMetadata->icc.size > 0) { + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker colr = avifRWStreamWriteBox(s, "colr", AVIF_BOX_SIZE_TBD); avifRWStreamWriteChars(s, "prof", 4); // unsigned int(32) colour_type; avifRWStreamWrite(s, imageMetadata->icc.data, imageMetadata->icc.size); avifRWStreamFinishBox(s, colr); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_FALSE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_FALSE); } } // HEIF 6.5.5.1, from Amendment 3 allows multiple colr boxes: "at most one for a given value of colour type" // Therefore, *always* writing an nclx box, even if an a prof box was already written above. + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker colr = avifRWStreamWriteBox(s, "colr", AVIF_BOX_SIZE_TBD); avifRWStreamWriteChars(s, "nclx", 4); // unsigned int(32) colour_type; avifRWStreamWriteU16(s, imageMetadata->colorPrimaries); // unsigned int(16) colour_primaries; @@ -235,21 +336,27 @@ avifRWStreamWriteU8(s, (imageMetadata->yuvRange == AVIF_RANGE_FULL) ? 0x80 : 0); // unsigned int(1) full_range_flag; // unsigned int(7) reserved = 0; avifRWStreamFinishBox(s, colr); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_FALSE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_FALSE); } // Write (Optional) Transformations if (imageMetadata->transformFlags & AVIF_TRANSFORM_PASP) { + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker pasp = avifRWStreamWriteBox(s, "pasp", AVIF_BOX_SIZE_TBD); avifRWStreamWriteU32(s, imageMetadata->pasp.hSpacing); // unsigned int(32) hSpacing; avifRWStreamWriteU32(s, imageMetadata->pasp.vSpacing); // unsigned int(32) vSpacing; avifRWStreamFinishBox(s, pasp); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_FALSE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_FALSE); } } if (imageMetadata->transformFlags & AVIF_TRANSFORM_CLAP) { + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker clap = avifRWStreamWriteBox(s, "clap", AVIF_BOX_SIZE_TBD); avifRWStreamWriteU32(s, imageMetadata->clap.widthN); // unsigned int(32) cleanApertureWidthN; avifRWStreamWriteU32(s, imageMetadata->clap.widthD); // unsigned int(32) cleanApertureWidthD; @@ -260,26 +367,32 @@ avifRWStreamWriteU32(s, imageMetadata->clap.vertOffN); // unsigned int(32) vertOffN; avifRWStreamWriteU32(s, imageMetadata->clap.vertOffD); // unsigned int(32) vertOffD; avifRWStreamFinishBox(s, clap); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_TRUE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_TRUE); } } if (imageMetadata->transformFlags & AVIF_TRANSFORM_IROT) { + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker irot = avifRWStreamWriteBox(s, "irot", AVIF_BOX_SIZE_TBD); uint8_t angle = imageMetadata->irot.angle & 0x3; avifRWStreamWrite(s, &angle, 1); // unsigned int (6) reserved = 0; unsigned int (2) angle; avifRWStreamFinishBox(s, irot); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_TRUE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_TRUE); } } if (imageMetadata->transformFlags & AVIF_TRANSFORM_IMIR) { + if (dedup) { + avifItemPropertyDedupStart(dedup); + } avifBoxMarker imir = avifRWStreamWriteBox(s, "imir", AVIF_BOX_SIZE_TBD); uint8_t mode = imageMetadata->imir.mode & 0x1; avifRWStreamWrite(s, &mode, 1); // unsigned int (7) reserved = 0; unsigned int (1) mode; avifRWStreamFinishBox(s, imir); - if (ipma && itemPropertyIndex) { - ipmaPush(ipma, ++(*itemPropertyIndex), AVIF_TRUE); + if (dedup) { + ipmaPush(ipma, avifItemPropertyDedupFinish(dedup, outputStream), AVIF_TRUE); } } } @@ -900,7 +1013,7 @@ avifBoxMarker iprp = avifRWStreamWriteBox(&s, "iprp", AVIF_BOX_SIZE_TBD); - uint8_t itemPropertyIndex = 0; + avifItemPropertyDedup * dedup = avifItemPropertyDedupCreate(); avifBoxMarker ipco = avifRWStreamWriteBox(&s, "ipco", AVIF_BOX_SIZE_TBD); for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { avifEncoderItem * item = &encoder->data->items.item[itemIndex]; @@ -940,40 +1053,46 @@ // Properties all av01 items need - avifBoxMarker ispe = avifRWStreamWriteFullBox(&s, "ispe", AVIF_BOX_SIZE_TBD, 0, 0); - avifRWStreamWriteU32(&s, imageWidth); // unsigned int(32) image_width; - avifRWStreamWriteU32(&s, imageHeight); // unsigned int(32) image_height; - avifRWStreamFinishBox(&s, ispe); - ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE); // ipma is 1-indexed, doing this afterwards is correct + avifItemPropertyDedupStart(dedup); + avifBoxMarker ispe = avifRWStreamWriteFullBox(&dedup->s, "ispe", AVIF_BOX_SIZE_TBD, 0, 0); + avifRWStreamWriteU32(&dedup->s, imageWidth); // unsigned int(32) image_width; + avifRWStreamWriteU32(&dedup->s, imageHeight); // unsigned int(32) image_height; + avifRWStreamFinishBox(&dedup->s, ispe); + ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_FALSE); + avifItemPropertyDedupStart(dedup); uint8_t channelCount = (item->alpha || (imageMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) ? 1 : 3; - avifBoxMarker pixi = avifRWStreamWriteFullBox(&s, "pixi", AVIF_BOX_SIZE_TBD, 0, 0); - avifRWStreamWriteU8(&s, channelCount); // unsigned int (8) num_channels; + avifBoxMarker pixi = avifRWStreamWriteFullBox(&dedup->s, "pixi", AVIF_BOX_SIZE_TBD, 0, 0); + avifRWStreamWriteU8(&dedup->s, channelCount); // unsigned int (8) num_channels; for (uint8_t chan = 0; chan < channelCount; ++chan) { - avifRWStreamWriteU8(&s, (uint8_t)imageMetadata->depth); // unsigned int (8) bits_per_channel; + avifRWStreamWriteU8(&dedup->s, (uint8_t)imageMetadata->depth); // unsigned int (8) bits_per_channel; } - avifRWStreamFinishBox(&s, pixi); - ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE); + avifRWStreamFinishBox(&dedup->s, pixi); + ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_FALSE); if (item->codec) { - writeConfigBox(&s, &item->av1C); - ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_TRUE); + avifItemPropertyDedupStart(dedup); + writeConfigBox(&dedup->s, &item->av1C); + ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_TRUE); } if (item->alpha) { // Alpha specific properties - avifBoxMarker auxC = avifRWStreamWriteFullBox(&s, "auxC", AVIF_BOX_SIZE_TBD, 0, 0); - avifRWStreamWriteChars(&s, alphaURN, alphaURNSize); // string aux_type; - avifRWStreamFinishBox(&s, auxC); - ipmaPush(&item->ipma, ++itemPropertyIndex, AVIF_FALSE); + avifItemPropertyDedupStart(dedup); + avifBoxMarker auxC = avifRWStreamWriteFullBox(&dedup->s, "auxC", AVIF_BOX_SIZE_TBD, 0, 0); + avifRWStreamWriteChars(&dedup->s, alphaURN, alphaURNSize); // string aux_type; + avifRWStreamFinishBox(&dedup->s, auxC); + ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_FALSE); } else { // Color specific properties - avifEncoderWriteColorProperties(&s, imageMetadata, &item->ipma, &itemPropertyIndex); + avifEncoderWriteColorProperties(&s, imageMetadata, &item->ipma, dedup); } } avifRWStreamFinishBox(&s, ipco); + avifItemPropertyDedupDestroy(dedup); + dedup = NULL; avifBoxMarker ipma = avifRWStreamWriteFullBox(&s, "ipma", AVIF_BOX_SIZE_TBD, 0, 0); {