| // Copyright 2019 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/internal.h" |
| |
| #include <assert.h> |
| #include <string.h> |
| #include <time.h> |
| |
| #define MAX_ASSOCIATIONS 16 |
| struct ipmaArray |
| { |
| uint8_t associations[MAX_ASSOCIATIONS]; |
| avifBool essential[MAX_ASSOCIATIONS]; |
| uint8_t count; |
| }; |
| |
| // Used to store offsets in meta boxes which need to point at mdat offsets that |
| // aren't known yet. When an item's mdat payload is written, all registered fixups |
| // will have this now-known offset "fixed up". |
| typedef struct avifOffsetFixup |
| { |
| size_t offset; |
| } avifOffsetFixup; |
| AVIF_ARRAY_DECLARE(avifOffsetFixupArray, avifOffsetFixup, fixup); |
| |
| static const char alphaURN[] = AVIF_URN_ALPHA0; |
| static const size_t alphaURNSize = sizeof(alphaURN); |
| |
| static const char xmpContentType[] = AVIF_CONTENT_TYPE_XMP; |
| static const size_t xmpContentTypeSize = sizeof(xmpContentType); |
| |
| static avifResult writeCodecConfig(avifRWStream * s, const avifCodecConfigurationBox * cfg); |
| static avifResult writeConfigBox(avifRWStream * s, const avifCodecConfigurationBox * cfg, const char * configPropName); |
| |
| // --------------------------------------------------------------------------- |
| // avifSetTileConfiguration |
| |
| static int floorLog2(uint32_t n) |
| { |
| assert(n > 0); |
| int count = 0; |
| while (n != 0) { |
| ++count; |
| n >>= 1; |
| } |
| return count - 1; |
| } |
| |
| // Splits tilesLog2 into *tileDim1Log2 and *tileDim2Log2, considering the ratio of dim1 to dim2. |
| // |
| // Precondition: |
| // dim1 >= dim2 |
| // Postcondition: |
| // tilesLog2 == *tileDim1Log2 + *tileDim2Log2 |
| // *tileDim1Log2 >= *tileDim2Log2 |
| static void splitTilesLog2(uint32_t dim1, uint32_t dim2, int tilesLog2, int * tileDim1Log2, int * tileDim2Log2) |
| { |
| assert(dim1 >= dim2); |
| uint32_t ratio = dim1 / dim2; |
| int diffLog2 = floorLog2(ratio); |
| int subtract = tilesLog2 - diffLog2; |
| if (subtract < 0) { |
| subtract = 0; |
| } |
| *tileDim2Log2 = subtract / 2; |
| *tileDim1Log2 = tilesLog2 - *tileDim2Log2; |
| assert(*tileDim1Log2 >= *tileDim2Log2); |
| } |
| |
| // Set the tile configuration: the number of tiles and the tile size. |
| // |
| // Tiles improve encoding and decoding speeds when multiple threads are available. However, for |
| // image coding, the total tile boundary length affects the compression efficiency because intra |
| // prediction can't go across tile boundaries. So the more tiles there are in an image, the worse |
| // the compression ratio is. For a given number of tiles, making the tile size close to a square |
| // tends to reduce the total tile boundary length inside the image. Use more tiles along the longer |
| // dimension of the image to make the tile size closer to a square. |
| void avifSetTileConfiguration(int threads, uint32_t width, uint32_t height, int * tileRowsLog2, int * tileColsLog2) |
| { |
| *tileRowsLog2 = 0; |
| *tileColsLog2 = 0; |
| if (threads > 1) { |
| // Avoid small tiles because they are particularly bad for image coding. |
| // |
| // Use no more tiles than the number of threads. Aim for one tile per thread. Using more |
| // than one thread inside one tile could be less efficient. Using more tiles than the |
| // number of threads would result in a compression penalty without much benefit. |
| const uint32_t kMinTileArea = 512 * 512; |
| const uint32_t kMaxTiles = 32; |
| uint32_t imageArea = width * height; |
| uint32_t tiles = (imageArea + kMinTileArea - 1) / kMinTileArea; |
| if (tiles > kMaxTiles) { |
| tiles = kMaxTiles; |
| } |
| if (tiles > (uint32_t)threads) { |
| tiles = threads; |
| } |
| int tilesLog2 = floorLog2(tiles); |
| // If the image's width is greater than the height, use more tile columns than tile rows. |
| if (width >= height) { |
| splitTilesLog2(width, height, tilesLog2, tileColsLog2, tileRowsLog2); |
| } else { |
| splitTilesLog2(height, width, tilesLog2, tileRowsLog2, tileColsLog2); |
| } |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // avifCodecEncodeOutput |
| |
| avifCodecEncodeOutput * avifCodecEncodeOutputCreate(void) |
| { |
| avifCodecEncodeOutput * encodeOutput = (avifCodecEncodeOutput *)avifAlloc(sizeof(avifCodecEncodeOutput)); |
| if (encodeOutput == NULL) { |
| return NULL; |
| } |
| memset(encodeOutput, 0, sizeof(avifCodecEncodeOutput)); |
| if (!avifArrayCreate(&encodeOutput->samples, sizeof(avifEncodeSample), 1)) { |
| avifCodecEncodeOutputDestroy(encodeOutput); |
| return NULL; |
| } |
| return encodeOutput; |
| } |
| |
| avifResult avifCodecEncodeOutputAddSample(avifCodecEncodeOutput * encodeOutput, const uint8_t * data, size_t len, avifBool sync) |
| { |
| avifEncodeSample * sample = (avifEncodeSample *)avifArrayPush(&encodeOutput->samples); |
| AVIF_CHECKERR(sample, AVIF_RESULT_OUT_OF_MEMORY); |
| const avifResult result = avifRWDataSet(&sample->data, data, len); |
| if (result != AVIF_RESULT_OK) { |
| avifArrayPop(&encodeOutput->samples); |
| return result; |
| } |
| sample->sync = sync; |
| return AVIF_RESULT_OK; |
| } |
| |
| void avifCodecEncodeOutputDestroy(avifCodecEncodeOutput * encodeOutput) |
| { |
| for (uint32_t sampleIndex = 0; sampleIndex < encodeOutput->samples.count; ++sampleIndex) { |
| avifRWDataFree(&encodeOutput->samples.sample[sampleIndex].data); |
| } |
| avifArrayDestroy(&encodeOutput->samples); |
| avifFree(encodeOutput); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // avifEncoderItem |
| |
| // one "item" worth for encoder |
| typedef struct avifEncoderItem |
| { |
| uint16_t id; |
| uint8_t type[4]; // 4-character 'item_type' field in the 'infe' (item info entry) box |
| avifCodec * codec; // only present on image items |
| avifCodecEncodeOutput * encodeOutput; // AV1 sample data |
| avifRWData metadataPayload; // Exif/XMP data |
| avifCodecConfigurationBox av1C; // Harvested in avifEncoderFinish(), if encodeOutput has samples |
| // TODO(yguyon): Rename or add av2C |
| uint32_t cellIndex; // Which row-major cell index corresponds to this item. only present on image items |
| avifItemCategory itemCategory; // Category of item being encoded |
| avifBool hiddenImage; // A hidden image item has (flags & 1) equal to 1 in its ItemInfoEntry. |
| |
| const char * infeName; |
| size_t infeNameSize; |
| const char * infeContentType; |
| size_t infeContentTypeSize; |
| avifOffsetFixupArray mdatFixups; |
| |
| uint16_t irefToID; // if non-zero, make an iref from this id -> irefToID |
| const char * irefType; |
| |
| uint32_t gridCols; // if non-zero (legal range [1-256]), this is a grid item |
| uint32_t gridRows; // if non-zero (legal range [1-256]), this is a grid item |
| |
| // the reconstructed image of a grid item will be trimmed to these dimensions (only present on grid items) |
| uint32_t gridWidth; |
| uint32_t gridHeight; |
| |
| uint32_t extraLayerCount; // if non-zero (legal range [1-(AVIF_MAX_AV1_LAYER_COUNT-1)]), this is a layered AV1 image |
| |
| uint16_t dimgFromID; // if non-zero, make an iref from dimgFromID -> this id |
| |
| struct ipmaArray ipma; |
| } avifEncoderItem; |
| AVIF_ARRAY_DECLARE(avifEncoderItemArray, avifEncoderItem, item); |
| |
| // --------------------------------------------------------------------------- |
| // avifEncoderItemReference |
| |
| // pointer to one "item" interested in |
| typedef avifEncoderItem * avifEncoderItemReference; |
| AVIF_ARRAY_DECLARE(avifEncoderItemReferenceArray, avifEncoderItemReference, ref); |
| |
| // --------------------------------------------------------------------------- |
| // avifEncoderFrame |
| |
| typedef struct avifEncoderFrame |
| { |
| uint64_t durationInTimescales; |
| } avifEncoderFrame; |
| AVIF_ARRAY_DECLARE(avifEncoderFrameArray, avifEncoderFrame, frame); |
| |
| // --------------------------------------------------------------------------- |
| // avifEncoderData |
| |
| AVIF_ARRAY_DECLARE(avifEncoderItemIdArray, uint16_t, item_id); |
| |
| typedef struct avifEncoderData |
| { |
| avifEncoderItemArray items; |
| avifEncoderFrameArray frames; |
| // Map the encoder settings quality and qualityAlpha to quantizer and quantizerAlpha |
| int quantizer; |
| int quantizerAlpha; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| int quantizerGainMap; |
| #endif |
| // tileRowsLog2 and tileColsLog2 are the actual tiling values after automatic tiling is handled |
| int tileRowsLog2; |
| int tileColsLog2; |
| avifEncoder lastEncoder; |
| // lastQuantizer and lastQuantizerAlpha are the quantizer and quantizerAlpha values used last |
| // time |
| int lastQuantizer; |
| int lastQuantizerAlpha; |
| // lastTileRowsLog2 and lastTileColsLog2 are the actual tiling values used last time |
| int lastTileRowsLog2; |
| int lastTileColsLog2; |
| avifImage * imageMetadata; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| // For convenience, holds metadata derived from the avifGainMap struct (when present) about the |
| // altenate image |
| avifImage * altImageMetadata; |
| #endif |
| uint16_t lastItemID; |
| uint16_t primaryItemID; |
| avifEncoderItemIdArray alternativeItemIDs; // list of item ids for an 'altr' box (group of alternatives to each other) |
| avifBool singleImage; // if true, the AVIF_ADD_IMAGE_FLAG_SINGLE flag was set on the first call to avifEncoderAddImage() |
| avifBool alphaPresent; |
| size_t gainMapSizeBytes; |
| // Fields specific to AV1/AV2 |
| const char * imageItemType; // "av01" for AV1 ("av02" for AV2 if AVIF_CODEC_AVM) |
| const char * configPropName; // "av1C" for AV1 ("av2C" for AV2 if AVIF_CODEC_AVM) |
| } avifEncoderData; |
| |
| static void avifEncoderDataDestroy(avifEncoderData * data); |
| |
| // Returns NULL if a memory allocation failed. |
| static avifEncoderData * avifEncoderDataCreate(void) |
| { |
| avifEncoderData * data = (avifEncoderData *)avifAlloc(sizeof(avifEncoderData)); |
| if (!data) { |
| return NULL; |
| } |
| memset(data, 0, sizeof(avifEncoderData)); |
| data->imageMetadata = avifImageCreateEmpty(); |
| if (!data->imageMetadata) { |
| goto error; |
| } |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| data->altImageMetadata = avifImageCreateEmpty(); |
| if (!data->altImageMetadata) { |
| goto error; |
| } |
| #endif |
| if (!avifArrayCreate(&data->items, sizeof(avifEncoderItem), 8)) { |
| goto error; |
| } |
| if (!avifArrayCreate(&data->frames, sizeof(avifEncoderFrame), 1)) { |
| goto error; |
| } |
| if (!avifArrayCreate(&data->alternativeItemIDs, sizeof(uint16_t), 1)) { |
| goto error; |
| } |
| return data; |
| |
| error: |
| avifEncoderDataDestroy(data); |
| return NULL; |
| } |
| |
| static avifEncoderItem * avifEncoderDataCreateItem(avifEncoderData * data, const char * type, const char * infeName, size_t infeNameSize, uint32_t cellIndex) |
| { |
| avifEncoderItem * item = (avifEncoderItem *)avifArrayPush(&data->items); |
| if (item == NULL) { |
| return NULL; |
| } |
| ++data->lastItemID; |
| item->id = data->lastItemID; |
| memcpy(item->type, type, sizeof(item->type)); |
| item->infeName = infeName; |
| item->infeNameSize = infeNameSize; |
| item->encodeOutput = avifCodecEncodeOutputCreate(); |
| if (item->encodeOutput == NULL) { |
| goto error; |
| } |
| item->cellIndex = cellIndex; |
| if (!avifArrayCreate(&item->mdatFixups, sizeof(avifOffsetFixup), 4)) { |
| goto error; |
| } |
| return item; |
| |
| error: |
| if (item->encodeOutput != NULL) { |
| avifCodecEncodeOutputDestroy(item->encodeOutput); |
| } |
| --data->lastItemID; |
| avifArrayPop(&data->items); |
| return NULL; |
| } |
| |
| static avifEncoderItem * avifEncoderDataFindItemByID(avifEncoderData * data, uint16_t id) |
| { |
| for (uint32_t itemIndex = 0; itemIndex < data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &data->items.item[itemIndex]; |
| if (item->id == id) { |
| return item; |
| } |
| } |
| return NULL; |
| } |
| |
| static void avifEncoderDataDestroy(avifEncoderData * data) |
| { |
| for (uint32_t i = 0; i < data->items.count; ++i) { |
| avifEncoderItem * item = &data->items.item[i]; |
| if (item->codec) { |
| avifCodecDestroy(item->codec); |
| } |
| avifCodecEncodeOutputDestroy(item->encodeOutput); |
| avifRWDataFree(&item->metadataPayload); |
| avifArrayDestroy(&item->mdatFixups); |
| } |
| if (data->imageMetadata) { |
| avifImageDestroy(data->imageMetadata); |
| } |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (data->altImageMetadata) { |
| avifImageDestroy(data->altImageMetadata); |
| } |
| #endif |
| avifArrayDestroy(&data->items); |
| avifArrayDestroy(&data->frames); |
| avifArrayDestroy(&data->alternativeItemIDs); |
| avifFree(data); |
| } |
| |
| static avifResult avifEncoderItemAddMdatFixup(avifEncoderItem * item, const avifRWStream * s) |
| { |
| avifOffsetFixup * fixup = (avifOffsetFixup *)avifArrayPush(&item->mdatFixups); |
| AVIF_CHECKERR(fixup != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| fixup->offset = avifRWStreamOffset(s); |
| return AVIF_RESULT_OK; |
| } |
| |
| // --------------------------------------------------------------------------- |
| // 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)); |
| if (dedup == NULL) { |
| return NULL; |
| } |
| memset(dedup, 0, sizeof(avifItemPropertyDedup)); |
| if (!avifArrayCreate(&dedup->properties, sizeof(avifItemProperty), 8)) { |
| avifFree(dedup); |
| return NULL; |
| } |
| if (avifRWDataRealloc(&dedup->buffer, 2048) != AVIF_RESULT_OK) { |
| avifArrayDestroy(&dedup->properties); |
| avifFree(dedup); |
| return NULL; |
| } |
| 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 property'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. |
| // |
| // On success, this function adds to the given ipma box a property association linking the reused |
| // or newly created property with the item. |
| static avifResult avifItemPropertyDedupFinish(avifItemPropertyDedup * dedup, avifRWStream * outputStream, struct ipmaArray * ipma, avifBool essential) |
| { |
| uint8_t propertyIndex = 0; |
| 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 |
| propertyIndex = property->index; |
| AVIF_ASSERT_OR_RETURN(propertyIndex != 0); |
| break; |
| } |
| } |
| |
| if (propertyIndex == 0) { |
| // Write a new property, and remember its location in the output stream for future deduplication |
| avifItemProperty * property = (avifItemProperty *)avifArrayPush(&dedup->properties); |
| AVIF_CHECKERR(property != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| property->index = ++dedup->nextIndex; // preincrement so the first new index is 1 (as ipma is 1-indexed) |
| property->size = newPropertySize; |
| property->offset = avifRWStreamOffset(outputStream); |
| AVIF_CHECKRES(avifRWStreamWrite(outputStream, dedup->buffer.data, newPropertySize)); |
| propertyIndex = property->index; |
| } |
| |
| AVIF_CHECKERR(ipma->count < MAX_ASSOCIATIONS, AVIF_RESULT_UNKNOWN_ERROR); |
| ipma->associations[ipma->count] = propertyIndex; |
| ipma->essential[ipma->count] = essential; |
| ++ipma->count; |
| return AVIF_RESULT_OK; |
| } |
| |
| // --------------------------------------------------------------------------- |
| |
| static const avifScalingMode noScaling = { { 1, 1 }, { 1, 1 } }; |
| |
| avifEncoder * avifEncoderCreate(void) |
| { |
| avifEncoder * encoder = (avifEncoder *)avifAlloc(sizeof(avifEncoder)); |
| if (!encoder) { |
| return NULL; |
| } |
| memset(encoder, 0, sizeof(avifEncoder)); |
| encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; |
| encoder->maxThreads = 1; |
| encoder->speed = AVIF_SPEED_DEFAULT; |
| encoder->keyframeInterval = 0; |
| encoder->timescale = 1; |
| encoder->repetitionCount = AVIF_REPETITION_COUNT_INFINITE; |
| encoder->quality = AVIF_QUALITY_DEFAULT; |
| encoder->qualityAlpha = AVIF_QUALITY_DEFAULT; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| encoder->qualityGainMap = AVIF_QUALITY_DEFAULT; |
| #endif |
| encoder->minQuantizer = AVIF_QUANTIZER_BEST_QUALITY; |
| encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY; |
| encoder->minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY; |
| encoder->maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY; |
| encoder->tileRowsLog2 = 0; |
| encoder->tileColsLog2 = 0; |
| encoder->autoTiling = AVIF_FALSE; |
| encoder->scalingMode = noScaling; |
| encoder->data = avifEncoderDataCreate(); |
| encoder->csOptions = avifCodecSpecificOptionsCreate(); |
| if (!encoder->data || !encoder->csOptions) { |
| avifEncoderDestroy(encoder); |
| return NULL; |
| } |
| encoder->headerFormat = AVIF_HEADER_FULL; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_NONE; |
| #endif |
| return encoder; |
| } |
| |
| void avifEncoderDestroy(avifEncoder * encoder) |
| { |
| if (encoder->csOptions) { |
| avifCodecSpecificOptionsDestroy(encoder->csOptions); |
| } |
| if (encoder->data) { |
| avifEncoderDataDestroy(encoder->data); |
| } |
| avifFree(encoder); |
| } |
| |
| avifResult avifEncoderSetCodecSpecificOption(avifEncoder * encoder, const char * key, const char * value) |
| { |
| return avifCodecSpecificOptionsSet(encoder->csOptions, key, value); |
| } |
| |
| static void avifEncoderBackupSettings(avifEncoder * encoder) |
| { |
| avifEncoder * lastEncoder = &encoder->data->lastEncoder; |
| |
| // lastEncoder->data is only used to mark that lastEncoder is initialized. lastEncoder->data |
| // must not be dereferenced. |
| lastEncoder->data = encoder->data; |
| lastEncoder->codecChoice = encoder->codecChoice; |
| lastEncoder->maxThreads = encoder->maxThreads; |
| lastEncoder->speed = encoder->speed; |
| lastEncoder->keyframeInterval = encoder->keyframeInterval; |
| lastEncoder->timescale = encoder->timescale; |
| lastEncoder->repetitionCount = encoder->repetitionCount; |
| lastEncoder->extraLayerCount = encoder->extraLayerCount; |
| lastEncoder->minQuantizer = encoder->minQuantizer; |
| lastEncoder->maxQuantizer = encoder->maxQuantizer; |
| lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha; |
| lastEncoder->maxQuantizerAlpha = encoder->maxQuantizerAlpha; |
| encoder->data->lastQuantizer = encoder->data->quantizer; |
| encoder->data->lastQuantizerAlpha = encoder->data->quantizerAlpha; |
| encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2; |
| encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2; |
| lastEncoder->scalingMode = encoder->scalingMode; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| lastEncoder->sampleTransformRecipe = encoder->sampleTransformRecipe; |
| #endif |
| } |
| |
| // This function detects changes made on avifEncoder. It returns true on success (i.e., if every |
| // change is valid), or false on failure (i.e., if any setting that can't change was changed). It |
| // reports a bitwise-OR of detected changes in encoderChanges. |
| static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncoderChanges * encoderChanges) |
| { |
| const avifEncoder * lastEncoder = &encoder->data->lastEncoder; |
| *encoderChanges = 0; |
| |
| if (!lastEncoder->data) { |
| // lastEncoder is not initialized. |
| return AVIF_TRUE; |
| } |
| |
| if ((lastEncoder->codecChoice != encoder->codecChoice) || (lastEncoder->maxThreads != encoder->maxThreads) || |
| (lastEncoder->speed != encoder->speed) || (lastEncoder->keyframeInterval != encoder->keyframeInterval) || |
| (lastEncoder->timescale != encoder->timescale) || (lastEncoder->repetitionCount != encoder->repetitionCount) || |
| (lastEncoder->extraLayerCount != encoder->extraLayerCount)) { |
| return AVIF_FALSE; |
| } |
| |
| if (encoder->data->lastQuantizer != encoder->data->quantizer) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_QUANTIZER; |
| } |
| if (encoder->data->lastQuantizerAlpha != encoder->data->quantizerAlpha) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA; |
| } |
| if (lastEncoder->minQuantizer != encoder->minQuantizer) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_MIN_QUANTIZER; |
| } |
| if (lastEncoder->maxQuantizer != encoder->maxQuantizer) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_MAX_QUANTIZER; |
| } |
| if (lastEncoder->minQuantizerAlpha != encoder->minQuantizerAlpha) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_MIN_QUANTIZER_ALPHA; |
| } |
| if (lastEncoder->maxQuantizerAlpha != encoder->maxQuantizerAlpha) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA; |
| } |
| if (encoder->data->lastTileRowsLog2 != encoder->data->tileRowsLog2) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2; |
| } |
| if (encoder->data->lastTileColsLog2 != encoder->data->tileColsLog2) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_TILE_COLS_LOG2; |
| } |
| if (memcmp(&lastEncoder->scalingMode, &encoder->scalingMode, sizeof(avifScalingMode)) != 0) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_SCALING_MODE; |
| } |
| if (encoder->csOptions->count > 0) { |
| *encoderChanges |= AVIF_ENCODER_CHANGE_CODEC_SPECIFIC; |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| if (lastEncoder->sampleTransformRecipe != encoder->sampleTransformRecipe) { |
| return AVIF_FALSE; |
| } |
| #endif |
| |
| return AVIF_TRUE; |
| } |
| |
| // Same as 'avifEncoderWriteColorProperties' but for the colr nclx box only. |
| static avifResult avifEncoderWriteNclxProperty(avifRWStream * dedupStream, |
| avifRWStream * outputStream, |
| const avifImage * imageMetadata, |
| struct ipmaArray * ipma, |
| avifItemPropertyDedup * dedup) |
| { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker colr; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "colr", AVIF_BOX_SIZE_TBD, &colr)); |
| AVIF_CHECKRES(avifRWStreamWriteChars(dedupStream, "nclx", 4)); // unsigned int(32) colour_type; |
| AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->colorPrimaries)); // unsigned int(16) colour_primaries; |
| AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->transferCharacteristics)); // unsigned int(16) transfer_characteristics; |
| AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->matrixCoefficients)); // unsigned int(16) matrix_coefficients; |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, (imageMetadata->yuvRange == AVIF_RANGE_FULL) ? 1 : 0, /*bitCount=*/1)); // unsigned int(1) full_range_flag; |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, 0, /*bitCount=*/7)); // unsigned int(7) reserved = 0; |
| avifRWStreamFinishBox(dedupStream, colr); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_FALSE)); |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| // Subset of avifEncoderWriteColorProperties() for the properties pasp, clap, irot, imir. |
| static avifResult avifEncoderWriteExtendedColorProperties(avifRWStream * dedupStream, |
| avifRWStream * outputStream, |
| const avifImage * imageMetadata, |
| struct ipmaArray * ipma, |
| avifItemPropertyDedup * dedup); |
| |
| // 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 avifResult avifEncoderWriteColorProperties(avifRWStream * outputStream, |
| const avifImage * imageMetadata, |
| struct ipmaArray * ipma, |
| avifItemPropertyDedup * dedup) |
| { |
| // outputStream is the final bitstream that will be output by the libavif encoder API. |
| // dedupStream is either equal to outputStream or to &dedup->s which is a temporary stream used |
| // to store parts of the final bitstream; these parts may be discarded if they are a duplicate |
| // of an already stored property. |
| avifRWStream * dedupStream = outputStream; |
| if (dedup) { |
| AVIF_ASSERT_OR_RETURN(ipma); |
| |
| // Use the dedup's temporary stream for box writes. |
| dedupStream = &dedup->s; |
| } |
| |
| if (imageMetadata->icc.size > 0) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker colr; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "colr", AVIF_BOX_SIZE_TBD, &colr)); |
| AVIF_CHECKRES(avifRWStreamWriteChars(dedupStream, "prof", 4)); // unsigned int(32) colour_type; |
| AVIF_CHECKRES(avifRWStreamWrite(dedupStream, imageMetadata->icc.data, imageMetadata->icc.size)); |
| avifRWStreamFinishBox(dedupStream, colr); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, 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. |
| AVIF_CHECKRES(avifEncoderWriteNclxProperty(dedupStream, outputStream, imageMetadata, ipma, dedup)); |
| |
| return avifEncoderWriteExtendedColorProperties(dedupStream, outputStream, imageMetadata, ipma, dedup); |
| } |
| |
| // Same as 'avifEncoderWriteColorProperties' but for properties related to High Dynamic Range only. |
| static avifResult avifEncoderWriteHDRProperties(avifRWStream * dedupStream, |
| avifRWStream * outputStream, |
| const avifImage * imageMetadata, |
| struct ipmaArray * ipma, |
| avifItemPropertyDedup * dedup) |
| { |
| // Write Content Light Level Information, if present |
| if (imageMetadata->clli.maxCLL || imageMetadata->clli.maxPALL) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker clli; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "clli", AVIF_BOX_SIZE_TBD, &clli)); |
| AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->clli.maxCLL)); // unsigned int(16) max_content_light_level; |
| AVIF_CHECKRES(avifRWStreamWriteU16(dedupStream, imageMetadata->clli.maxPALL)); // unsigned int(16) max_pic_average_light_level; |
| avifRWStreamFinishBox(dedupStream, clli); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_FALSE)); |
| } |
| } |
| |
| // TODO(maryla): add other HDR boxes: mdcv, cclv, etc. |
| |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderWriteExtendedColorProperties(avifRWStream * dedupStream, |
| avifRWStream * outputStream, |
| const avifImage * imageMetadata, |
| struct ipmaArray * ipma, |
| avifItemPropertyDedup * dedup) |
| { |
| // Write (Optional) Transformations |
| if (imageMetadata->transformFlags & AVIF_TRANSFORM_PASP) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker pasp; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "pasp", AVIF_BOX_SIZE_TBD, &pasp)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->pasp.hSpacing)); // unsigned int(32) hSpacing; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->pasp.vSpacing)); // unsigned int(32) vSpacing; |
| avifRWStreamFinishBox(dedupStream, pasp); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_FALSE)); |
| } |
| } |
| if (imageMetadata->transformFlags & AVIF_TRANSFORM_CLAP) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker clap; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "clap", AVIF_BOX_SIZE_TBD, &clap)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.widthN)); // unsigned int(32) cleanApertureWidthN; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.widthD)); // unsigned int(32) cleanApertureWidthD; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.heightN)); // unsigned int(32) cleanApertureHeightN; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.heightD)); // unsigned int(32) cleanApertureHeightD; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.horizOffN)); // unsigned int(32) horizOffN; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.horizOffD)); // unsigned int(32) horizOffD; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.vertOffN)); // unsigned int(32) vertOffN; |
| AVIF_CHECKRES(avifRWStreamWriteU32(dedupStream, imageMetadata->clap.vertOffD)); // unsigned int(32) vertOffD; |
| avifRWStreamFinishBox(dedupStream, clap); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_TRUE)); |
| } |
| } |
| if (imageMetadata->transformFlags & AVIF_TRANSFORM_IROT) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker irot; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "irot", AVIF_BOX_SIZE_TBD, &irot)); |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, 0, /*bitCount=*/6)); // unsigned int (6) reserved = 0; |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, imageMetadata->irot.angle & 0x3, /*bitCount=*/2)); // unsigned int (2) angle; |
| avifRWStreamFinishBox(dedupStream, irot); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_TRUE)); |
| } |
| } |
| if (imageMetadata->transformFlags & AVIF_TRANSFORM_IMIR) { |
| if (dedup) { |
| avifItemPropertyDedupStart(dedup); |
| } |
| avifBoxMarker imir; |
| AVIF_CHECKRES(avifRWStreamWriteBox(dedupStream, "imir", AVIF_BOX_SIZE_TBD, &imir)); |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, 0, /*bitCount=*/7)); // unsigned int(7) reserved = 0; |
| AVIF_CHECKRES(avifRWStreamWriteBits(dedupStream, imageMetadata->imir.axis ? 1 : 0, /*bitCount=*/1)); // unsigned int(1) axis; |
| avifRWStreamFinishBox(dedupStream, imir); |
| if (dedup) { |
| AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, outputStream, ipma, AVIF_TRUE)); |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| // Write unassociated metadata items (EXIF, XMP) to a small meta box inside of a trak box. |
| // These items are implicitly associated with the track they are contained within. |
| static avifResult avifEncoderWriteTrackMetaBox(avifEncoder * encoder, avifRWStream * s) |
| { |
| // Count how many non-image items (such as EXIF/XMP) are being written |
| uint32_t metadataItemCount = 0; |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| if (memcmp(item->type, encoder->data->imageItemType, 4) != 0) { |
| ++metadataItemCount; |
| } |
| } |
| if (metadataItemCount == 0) { |
| // Don't even bother writing the trak meta box |
| return AVIF_RESULT_OK; |
| } |
| |
| avifBoxMarker meta; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "meta", AVIF_BOX_SIZE_TBD, 0, 0, &meta)); |
| |
| avifBoxMarker hdlr; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "hdlr", AVIF_BOX_SIZE_TBD, 0, 0, &hdlr)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, 0)); // unsigned int(32) pre_defined = 0; |
| AVIF_CHECKRES(avifRWStreamWriteChars(s, "pict", 4)); // unsigned int(32) handler_type; |
| AVIF_CHECKRES(avifRWStreamWriteZeros(s, 12)); // const unsigned int(32)[3] reserved = 0; |
| AVIF_CHECKRES(avifRWStreamWriteChars(s, "libavif", 8)); // string name; (writing null terminator) |
| avifRWStreamFinishBox(s, hdlr); |
| |
| avifBoxMarker iloc; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "iloc", AVIF_BOX_SIZE_TBD, 0, 0, &iloc)); |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, 4, /*bitCount=*/4)); // unsigned int(4) offset_size; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, 4, /*bitCount=*/4)); // unsigned int(4) length_size; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, /*bitCount=*/4)); // unsigned int(4) base_offset_size; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, /*bitCount=*/4)); // unsigned int(4) reserved; |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, (uint16_t)metadataItemCount)); // unsigned int(16) item_count; |
| for (uint32_t trakItemIndex = 0; trakItemIndex < encoder->data->items.count; ++trakItemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[trakItemIndex]; |
| if (memcmp(item->type, encoder->data->imageItemType, 4) == 0) { |
| // Skip over all non-metadata items |
| continue; |
| } |
| |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, item->id)); // unsigned int(16) item_ID; |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, 0)); // unsigned int(16) data_reference_index; |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, 1)); // unsigned int(16) extent_count; |
| AVIF_CHECKRES(avifEncoderItemAddMdatFixup(item, s)); // |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, 0 /* set later */)); // unsigned int(offset_size*8) extent_offset; |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, (uint32_t)item->metadataPayload.size)); // unsigned int(length_size*8) extent_length; |
| } |
| avifRWStreamFinishBox(s, iloc); |
| |
| avifBoxMarker iinf; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "iinf", AVIF_BOX_SIZE_TBD, 0, 0, &iinf)); |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, (uint16_t)metadataItemCount)); // unsigned int(16) entry_count; |
| for (uint32_t trakItemIndex = 0; trakItemIndex < encoder->data->items.count; ++trakItemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[trakItemIndex]; |
| if (memcmp(item->type, encoder->data->imageItemType, 4) == 0) { |
| continue; |
| } |
| |
| AVIF_ASSERT_OR_RETURN(!item->hiddenImage); |
| avifBoxMarker infe; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "infe", AVIF_BOX_SIZE_TBD, 2, 0, &infe)); |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, item->id)); // unsigned int(16) item_ID; |
| AVIF_CHECKRES(avifRWStreamWriteU16(s, 0)); // unsigned int(16) item_protection_index; |
| AVIF_CHECKRES(avifRWStreamWrite(s, item->type, 4)); // unsigned int(32) item_type; |
| AVIF_CHECKRES(avifRWStreamWriteChars(s, item->infeName, item->infeNameSize)); // string item_name; (writing null terminator) |
| if (item->infeContentType && item->infeContentTypeSize) { // string content_type; (writing null terminator) |
| AVIF_CHECKRES(avifRWStreamWriteChars(s, item->infeContentType, item->infeContentTypeSize)); |
| } |
| avifRWStreamFinishBox(s, infe); |
| } |
| avifRWStreamFinishBox(s, iinf); |
| |
| avifRWStreamFinishBox(s, meta); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifWriteGridPayload(avifRWData * data, uint32_t gridCols, uint32_t gridRows, uint32_t gridWidth, uint32_t gridHeight) |
| { |
| // ISO/IEC 23008-12 6.6.2.3.2 |
| // aligned(8) class ImageGrid { |
| // unsigned int(8) version = 0; |
| // unsigned int(8) flags; |
| // FieldLength = ((flags & 1) + 1) * 16; |
| // unsigned int(8) rows_minus_one; |
| // unsigned int(8) columns_minus_one; |
| // unsigned int(FieldLength) output_width; |
| // unsigned int(FieldLength) output_height; |
| // } |
| |
| uint8_t gridFlags = ((gridWidth > 65535) || (gridHeight > 65535)) ? 1 : 0; |
| |
| avifRWStream s; |
| avifRWStreamStart(&s, data); |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, 0)); // unsigned int(8) version = 0; |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, gridFlags)); // unsigned int(8) flags; |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, (uint8_t)(gridRows - 1))); // unsigned int(8) rows_minus_one; |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, (uint8_t)(gridCols - 1))); // unsigned int(8) columns_minus_one; |
| if (gridFlags & 1) { |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, gridWidth)); // unsigned int(FieldLength) output_width; |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, gridHeight)); // unsigned int(FieldLength) output_height; |
| } else { |
| uint16_t tmpWidth = (uint16_t)gridWidth; |
| uint16_t tmpHeight = (uint16_t)gridHeight; |
| AVIF_CHECKRES(avifRWStreamWriteU16(&s, tmpWidth)); // unsigned int(FieldLength) output_width; |
| AVIF_CHECKRES(avifRWStreamWriteU16(&s, tmpHeight)); // unsigned int(FieldLength) output_height; |
| } |
| avifRWStreamFinishWrite(&s); |
| return AVIF_RESULT_OK; |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| |
| static avifBool avifWriteToneMappedImagePayload(avifRWData * data, const avifGainMapMetadata * metadata) |
| { |
| avifRWStream s; |
| avifRWStreamStart(&s, data); |
| const uint8_t version = 0; |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, version)); |
| |
| const uint16_t minimumVersion = 0; |
| AVIF_CHECKRES(avifRWStreamWriteU16(&s, minimumVersion)); |
| const uint16_t writerVersion = 0; |
| AVIF_CHECKRES(avifRWStreamWriteU16(&s, writerVersion)); |
| |
| uint8_t flags = 0u; |
| // Always write three channels for now for simplicity. |
| // TODO(maryla): the draft says that this specifies the count of channels of the |
| // gain map. But tone mapping is done in RGB space so there are always three |
| // channels, even if the gain map is grayscale. Should this be revised? |
| const avifBool allChannelsIdentical = |
| metadata->gainMapMinN[0] == metadata->gainMapMinN[1] && metadata->gainMapMinN[0] == metadata->gainMapMinN[2] && |
| metadata->gainMapMinD[0] == metadata->gainMapMinD[1] && metadata->gainMapMinD[0] == metadata->gainMapMinD[2] && |
| metadata->gainMapMaxN[0] == metadata->gainMapMaxN[1] && metadata->gainMapMaxN[0] == metadata->gainMapMaxN[2] && |
| metadata->gainMapMaxD[0] == metadata->gainMapMaxD[1] && metadata->gainMapMaxD[0] == metadata->gainMapMaxD[2] && |
| metadata->gainMapGammaN[0] == metadata->gainMapGammaN[1] && metadata->gainMapGammaN[0] == metadata->gainMapGammaN[2] && |
| metadata->gainMapGammaD[0] == metadata->gainMapGammaD[1] && metadata->gainMapGammaD[0] == metadata->gainMapGammaD[2] && |
| metadata->baseOffsetN[0] == metadata->baseOffsetN[1] && metadata->baseOffsetN[0] == metadata->baseOffsetN[2] && |
| metadata->baseOffsetD[0] == metadata->baseOffsetD[1] && metadata->baseOffsetD[0] == metadata->baseOffsetD[2] && |
| metadata->alternateOffsetN[0] == metadata->alternateOffsetN[1] && |
| metadata->alternateOffsetN[0] == metadata->alternateOffsetN[2] && |
| metadata->alternateOffsetD[0] == metadata->alternateOffsetD[1] && |
| metadata->alternateOffsetD[0] == metadata->alternateOffsetD[2]; |
| const uint8_t channelCount = allChannelsIdentical ? 1u : 3u; |
| if (channelCount == 3) { |
| flags |= (1 << 7); |
| } |
| if (metadata->useBaseColorSpace) { |
| flags |= (1 << 6); |
| } |
| AVIF_CHECKRES(avifRWStreamWriteU8(&s, flags)); |
| |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->baseHdrHeadroomN)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->baseHdrHeadroomD)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->alternateHdrHeadroomN)); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->alternateHdrHeadroomD)); |
| |
| for (int c = 0; c < channelCount; ++c) { |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)metadata->gainMapMinN[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->gainMapMinD[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)metadata->gainMapMaxN[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->gainMapMaxD[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->gainMapGammaN[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->gainMapGammaD[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)metadata->baseOffsetN[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->baseOffsetD[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, (uint32_t)metadata->alternateOffsetN[c])); |
| AVIF_CHECKRES(avifRWStreamWriteU32(&s, metadata->alternateOffsetD[c])); |
| } |
| |
| avifRWStreamFinishWrite(&s); |
| return AVIF_TRUE; |
| } |
| |
| size_t avifEncoderGetGainMapSizeBytes(avifEncoder * encoder) |
| { |
| return encoder->data->gainMapSizeBytes; |
| } |
| |
| // Sets altImageMetadata's metadata values to represent the "alternate" image as if applying the gain map to the base image. |
| static avifResult avifImageCopyAltImageMetadata(avifImage * altImageMetadata, const avifImage * imageWithGainMap) |
| { |
| altImageMetadata->width = imageWithGainMap->width; |
| altImageMetadata->height = imageWithGainMap->height; |
| AVIF_CHECKRES(avifRWDataSet(&altImageMetadata->icc, imageWithGainMap->gainMap->altICC.data, imageWithGainMap->gainMap->altICC.size)); |
| altImageMetadata->colorPrimaries = imageWithGainMap->gainMap->altColorPrimaries; |
| altImageMetadata->transferCharacteristics = imageWithGainMap->gainMap->altTransferCharacteristics; |
| altImageMetadata->matrixCoefficients = imageWithGainMap->gainMap->altMatrixCoefficients; |
| altImageMetadata->yuvRange = imageWithGainMap->gainMap->altYUVRange; |
| altImageMetadata->depth = imageWithGainMap->gainMap->altDepth |
| ? imageWithGainMap->gainMap->altDepth |
| : AVIF_MAX(imageWithGainMap->depth, imageWithGainMap->gainMap->image->depth); |
| altImageMetadata->yuvFormat = (imageWithGainMap->gainMap->altPlaneCount == 1) ? AVIF_PIXEL_FORMAT_YUV400 : AVIF_PIXEL_FORMAT_YUV444; |
| altImageMetadata->clli = imageWithGainMap->gainMap->altCLLI; |
| return AVIF_RESULT_OK; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| static avifResult avifEncoderWriteSampleTransformTokens(avifRWStream * s, const avifSampleTransformExpression * expression) |
| { |
| AVIF_ASSERT_OR_RETURN(expression->count <= 256); |
| AVIF_CHECKRES(avifRWStreamWriteU8(s, (uint8_t)expression->count)); // unsigned int(8) token_count; |
| |
| for (uint32_t t = 0; t < expression->count; ++t) { |
| const avifSampleTransformToken * token = &expression->tokens[t]; |
| AVIF_CHECKRES(avifRWStreamWriteU8(s, token->type)); // unsigned int(8) token; |
| |
| if (token->type == AVIF_SAMPLE_TRANSFORM_CONSTANT) { |
| // TODO(yguyon): Verify two's complement representation is guaranteed here. |
| const uint32_t constant = *(const uint32_t *)&token->constant; |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, constant)); // signed int(1<<(bit_depth+3)) constant; |
| } else if (token->type == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX) { |
| AVIF_CHECKRES(avifRWStreamWriteU8(s, token->inputImageItemIndex)); // unsigned int(8) input_image_item_index; |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderWriteSampleTransformPayload(avifEncoder * encoder, avifRWData * data) |
| { |
| avifRWStream s; |
| avifRWStreamStart(&s, data); |
| AVIF_CHECKRES(avifRWStreamWriteBits(&s, 0, /*bitCount=*/6)); // unsigned int(6) version = 0; |
| // AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32 is necessary because the two input images |
| // once combined use 16-bit unsigned values, but intermediate results are stored in signed integers. |
| AVIF_CHECKRES(avifRWStreamWriteBits(&s, AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, /*bitCount=*/2)); // unsigned int(2) bit_depth; |
| |
| avifSampleTransformExpression expression = { 0 }; |
| AVIF_CHECKRES(avifSampleTransformRecipeToExpression(encoder->sampleTransformRecipe, &expression)); |
| const avifResult result = avifEncoderWriteSampleTransformTokens(&s, &expression); |
| avifArrayDestroy(&expression); |
| if (result != AVIF_RESULT_OK) { |
| avifDiagnosticsPrintf(&encoder->diag, "Failed to write sample transform metadata for recipe %d", (int)encoder->sampleTransformRecipe); |
| return result; |
| } |
| |
| avifRWStreamFinishWrite(&s); |
| return AVIF_RESULT_OK; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM |
| |
| static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif) |
| { |
| size_t exifTiffHeaderOffset; |
| const avifResult result = avifGetExifTiffHeaderOffset(exif->data, exif->size, &exifTiffHeaderOffset); |
| if (result != AVIF_RESULT_OK) { |
| // Couldn't find the TIFF header |
| return result; |
| } |
| |
| avifEncoderItem * exifItem = avifEncoderDataCreateItem(data, "Exif", "Exif", 5, 0); |
| if (!exifItem) { |
| return AVIF_RESULT_OUT_OF_MEMORY; |
| } |
| exifItem->irefToID = data->primaryItemID; |
| exifItem->irefType = "cdsc"; |
| |
| const uint32_t offset32bit = avifHTONL((uint32_t)exifTiffHeaderOffset); |
| AVIF_CHECKRES(avifRWDataRealloc(&exifItem->metadataPayload, sizeof(offset32bit) + exif->size)); |
| memcpy(exifItem->metadataPayload.data, &offset32bit, sizeof(offset32bit)); |
| memcpy(exifItem->metadataPayload.data + sizeof(offset32bit), exif->data, exif->size); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderDataCreateXMPItem(avifEncoderData * data, const avifRWData * xmp) |
| { |
| avifEncoderItem * xmpItem = avifEncoderDataCreateItem(data, "mime", "XMP", 4, 0); |
| if (!xmpItem) { |
| return AVIF_RESULT_OUT_OF_MEMORY; |
| } |
| xmpItem->irefToID = data->primaryItemID; |
| xmpItem->irefType = "cdsc"; |
| |
| xmpItem->infeContentType = xmpContentType; |
| xmpItem->infeContentTypeSize = xmpContentTypeSize; |
| AVIF_CHECKRES(avifRWDataSet(&xmpItem->metadataPayload, xmp->data, xmp->size)); |
| return AVIF_RESULT_OK; |
| } |
| |
| // Same as avifImageCopy() but pads the dstImage with border pixel values to reach dstWidth and dstHeight. |
| static avifResult avifImageCopyAndPad(avifImage * const dstImage, const avifImage * srcImage, uint32_t dstWidth, uint32_t dstHeight) |
| { |
| AVIF_ASSERT_OR_RETURN(dstImage); |
| AVIF_ASSERT_OR_RETURN(!dstImage->width && !dstImage->height); // dstImage is not set yet. |
| AVIF_ASSERT_OR_RETURN(dstWidth >= srcImage->width); |
| AVIF_ASSERT_OR_RETURN(dstHeight >= srcImage->height); |
| |
| // Copy all fields but do not allocate the planes. |
| AVIF_CHECKRES(avifImageCopy(dstImage, srcImage, (avifPlanesFlag)0)); |
| dstImage->width = dstWidth; |
| dstImage->height = dstHeight; |
| |
| if (srcImage->yuvPlanes[AVIF_CHAN_Y]) { |
| AVIF_CHECKRES(avifImageAllocatePlanes(dstImage, AVIF_PLANES_YUV)); |
| } |
| if (srcImage->alphaPlane) { |
| AVIF_CHECKRES(avifImageAllocatePlanes(dstImage, AVIF_PLANES_A)); |
| } |
| const avifBool usesU16 = avifImageUsesU16(srcImage); |
| for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_A; ++plane) { |
| const uint8_t * srcRow = avifImagePlane(srcImage, plane); |
| const uint32_t srcRowBytes = avifImagePlaneRowBytes(srcImage, plane); |
| const uint32_t srcPlaneWidth = avifImagePlaneWidth(srcImage, plane); |
| const uint32_t srcPlaneHeight = avifImagePlaneHeight(srcImage, plane); // 0 for A if no alpha and 0 for UV if 4:0:0. |
| const size_t srcPlaneWidthBytes = (size_t)srcPlaneWidth << usesU16; |
| |
| uint8_t * dstRow = avifImagePlane(dstImage, plane); |
| const uint32_t dstRowBytes = avifImagePlaneRowBytes(dstImage, plane); |
| const uint32_t dstPlaneWidth = avifImagePlaneWidth(dstImage, plane); |
| const uint32_t dstPlaneHeight = avifImagePlaneHeight(dstImage, plane); // 0 for A if no alpha and 0 for UV if 4:0:0. |
| const size_t dstPlaneWidthBytes = (size_t)dstPlaneWidth << usesU16; |
| |
| for (uint32_t j = 0; j < srcPlaneHeight; ++j) { |
| memcpy(dstRow, srcRow, srcPlaneWidthBytes); |
| |
| // Pad columns. |
| if (dstPlaneWidth > srcPlaneWidth) { |
| if (usesU16) { |
| uint16_t * dstRow16 = (uint16_t *)dstRow; |
| for (uint32_t x = srcPlaneWidth; x < dstPlaneWidth; ++x) { |
| dstRow16[x] = dstRow16[srcPlaneWidth - 1]; |
| } |
| } else { |
| memset(&dstRow[srcPlaneWidth], dstRow[srcPlaneWidth - 1], dstPlaneWidth - srcPlaneWidth); |
| } |
| } |
| srcRow += srcRowBytes; |
| dstRow += dstRowBytes; |
| } |
| |
| // Pad rows. |
| for (uint32_t j = srcPlaneHeight; j < dstPlaneHeight; ++j) { |
| memcpy(dstRow, dstRow - dstRowBytes, dstPlaneWidthBytes); |
| dstRow += dstRowBytes; |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| static int avifQualityToQuantizer(int quality, int minQuantizer, int maxQuantizer) |
| { |
| int quantizer; |
| if (quality == AVIF_QUALITY_DEFAULT) { |
| // In older libavif releases, avifEncoder didn't have the quality and qualityAlpha fields. |
| // Supply a default value for quantizer. |
| quantizer = (minQuantizer + maxQuantizer) / 2; |
| quantizer = AVIF_CLAMP(quantizer, 0, 63); |
| } else { |
| quality = AVIF_CLAMP(quality, 0, 100); |
| quantizer = ((100 - quality) * 63 + 50) / 100; |
| } |
| return quantizer; |
| } |
| |
| static const char infeNameColor[] = "Color"; |
| static const char infeNameAlpha[] = "Alpha"; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| static const char infeNameGainMap[] = "GMap"; |
| #endif |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| static const char infeNameSampleTransform[] = "SampleTransform"; |
| #endif |
| |
| static const char * getInfeName(avifItemCategory itemCategory) |
| { |
| if (avifIsAlpha(itemCategory)) { |
| return infeNameAlpha; |
| } |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (itemCategory == AVIF_ITEM_GAIN_MAP) { |
| return infeNameGainMap; |
| } |
| #endif |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| if (itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY) { |
| return infeNameSampleTransform; |
| } |
| #endif |
| return infeNameColor; |
| } |
| |
| // Adds the items for a single cell or a grid of cells. Outputs the topLevelItemID which is |
| // the only item if there is exactly one cell, or the grid item for multiple cells. |
| // Note: The topLevelItemID output argument has the type uint16_t* instead of avifEncoderItem** because |
| // the avifEncoderItem pointer may be invalidated by a call to avifEncoderDataCreateItem(). |
| static avifResult avifEncoderAddImageItems(avifEncoder * encoder, |
| uint32_t gridCols, |
| uint32_t gridRows, |
| uint32_t gridWidth, |
| uint32_t gridHeight, |
| avifItemCategory itemCategory, |
| uint16_t * topLevelItemID) |
| { |
| const uint32_t cellCount = gridCols * gridRows; |
| const char * infeName = getInfeName(itemCategory); |
| const size_t infeNameSize = strlen(infeName) + 1; |
| |
| if (cellCount > 1) { |
| avifEncoderItem * gridItem = avifEncoderDataCreateItem(encoder->data, "grid", infeName, infeNameSize, 0); |
| AVIF_CHECKRES(avifWriteGridPayload(&gridItem->metadataPayload, gridCols, gridRows, gridWidth, gridHeight)); |
| gridItem->itemCategory = itemCategory; |
| gridItem->gridCols = gridCols; |
| gridItem->gridRows = gridRows; |
| gridItem->gridWidth = gridWidth; |
| gridItem->gridHeight = gridHeight; |
| *topLevelItemID = gridItem->id; |
| } |
| |
| for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) { |
| avifEncoderItem * item = |
| avifEncoderDataCreateItem(encoder->data, encoder->data->imageItemType, infeName, infeNameSize, cellIndex); |
| AVIF_CHECKERR(item, AVIF_RESULT_OUT_OF_MEMORY); |
| AVIF_CHECKRES(avifCodecCreate(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE, &item->codec)); |
| item->codec->csOptions = encoder->csOptions; |
| item->codec->diag = &encoder->diag; |
| item->itemCategory = itemCategory; |
| item->extraLayerCount = encoder->extraLayerCount; |
| |
| if (cellCount > 1) { |
| item->dimgFromID = *topLevelItemID; |
| item->hiddenImage = AVIF_TRUE; |
| } else { |
| *topLevelItemID = item->id; |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| static avifResult avifEncoderCreateBitDepthExtensionItems(avifEncoder * encoder, |
| uint32_t gridCols, |
| uint32_t gridRows, |
| uint32_t gridWidth, |
| uint32_t gridHeight, |
| uint16_t colorItemID) |
| { |
| AVIF_ASSERT_OR_RETURN(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || |
| encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B); |
| |
| // There are multiple possible ISOBMFF box hierarchies for translucent images, |
| // using 'sato' (Sample Transform) derived image items: |
| // - a primary 'sato' item uses a main color coded item and a hidden color coded item; each color coded |
| // item has an auxiliary alpha coded item; the main color coded item and the 'sato' item are in |
| // an 'altr' group (backward-compatible, implemented) |
| // - a primary 'sato' item uses a main color coded item and a hidden color coded item; the primary |
| // 'sato' item has an auxiliary alpha 'sato' item using two alpha coded items (backward-incompatible) |
| // Likewise, there are multiple possible ISOBMFF box hierarchies for bit-depth-extended grids, |
| // using 'sato' (Sample Transform) derived image items: |
| // - a primary color 'grid', an auxiliary alpha 'grid', a hidden color 'grid', a hidden auxiliary alpha 'grid' |
| // and a 'sato' using the two color 'grid's as input items in this order; the primary color item |
| // and the 'sato' item being in an 'altr' group (backward-compatible, implemented) |
| // - a primary 'grid' of 'sato' cells and an auxiliary alpha 'grid' of 'sato' cells (backward-incompatible) |
| avifEncoderItem * sampleTransformItem = avifEncoderDataCreateItem(encoder->data, |
| "sato", |
| infeNameSampleTransform, |
| /*infeNameSize=*/strlen(infeNameSampleTransform) + 1, |
| /*cellIndex=*/0); |
| AVIF_CHECKRES(avifEncoderWriteSampleTransformPayload(encoder, &sampleTransformItem->metadataPayload)); |
| sampleTransformItem->itemCategory = AVIF_ITEM_SAMPLE_TRANSFORM; |
| uint16_t sampleTransformItemID = sampleTransformItem->id; |
| // 'altr' group |
| uint16_t * alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); |
| AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| *alternativeItemID = sampleTransformItem->id; |
| alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); |
| AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| *alternativeItemID = colorItemID; |
| |
| uint16_t bitDepthExtensionColorItemId; |
| AVIF_CHECKRES( |
| avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR, &bitDepthExtensionColorItemId)); |
| avifEncoderItem * bitDepthExtensionColorItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionColorItemId); |
| assert(bitDepthExtensionColorItem); |
| bitDepthExtensionColorItem->hiddenImage = AVIF_TRUE; |
| |
| // Set the color and bit depth extension items' dimgFromID value to point to the sample transform item. |
| // The color item shall be first, and the bit depth extension item second. avifEncoderFinish() writes the |
| // dimg item references in item id order, so as long as colorItemID < bitDepthExtensionColorItemId, the order |
| // will be correct. |
| AVIF_ASSERT_OR_RETURN(colorItemID < bitDepthExtensionColorItemId); |
| avifEncoderItem * colorItem = avifEncoderDataFindItemByID(encoder->data, colorItemID); |
| AVIF_ASSERT_OR_RETURN(colorItem != NULL); |
| AVIF_ASSERT_OR_RETURN(colorItem->dimgFromID == 0); // Our internal API only allows one dimg value per item. |
| colorItem->dimgFromID = sampleTransformItemID; |
| bitDepthExtensionColorItem->dimgFromID = sampleTransformItemID; |
| |
| if (encoder->data->alphaPresent) { |
| uint16_t bitDepthExtensionAlphaItemId; |
| AVIF_CHECKRES( |
| avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA, &bitDepthExtensionAlphaItemId)); |
| avifEncoderItem * bitDepthExtensionAlphaItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionAlphaItemId); |
| assert(bitDepthExtensionAlphaItem); |
| bitDepthExtensionAlphaItem->irefType = "auxl"; |
| bitDepthExtensionAlphaItem->irefToID = bitDepthExtensionColorItemId; |
| if (encoder->data->imageMetadata->alphaPremultiplied) { |
| // The reference may have changed; fetch it again. |
| bitDepthExtensionColorItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionColorItemId); |
| assert(bitDepthExtensionColorItem); |
| bitDepthExtensionColorItem->irefType = "prem"; |
| bitDepthExtensionColorItem->irefToID = bitDepthExtensionAlphaItemId; |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| // Same as avifImageApplyExpression() but for the expression (inputImageItem [op] constant). |
| // Convenience function. |
| static avifResult avifImageApplyImgOpConst(avifImage * result, |
| const avifImage * inputImageItem, |
| avifSampleTransformTokenType op, |
| int32_t constant, |
| avifPlanesFlags planes) |
| { |
| // Postfix notation. |
| const avifSampleTransformToken tokens[] = { { AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0, /*inputImageItemIndex=*/1 }, |
| { AVIF_SAMPLE_TRANSFORM_CONSTANT, constant, 0 }, |
| { (uint8_t)op, 0, 0 } }; |
| return avifImageApplyOperations(result, AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, /*numTokens=*/3, tokens, /*numInputImageItems=*/1, &inputImageItem, planes); |
| } |
| |
| static avifResult avifImageCreateAllocate(avifImage ** sampleTransformedImage, const avifImage * reference, uint32_t numBits, avifPlanesFlag planes) |
| { |
| *sampleTransformedImage = avifImageCreate(reference->width, reference->height, numBits, reference->yuvFormat); |
| AVIF_CHECKERR(*sampleTransformedImage != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| return avifImageAllocatePlanes(*sampleTransformedImage, planes); |
| } |
| |
| static avifResult avifEncoderCreateSatoImage(const avifEncoder * encoder, |
| const avifEncoderItem * item, |
| avifBool itemWillBeEncodedLosslessly, |
| const avifImage * image, |
| avifImage ** sampleTransformedImage) |
| { |
| const avifPlanesFlag planes = avifIsAlpha(item->itemCategory) ? AVIF_PLANES_A : AVIF_PLANES_YUV; |
| // The first image item used as input to the 'sato' Sample Transform derived image item. |
| avifBool isBase = item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA; |
| if (!isBase) { |
| // The second image item used as input to the 'sato' Sample Transform derived image item. |
| AVIF_ASSERT_OR_RETURN(item->itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && |
| item->itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY); |
| } |
| |
| if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B) { |
| if (isBase) { |
| AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 8, planes)); |
| AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_DIVIDE, 256, planes)); |
| } else { |
| AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 8, planes)); |
| AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_AND, 255, planes)); |
| } |
| } else { |
| AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B, AVIF_RESULT_NOT_IMPLEMENTED); |
| if (isBase) { |
| AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 12, planes)); |
| AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_DIVIDE, 16, planes)); |
| } else { |
| AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 8, planes)); |
| AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_AND, 15, planes)); |
| // AVIF only supports 8, 10 or 12-bit image items. Scale the samples to fit the range. |
| // Note: The samples could be encoded as is without being shifted left before encoding, |
| // but they would not be shifted right after decoding either. Right shifting after |
| // decoding provides a guarantee on the range of values and on the lack of integer |
| // overflow, so it is safer to do these extra steps. |
| // It also makes more sense from a compression point-of-view to use the full range. |
| // Transform in-place. |
| AVIF_CHECKRES( |
| avifImageApplyImgOpConst(*sampleTransformedImage, *sampleTransformedImage, AVIF_SAMPLE_TRANSFORM_PRODUCT, 16, planes)); |
| if (!itemWillBeEncodedLosslessly) { |
| // Small loss at encoding could be amplified by the truncation caused by the right |
| // shift after decoding. Offset sample values now, before encoding, to round rather |
| // than floor the samples shifted after decoding. |
| // Note: Samples were just left shifted by numShiftedBits, so adding less than |
| // (1<<numShiftedBits) will not trigger any integer overflow. |
| // Transform in-place. |
| AVIF_CHECKRES( |
| avifImageApplyImgOpConst(*sampleTransformedImage, *sampleTransformedImage, AVIF_SAMPLE_TRANSFORM_SUM, 7, planes)); |
| } |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderCreateBitDepthExtensionImage(const avifEncoder * encoder, |
| const avifEncoderItem * item, |
| avifBool itemWillBeEncodedLosslessly, |
| const avifImage * image, |
| avifImage ** sampleTransformedImage) |
| { |
| AVIF_ASSERT_OR_RETURN(image->depth == 16); // Other bit depths could be supported but for now it is 16-bit only. |
| *sampleTransformedImage = NULL; |
| const avifResult result = avifEncoderCreateSatoImage(encoder, item, itemWillBeEncodedLosslessly, image, sampleTransformedImage); |
| if (result != AVIF_RESULT_OK && *sampleTransformedImage != NULL) { |
| avifImageDestroy(*sampleTransformedImage); |
| } |
| return result; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM |
| |
| static avifCodecType avifEncoderGetCodecType(const avifEncoder * encoder) |
| { |
| // TODO(yguyon): Rework when AVIF_CODEC_CHOICE_AUTO can be AVM |
| assert((encoder->codecChoice != AVIF_CODEC_CHOICE_AUTO) || |
| (strcmp(avifCodecName(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE), "avm") != 0)); |
| return avifCodecTypeFromChoice(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE); |
| } |
| |
| // This function is called after every color frame is encoded. It returns AVIF_TRUE if a keyframe needs to be forced for the next |
| // alpha frame to be encoded, AVIF_FALSE otherwise. |
| static avifBool avifEncoderDataShouldForceKeyframeForAlpha(const avifEncoderData * data, |
| const avifEncoderItem * colorItem, |
| avifAddImageFlags addImageFlags) |
| { |
| if (!data->alphaPresent) { |
| // There is no alpha plane. |
| return AVIF_FALSE; |
| } |
| if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) { |
| // Not an animated image. |
| return AVIF_FALSE; |
| } |
| if (data->frames.count == 0) { |
| // data->frames.count is the number of frames that have been encoded so far by previous calls to avifEncoderAddImage. If |
| // this is the first frame, there is no need to force keyframe. |
| return AVIF_FALSE; |
| } |
| const uint32_t colorFramesOutputSoFar = colorItem->encodeOutput->samples.count; |
| const avifBool isLaggedOutput = (data->frames.count + 1) != colorFramesOutputSoFar; |
| if (isLaggedOutput) { |
| // If the encoder is operating with lag, then there is no way to determine if the last encoded frame was a keyframe until |
| // the encoder outputs it (after the lag). So do not force keyframe for alpha channel in this case. |
| return AVIF_FALSE; |
| } |
| return colorItem->encodeOutput->samples.sample[colorFramesOutputSoFar - 1].sync; |
| } |
| |
| static avifResult avifGetErrorForItemCategory(avifItemCategory itemCategory) |
| { |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (itemCategory == AVIF_ITEM_GAIN_MAP) { |
| return AVIF_RESULT_ENCODE_GAIN_MAP_FAILED; |
| } |
| #endif |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| if (itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM || |
| (itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY)) { |
| return AVIF_RESULT_ENCODE_SAMPLE_TRANSFORM_FAILED; |
| } |
| #endif |
| return avifIsAlpha(itemCategory) ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED; |
| } |
| |
| static uint32_t avifGridWidth(uint32_t gridCols, const avifImage * firstCell, const avifImage * bottomRightCell) |
| { |
| return (gridCols - 1) * firstCell->width + bottomRightCell->width; |
| } |
| |
| static uint32_t avifGridHeight(uint32_t gridRows, const avifImage * firstCell, const avifImage * bottomRightCell) |
| { |
| return (gridRows - 1) * firstCell->height + bottomRightCell->height; |
| } |
| |
| static avifResult avifValidateGrid(uint32_t gridCols, |
| uint32_t gridRows, |
| const avifImage * const * cellImages, |
| avifBool validateGainMap, |
| avifDiagnostics * diag) |
| { |
| const uint32_t cellCount = gridCols * gridRows; |
| const avifImage * firstCell = cellImages[0]; |
| const avifImage * bottomRightCell = cellImages[cellCount - 1]; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (validateGainMap) { |
| AVIF_ASSERT_OR_RETURN(firstCell->gainMap && firstCell->gainMap->image); |
| firstCell = firstCell->gainMap->image; |
| AVIF_ASSERT_OR_RETURN(bottomRightCell->gainMap && bottomRightCell->gainMap->image); |
| bottomRightCell = bottomRightCell->gainMap->image; |
| } |
| #endif |
| const uint32_t tileWidth = firstCell->width; |
| const uint32_t tileHeight = firstCell->height; |
| const uint32_t gridWidth = avifGridWidth(gridCols, firstCell, bottomRightCell); |
| const uint32_t gridHeight = avifGridHeight(gridRows, firstCell, bottomRightCell); |
| for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) { |
| const avifImage * cellImage = cellImages[cellIndex]; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (validateGainMap) { |
| AVIF_ASSERT_OR_RETURN(cellImage->gainMap && cellImage->gainMap->image); |
| cellImage = cellImage->gainMap->image; |
| } |
| #endif |
| const uint32_t expectedCellWidth = ((cellIndex + 1) % gridCols) ? tileWidth : bottomRightCell->width; |
| const uint32_t expectedCellHeight = (cellIndex < (cellCount - gridCols)) ? tileHeight : bottomRightCell->height; |
| if ((cellImage->width != expectedCellWidth) || (cellImage->height != expectedCellHeight)) { |
| avifDiagnosticsPrintf(diag, |
| "%s cell %u has invalid dimensions: expected %ux%u found %ux%u", |
| validateGainMap ? "gain map" : "image", |
| cellIndex, |
| expectedCellWidth, |
| expectedCellHeight, |
| cellImage->width, |
| cellImage->height); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| |
| // MIAF (ISO 23000-22:2019), Section 7.3.11.4.1: |
| // All input images of a grid image item shall use the same coding format, chroma sampling format, and the |
| // same decoder configuration (see 7.3.6.2). |
| if ((cellImage->depth != firstCell->depth) || (cellImage->yuvFormat != firstCell->yuvFormat) || |
| (cellImage->yuvRange != firstCell->yuvRange) || (cellImage->colorPrimaries != firstCell->colorPrimaries) || |
| (cellImage->transferCharacteristics != firstCell->transferCharacteristics) || |
| (cellImage->matrixCoefficients != firstCell->matrixCoefficients) || (!!cellImage->alphaPlane != !!firstCell->alphaPlane) || |
| (cellImage->alphaPremultiplied != firstCell->alphaPremultiplied)) { |
| avifDiagnosticsPrintf(diag, |
| "all grid cells should have the same value for: depth, yuvFormat, yuvRange, colorPrimaries, " |
| "transferCharacteristics, matrixCoefficients, alphaPlane presence, alphaPremultiplied"); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| |
| if (!cellImage->yuvPlanes[AVIF_CHAN_Y]) { |
| return AVIF_RESULT_NO_CONTENT; |
| } |
| } |
| |
| if ((bottomRightCell->width > tileWidth) || (bottomRightCell->height > tileHeight)) { |
| avifDiagnosticsPrintf(diag, |
| "the last %s cell can be smaller but not larger than the other cells which are %ux%u, found %ux%u", |
| validateGainMap ? "gain map" : "image", |
| tileWidth, |
| tileHeight, |
| bottomRightCell->width, |
| bottomRightCell->height); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| if ((cellCount > 1) && !avifAreGridDimensionsValid(firstCell->yuvFormat, gridWidth, gridHeight, tileWidth, tileHeight, diag)) { |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, |
| uint32_t gridCols, |
| uint32_t gridRows, |
| const avifImage * const * cellImages, |
| uint64_t durationInTimescales, |
| avifAddImageFlags addImageFlags) |
| { |
| // ----------------------------------------------------------------------- |
| // Verify encoding is possible |
| |
| if (!avifCodecName(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE)) { |
| return AVIF_RESULT_NO_CODEC_AVAILABLE; |
| } |
| |
| if (encoder->extraLayerCount >= AVIF_MAX_AV1_LAYER_COUNT) { |
| avifDiagnosticsPrintf(&encoder->diag, "extraLayerCount [%u] must be less than %d", encoder->extraLayerCount, AVIF_MAX_AV1_LAYER_COUNT); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| |
| // ----------------------------------------------------------------------- |
| // Validate images |
| |
| const uint32_t cellCount = gridCols * gridRows; |
| if (cellCount == 0) { |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| |
| const avifImage * firstCell = cellImages[0]; |
| const avifImage * bottomRightCell = cellImages[cellCount - 1]; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| AVIF_CHECKERR(firstCell->depth == 8 || firstCell->depth == 10 || firstCell->depth == 12 || |
| (firstCell->depth == 16 && encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE), |
| AVIF_RESULT_UNSUPPORTED_DEPTH); |
| #else |
| AVIF_CHECKERR(firstCell->depth == 8 || firstCell->depth == 10 || firstCell->depth == 12, AVIF_RESULT_UNSUPPORTED_DEPTH); |
| #endif |
| AVIF_CHECKERR(firstCell->yuvFormat != AVIF_PIXEL_FORMAT_NONE, AVIF_RESULT_NO_YUV_FORMAT_SELECTED); |
| if (!firstCell->width || !firstCell->height || !bottomRightCell->width || !bottomRightCell->height) { |
| return AVIF_RESULT_NO_CONTENT; |
| } |
| |
| AVIF_CHECKRES(avifValidateGrid(gridCols, gridRows, cellImages, /*validateGainMap=*/AVIF_FALSE, &encoder->diag)); |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| const avifBool hasGainMap = (firstCell->gainMap && firstCell->gainMap->image != NULL); |
| |
| // Check that either all cells have a gain map, or none of them do. |
| // If a gain map is present, check that they all have the same gain map metadata. |
| for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) { |
| const avifImage * cellImage = cellImages[cellIndex]; |
| const avifBool cellHasGainMap = (cellImage->gainMap && cellImage->gainMap->image); |
| if (cellHasGainMap != hasGainMap) { |
| avifDiagnosticsPrintf(&encoder->diag, "cells should either all have a gain map image, or none of them should, found a mix"); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| if (hasGainMap) { |
| const avifGainMap * firstGainMap = firstCell->gainMap; |
| const avifGainMap * cellGainMap = cellImage->gainMap; |
| if (cellGainMap->altICC.size != firstGainMap->altICC.size || |
| memcmp(cellGainMap->altICC.data, firstGainMap->altICC.data, cellGainMap->altICC.size) != 0 || |
| cellGainMap->altColorPrimaries != firstGainMap->altColorPrimaries || |
| cellGainMap->altTransferCharacteristics != firstGainMap->altTransferCharacteristics || |
| cellGainMap->altMatrixCoefficients != firstGainMap->altMatrixCoefficients || |
| cellGainMap->altYUVRange != firstGainMap->altYUVRange || cellGainMap->altDepth != firstGainMap->altDepth || |
| cellGainMap->altPlaneCount != firstGainMap->altPlaneCount || cellGainMap->altCLLI.maxCLL != firstGainMap->altCLLI.maxCLL || |
| cellGainMap->altCLLI.maxPALL != firstGainMap->altCLLI.maxPALL) { |
| avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same alternate image metadata in the gain map"); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| const avifGainMapMetadata * firstMetadata = &firstGainMap->metadata; |
| const avifGainMapMetadata * cellMetadata = &cellGainMap->metadata; |
| if (cellMetadata->baseHdrHeadroomN != firstMetadata->baseHdrHeadroomN || |
| cellMetadata->baseHdrHeadroomD != firstMetadata->baseHdrHeadroomD || |
| cellMetadata->alternateHdrHeadroomN != firstMetadata->alternateHdrHeadroomN || |
| cellMetadata->alternateHdrHeadroomD != firstMetadata->alternateHdrHeadroomD) { |
| avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same gain map metadata"); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| for (int c = 0; c < 3; ++c) { |
| if (cellMetadata->gainMapMinN[c] != firstMetadata->gainMapMinN[c] || |
| cellMetadata->gainMapMinD[c] != firstMetadata->gainMapMinD[c] || |
| cellMetadata->gainMapMaxN[c] != firstMetadata->gainMapMaxN[c] || |
| cellMetadata->gainMapMaxD[c] != firstMetadata->gainMapMaxD[c] || |
| cellMetadata->gainMapGammaN[c] != firstMetadata->gainMapGammaN[c] || |
| cellMetadata->gainMapGammaD[c] != firstMetadata->gainMapGammaD[c] || |
| cellMetadata->baseOffsetN[c] != firstMetadata->baseOffsetN[c] || |
| cellMetadata->baseOffsetD[c] != firstMetadata->baseOffsetD[c] || |
| cellMetadata->alternateOffsetN[c] != firstMetadata->alternateOffsetN[c] || |
| cellMetadata->alternateOffsetD[c] != firstMetadata->alternateOffsetD[c]) { |
| avifDiagnosticsPrintf(&encoder->diag, "all cells should have the same gain map metadata"); |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| } |
| } |
| } |
| |
| if (hasGainMap) { |
| // AVIF supports 16-bit images through sample transforms used as bit depth extensions, |
| // but this is not implemented for gain maps for now. Stick to at most 12 bits. |
| // TODO(yguyon): Implement 16-bit gain maps. |
| AVIF_CHECKERR(firstCell->gainMap->image->depth == 8 || firstCell->gainMap->image->depth == 10 || |
| firstCell->gainMap->image->depth == 12, |
| AVIF_RESULT_UNSUPPORTED_DEPTH); |
| AVIF_CHECKERR(firstCell->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_NONE, AVIF_RESULT_NO_YUV_FORMAT_SELECTED); |
| AVIF_CHECKRES(avifValidateGrid(gridCols, gridRows, cellImages, /*validateGainMap=*/AVIF_TRUE, &encoder->diag)); |
| if (firstCell->gainMap->image->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED || |
| firstCell->gainMap->image->transferCharacteristics != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) { |
| avifDiagnosticsPrintf(&encoder->diag, "the gain map image must have colorPrimaries = 2 and transferCharacteristics = 2"); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP |
| |
| // ----------------------------------------------------------------------- |
| // Validate flags |
| |
| if (encoder->data->singleImage) { |
| // The previous call to avifEncoderAddImage() set AVIF_ADD_IMAGE_FLAG_SINGLE. |
| // avifEncoderAddImage() cannot be called again for this encode. |
| return AVIF_RESULT_ENCODE_COLOR_FAILED; |
| } |
| |
| if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) { |
| encoder->data->singleImage = AVIF_TRUE; |
| |
| if (encoder->extraLayerCount > 0) { |
| // AVIF_ADD_IMAGE_FLAG_SINGLE may not be set for layered image. |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| |
| if (encoder->data->items.count > 0) { |
| // AVIF_ADD_IMAGE_FLAG_SINGLE may only be set on the first and only image. |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| } |
| |
| // ----------------------------------------------------------------------- |
| // Choose AV1 or AV2 |
| |
| const avifCodecType codecType = avifEncoderGetCodecType(encoder); |
| switch (codecType) { |
| case AVIF_CODEC_TYPE_AV1: |
| encoder->data->imageItemType = "av01"; |
| encoder->data->configPropName = "av1C"; |
| break; |
| #if defined(AVIF_CODEC_AVM) |
| case AVIF_CODEC_TYPE_AV2: |
| encoder->data->imageItemType = "av02"; |
| encoder->data->configPropName = "av2C"; |
| break; |
| #endif |
| default: |
| return AVIF_RESULT_NO_CODEC_AVAILABLE; |
| } |
| |
| // ----------------------------------------------------------------------- |
| // Map quality and qualityAlpha to quantizer and quantizerAlpha |
| encoder->data->quantizer = avifQualityToQuantizer(encoder->quality, encoder->minQuantizer, encoder->maxQuantizer); |
| encoder->data->quantizerAlpha = avifQualityToQuantizer(encoder->qualityAlpha, encoder->minQuantizerAlpha, encoder->maxQuantizerAlpha); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| encoder->data->quantizerGainMap = |
| avifQualityToQuantizer(encoder->qualityGainMap, AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY); |
| #endif |
| |
| // ----------------------------------------------------------------------- |
| // Handle automatic tiling |
| |
| encoder->data->tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6); |
| encoder->data->tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6); |
| if (encoder->autoTiling) { |
| // Use as many tiles as allowed by the minimum tile area requirement and impose a maximum |
| // of 8 tiles. |
| const int threads = 8; |
| avifSetTileConfiguration(threads, firstCell->width, firstCell->height, &encoder->data->tileRowsLog2, &encoder->data->tileColsLog2); |
| } |
| |
| // ----------------------------------------------------------------------- |
| // All encoder settings are known now. Detect changes. |
| |
| avifEncoderChanges encoderChanges; |
| if (!avifEncoderDetectChanges(encoder, &encoderChanges)) { |
| return AVIF_RESULT_CANNOT_CHANGE_SETTING; |
| } |
| avifEncoderBackupSettings(encoder); |
| |
| // ----------------------------------------------------------------------- |
| |
| if (durationInTimescales == 0) { |
| durationInTimescales = 1; |
| } |
| |
| if (encoder->data->items.count == 0) { |
| // Make a copy of the first image's metadata (sans pixels) for future writing/validation |
| AVIF_CHECKRES(avifImageCopy(encoder->data->imageMetadata, firstCell, 0)); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (hasGainMap) { |
| AVIF_CHECKRES(avifImageCopyAltImageMetadata(encoder->data->altImageMetadata, encoder->data->imageMetadata)); |
| } |
| #endif |
| |
| // Prepare all AV1 items |
| uint16_t colorItemID; |
| const uint32_t gridWidth = avifGridWidth(gridCols, firstCell, bottomRightCell); |
| const uint32_t gridHeight = avifGridHeight(gridRows, firstCell, bottomRightCell); |
| AVIF_CHECKRES(avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_COLOR, &colorItemID)); |
| encoder->data->primaryItemID = colorItemID; |
| |
| encoder->data->alphaPresent = (firstCell->alphaPlane != NULL); |
| if (encoder->data->alphaPresent && (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE)) { |
| // If encoding a single image in which the alpha plane exists but is entirely opaque, |
| // simply skip writing an alpha AV1 payload entirely, as it'll be interpreted as opaque |
| // and is less bytes. |
| // |
| // However, if encoding an image sequence, the first frame's alpha plane being entirely |
| // opaque could be a false positive for removing the alpha AV1 payload, as it might simply |
| // be a fade out later in the sequence. This is why avifImageIsOpaque() is only called |
| // when encoding a single image. |
| |
| encoder->data->alphaPresent = AVIF_FALSE; |
| for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) { |
| const avifImage * cellImage = cellImages[cellIndex]; |
| if (!avifImageIsOpaque(cellImage)) { |
| encoder->data->alphaPresent = AVIF_TRUE; |
| break; |
| } |
| } |
| } |
| |
| if (encoder->data->alphaPresent) { |
| uint16_t alphaItemID; |
| AVIF_CHECKRES(avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_ALPHA, &alphaItemID)); |
| avifEncoderItem * alphaItem = avifEncoderDataFindItemByID(encoder->data, alphaItemID); |
| AVIF_ASSERT_OR_RETURN(alphaItem); |
| alphaItem->irefType = "auxl"; |
| alphaItem->irefToID = colorItemID; |
| if (encoder->data->imageMetadata->alphaPremultiplied) { |
| avifEncoderItem * colorItem = avifEncoderDataFindItemByID(encoder->data, colorItemID); |
| AVIF_ASSERT_OR_RETURN(colorItem); |
| colorItem->irefType = "prem"; |
| colorItem->irefToID = alphaItemID; |
| } |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (firstCell->gainMap && firstCell->gainMap->image) { |
| avifEncoderItem * toneMappedItem = avifEncoderDataCreateItem(encoder->data, |
| "tmap", |
| infeNameGainMap, |
| /*infeNameSize=*/strlen(infeNameGainMap) + 1, |
| /*cellIndex=*/0); |
| if (!avifWriteToneMappedImagePayload(&toneMappedItem->metadataPayload, &firstCell->gainMap->metadata)) { |
| avifDiagnosticsPrintf(&encoder->diag, "failed to write gain map metadata, some values may be negative or too large"); |
| return AVIF_RESULT_ENCODE_GAIN_MAP_FAILED; |
| } |
| // Even though the 'tmap' item is related to the gain map, it represents a color image and its metadata is more similar to the color item. |
| toneMappedItem->itemCategory = AVIF_ITEM_COLOR; |
| uint16_t toneMappedItemID = toneMappedItem->id; |
| |
| AVIF_ASSERT_OR_RETURN(encoder->data->alternativeItemIDs.count == 0); |
| uint16_t * alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); |
| AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| *alternativeItemID = toneMappedItemID; |
| |
| alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); |
| AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| *alternativeItemID = colorItemID; |
| |
| const uint32_t gainMapGridWidth = |
| avifGridWidth(gridCols, cellImages[0]->gainMap->image, cellImages[gridCols * gridRows - 1]->gainMap->image); |
| const uint32_t gainMapGridHeight = |
| avifGridHeight(gridRows, cellImages[0]->gainMap->image, cellImages[gridCols * gridRows - 1]->gainMap->image); |
| |
| uint16_t gainMapItemID; |
| AVIF_CHECKRES( |
| avifEncoderAddImageItems(encoder, gridCols, gridRows, gainMapGridWidth, gainMapGridHeight, AVIF_ITEM_GAIN_MAP, &gainMapItemID)); |
| avifEncoderItem * gainMapItem = avifEncoderDataFindItemByID(encoder->data, gainMapItemID); |
| AVIF_ASSERT_OR_RETURN(gainMapItem); |
| gainMapItem->hiddenImage = AVIF_TRUE; |
| |
| // Set the color item and gain map item's dimgFromID value to point to the tone mapped item. |
| // The color item shall be first, and the gain map second. avifEncoderFinish() writes the |
| // dimg item references in item id order, so as long as colorItemID < gainMapItemID, the order |
| // will be correct. |
| AVIF_ASSERT_OR_RETURN(colorItemID < gainMapItemID); |
| avifEncoderItem * colorItem = avifEncoderDataFindItemByID(encoder->data, colorItemID); |
| AVIF_ASSERT_OR_RETURN(colorItem); |
| AVIF_ASSERT_OR_RETURN(colorItem->dimgFromID == 0); // Our internal API only allows one dimg value per item. |
| colorItem->dimgFromID = toneMappedItemID; |
| gainMapItem->dimgFromID = toneMappedItemID; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || |
| encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) { |
| // For now, only 16-bit depth is supported. |
| AVIF_ASSERT_OR_RETURN(firstCell->depth == 16); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| AVIF_CHECKERR(!firstCell->gainMap, AVIF_RESULT_NOT_IMPLEMENTED); // TODO(yguyon): Implement 16-bit HDR |
| #endif |
| AVIF_CHECKRES(avifEncoderCreateBitDepthExtensionItems(encoder, gridCols, gridRows, gridWidth, gridHeight, colorItemID)); |
| } else { |
| AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_NONE, AVIF_RESULT_NOT_IMPLEMENTED); |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM |
| |
| // ----------------------------------------------------------------------- |
| // Create metadata items (Exif, XMP) |
| |
| if (firstCell->exif.size > 0) { |
| const avifResult result = avifEncoderDataCreateExifItem(encoder->data, &firstCell->exif); |
| if (result != AVIF_RESULT_OK) { |
| return result; |
| } |
| } |
| |
| if (firstCell->xmp.size > 0) { |
| const avifResult result = avifEncoderDataCreateXMPItem(encoder->data, &firstCell->xmp); |
| if (result != AVIF_RESULT_OK) { |
| return result; |
| } |
| } |
| } else { |
| // Another frame in an image sequence, or layer in a layered image |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (hasGainMap) { |
| avifDiagnosticsPrintf(&encoder->diag, "gain maps are not supported for image sequences or layered images"); |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| #endif |
| |
| const avifImage * imageMetadata = encoder->data->imageMetadata; |
| // Image metadata that are copied to the configuration property and nclx boxes are not allowed to change. |
| // If the first image in the sequence had an alpha plane (even if fully opaque), all |
| // subsequent images must have alpha as well. |
| if ((imageMetadata->depth != firstCell->depth) || (imageMetadata->yuvFormat != firstCell->yuvFormat) || |
| (imageMetadata->yuvRange != firstCell->yuvRange) || |
| (imageMetadata->yuvChromaSamplePosition != firstCell->yuvChromaSamplePosition) || |
| (imageMetadata->colorPrimaries != firstCell->colorPrimaries) || |
| (imageMetadata->transferCharacteristics != firstCell->transferCharacteristics) || |
| (imageMetadata->matrixCoefficients != firstCell->matrixCoefficients) || |
| (imageMetadata->alphaPremultiplied != firstCell->alphaPremultiplied) || |
| (encoder->data->alphaPresent && !firstCell->alphaPlane)) { |
| return AVIF_RESULT_INCOMPATIBLE_IMAGE; |
| } |
| } |
| |
| if (encoder->data->frames.count == 1) { |
| // We will be writing an image sequence. When writing the AV1SampleEntry (derived from |
| // VisualSampleEntry) in the stsd box, we need to cast imageMetadata->width and |
| // imageMetadata->height to uint16_t: |
| // class VisualSampleEntry(codingname) extends SampleEntry (codingname){ |
| // ... |
| // unsigned int(16) width; |
| // unsigned int(16) height; |
| // ... |
| // } |
| // Check whether it is safe to cast width and height to uint16_t. The maximum width and |
| // height of an AV1 frame are 65536, which just exceeds uint16_t. |
| AVIF_ASSERT_OR_RETURN(encoder->data->items.count > 0); |
| const avifImage * imageMetadata = encoder->data->imageMetadata; |
| AVIF_CHECKERR(imageMetadata->width <= 65535 && imageMetadata->height <= 65535, AVIF_RESULT_INVALID_ARGUMENT); |
| } |
| |
| // ----------------------------------------------------------------------- |
| // Encode AV1 OBUs |
| |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| if (item->codec) { |
| const avifImage * cellImage = cellImages[item->cellIndex]; |
| avifImage * cellImagePlaceholder = NULL; // May be used as a temporary, modified cellImage. Left as NULL otherwise. |
| const avifImage * firstCellImage = firstCell; |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| if (item->itemCategory == AVIF_ITEM_GAIN_MAP) { |
| AVIF_ASSERT_OR_RETURN(cellImage->gainMap && cellImage->gainMap->image); |
| cellImage = cellImage->gainMap->image; |
| AVIF_ASSERT_OR_RETURN(firstCell->gainMap && firstCell->gainMap->image); |
| firstCellImage = firstCell->gainMap->image; |
| } |
| #endif |
| |
| if ((cellImage->width != firstCellImage->width) || (cellImage->height != firstCellImage->height)) { |
| // Pad the right-most and/or bottom-most tiles so that all tiles share the same dimensions. |
| cellImagePlaceholder = avifImageCreateEmpty(); |
| AVIF_CHECKERR(cellImagePlaceholder, AVIF_RESULT_OUT_OF_MEMORY); |
| const avifResult result = |
| avifImageCopyAndPad(cellImagePlaceholder, cellImage, firstCellImage->width, firstCellImage->height); |
| if (result != AVIF_RESULT_OK) { |
| avifImageDestroy(cellImagePlaceholder); |
| return result; |
| } |
| cellImage = cellImagePlaceholder; |
| } |
| |
| const avifBool isAlpha = avifIsAlpha(item->itemCategory); |
| int quantizer = isAlpha ? encoder->data->quantizerAlpha |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| : (item->itemCategory == AVIF_ITEM_GAIN_MAP) ? encoder->data->quantizerGainMap |
| #endif |
| : encoder->data->quantizer; |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| // Remember original quantizer values in case they change, to reset them afterwards. |
| int * encoderMinQuantizer = isAlpha ? &encoder->minQuantizerAlpha : &encoder->minQuantizer; |
| int * encoderMaxQuantizer = isAlpha ? &encoder->maxQuantizerAlpha : &encoder->maxQuantizer; |
| const int originalMinQuantizer = *encoderMinQuantizer; |
| const int originalMaxQuantizer = *encoderMaxQuantizer; |
| |
| if (encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE) { |
| if ((encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || |
| encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) && |
| (item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA)) { |
| // Encoding the least significant bits of a sample does not make any sense if the |
| // other bits are lossily compressed. Encode the most significant bits losslessly. |
| quantizer = AVIF_QUANTIZER_LOSSLESS; |
| *encoderMinQuantizer = AVIF_QUANTIZER_LOSSLESS; |
| *encoderMaxQuantizer = AVIF_QUANTIZER_LOSSLESS; |
| if (!avifEncoderDetectChanges(encoder, &encoderChanges)) { |
| assert(AVIF_FALSE); |
| } |
| } |
| |
| // Replace cellImage by the first or second input to the AVIF_ITEM_SAMPLE_TRANSFORM derived image item. |
| const avifBool itemWillBeEncodedLosslessly = (quantizer == AVIF_QUANTIZER_LOSSLESS); |
| avifImage * sampleTransformedImage = NULL; |
| if (cellImagePlaceholder) { |
| avifImageDestroy(cellImagePlaceholder); // Replaced by sampleTransformedImage. |
| cellImagePlaceholder = NULL; |
| } |
| AVIF_CHECKRES( |
| avifEncoderCreateBitDepthExtensionImage(encoder, item, itemWillBeEncodedLosslessly, cellImage, &sampleTransformedImage)); |
| cellImagePlaceholder = sampleTransformedImage; // Transfer ownership. |
| cellImage = cellImagePlaceholder; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM |
| |
| // If alpha channel is present, set disableLaggedOutput to AVIF_TRUE. If the encoder supports it, this enables |
| // avifEncoderDataShouldForceKeyframeForAlpha to force a keyframe in the alpha channel whenever a keyframe has been |
| // encoded in the color channel for animated images. |
| avifResult encodeResult = item->codec->encodeImage(item->codec, |
| encoder, |
| cellImage, |
| isAlpha, |
| encoder->data->tileRowsLog2, |
| encoder->data->tileColsLog2, |
| quantizer, |
| encoderChanges, |
| /*disableLaggedOutput=*/encoder->data->alphaPresent, |
| addImageFlags, |
| item->encodeOutput); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| // Revert quality settings if they changed. |
| if (*encoderMinQuantizer != originalMinQuantizer || *encoderMaxQuantizer != originalMaxQuantizer) { |
| avifEncoderBackupSettings(encoder); // Remember last encoding settings for next avifEncoderDetectChanges(). |
| *encoderMinQuantizer = originalMinQuantizer; |
| *encoderMaxQuantizer = originalMaxQuantizer; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM |
| if (cellImagePlaceholder) { |
| avifImageDestroy(cellImagePlaceholder); |
| } |
| if (encodeResult == AVIF_RESULT_UNKNOWN_ERROR) { |
| encodeResult = avifGetErrorForItemCategory(item->itemCategory); |
| } |
| AVIF_CHECKRES(encodeResult); |
| if (itemIndex == 0 && avifEncoderDataShouldForceKeyframeForAlpha(encoder->data, item, addImageFlags)) { |
| addImageFlags |= AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME; |
| } |
| } |
| } |
| |
| avifCodecSpecificOptionsClear(encoder->csOptions); |
| avifEncoderFrame * frame = (avifEncoderFrame *)avifArrayPush(&encoder->data->frames); |
| AVIF_CHECKERR(frame != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| frame->durationInTimescales = durationInTimescales; |
| return AVIF_RESULT_OK; |
| } |
| |
| avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags) |
| { |
| avifDiagnosticsClearError(&encoder->diag); |
| return avifEncoderAddImageInternal(encoder, 1, 1, &image, durationInTimescales, addImageFlags); |
| } |
| |
| avifResult avifEncoderAddImageGrid(avifEncoder * encoder, |
| uint32_t gridCols, |
| uint32_t gridRows, |
| const avifImage * const * cellImages, |
| avifAddImageFlags addImageFlags) |
| { |
| avifDiagnosticsClearError(&encoder->diag); |
| if ((gridCols == 0) || (gridCols > 256) || (gridRows == 0) || (gridRows > 256)) { |
| return AVIF_RESULT_INVALID_IMAGE_GRID; |
| } |
| if (encoder->extraLayerCount == 0) { |
| addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE; // image grids cannot be image sequences |
| } |
| return avifEncoderAddImageInternal(encoder, gridCols, gridRows, cellImages, 1, addImageFlags); |
| } |
| |
| static size_t avifEncoderFindExistingChunk(avifRWStream * s, size_t mdatStartOffset, const uint8_t * data, size_t size) |
| { |
| const size_t mdatCurrentOffset = avifRWStreamOffset(s); |
| const size_t mdatSearchSize = mdatCurrentOffset - mdatStartOffset; |
| if (mdatSearchSize < size) { |
| return 0; |
| } |
| const size_t mdatEndSearchOffset = mdatCurrentOffset - size; |
| for (size_t searchOffset = mdatStartOffset; searchOffset <= mdatEndSearchOffset; ++searchOffset) { |
| if (!memcmp(data, &s->raw->data[searchOffset], size)) { |
| return searchOffset; |
| } |
| } |
| return 0; |
| } |
| |
| static avifResult avifEncoderWriteMediaDataBox(avifEncoder * encoder, |
| avifRWStream * s, |
| avifEncoderItemReferenceArray * layeredColorItems, |
| avifEncoderItemReferenceArray * layeredAlphaItems) |
| { |
| encoder->ioStats.colorOBUSize = 0; |
| encoder->ioStats.alphaOBUSize = 0; |
| encoder->data->gainMapSizeBytes = 0; |
| |
| avifBoxMarker mdat; |
| AVIF_CHECKRES(avifRWStreamWriteBox(s, "mdat", AVIF_BOX_SIZE_TBD, &mdat)); |
| const size_t mdatStartOffset = avifRWStreamOffset(s); |
| for (uint32_t itemPasses = 0; itemPasses < 3; ++itemPasses) { |
| // Use multiple passes to pack in the following order: |
| // * Pass 0: metadata (Exif/XMP) |
| // * Pass 1: alpha, gain map (AV1) |
| // * Pass 2: all other item data (AV1 color) |
| // |
| // See here for the discussion on alpha coming before color: |
| // https://github.com/AOMediaCodec/libavif/issues/287 |
| // |
| // Exif and XMP are packed first as they're required to be fully available |
| // by avifDecoderParse() before it returns AVIF_RESULT_OK, unless ignoreXMP |
| // and ignoreExif are enabled. |
| // |
| const avifBool metadataPass = (itemPasses == 0); |
| const avifBool alphaAndGainMapPass = (itemPasses == 1); |
| |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| if ((item->metadataPayload.size == 0) && (item->encodeOutput->samples.count == 0)) { |
| // this item has nothing for the mdat box |
| continue; |
| } |
| const avifBool isMetadata = !memcmp(item->type, "mime", 4) || !memcmp(item->type, "Exif", 4); |
| if (metadataPass != isMetadata) { |
| // only process metadata (XMP/Exif) payloads when metadataPass is true |
| continue; |
| } |
| const avifBool isAlpha = avifIsAlpha(item->itemCategory); |
| const avifBool isAlphaOrGainMap = isAlpha |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| || item->itemCategory == AVIF_ITEM_GAIN_MAP |
| #endif |
| ; |
| if (alphaAndGainMapPass != isAlphaOrGainMap) { |
| // only process alpha payloads when alphaPass is true |
| continue; |
| } |
| |
| if ((encoder->extraLayerCount > 0) && (item->encodeOutput->samples.count > 0)) { |
| // Interleave - Pick out AV1 items and interleave them later. |
| // We always interleave all AV1 items for layered images. |
| AVIF_ASSERT_OR_RETURN(item->encodeOutput->samples.count == item->mdatFixups.count); |
| |
| avifEncoderItemReference * ref = |
| (avifEncoderItemReference *)avifArrayPush(isAlpha ? layeredAlphaItems : layeredColorItems); |
| AVIF_CHECKERR(ref != NULL, AVIF_RESULT_OUT_OF_MEMORY); |
| *ref = item; |
| continue; |
| } |
| |
| size_t chunkOffset = 0; |
| |
| // Deduplication - See if an identical chunk to this has already been written. |
| // Doing it when item->encodeOutput->samples.count > 1 would require contiguous memory. |
| if (item->encodeOutput->samples.count == 1) { |
| avifEncodeSample * sample = &item->encodeOutput->samples.sample[0]; |
| chunkOffset = avifEncoderFindExistingChunk(s, mdatStartOffset, sample->data.data, sample->data.size); |
| } else if (item->encodeOutput->samples.count == 0) { |
| chunkOffset = avifEncoderFindExistingChunk(s, mdatStartOffset, item->metadataPayload.data, item->metadataPayload.size); |
| } |
| |
| if (!chunkOffset) { |
| // We've never seen this chunk before; write it out |
| chunkOffset = avifRWStreamOffset(s); |
| if (item->encodeOutput->samples.count > 0) { |
| for (uint32_t sampleIndex = 0; sampleIndex < item->encodeOutput->samples.count; ++sampleIndex) { |
| avifEncodeSample * sample = &item->encodeOutput->samples.sample[sampleIndex]; |
| AVIF_CHECKRES(avifRWStreamWrite(s, sample->data.data, sample->data.size)); |
| |
| if (isAlpha) { |
| encoder->ioStats.alphaOBUSize += sample->data.size; |
| } else if (item->itemCategory == AVIF_ITEM_COLOR) { |
| encoder->ioStats.colorOBUSize += sample->data.size; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| } else if (item->itemCategory == AVIF_ITEM_GAIN_MAP) { |
| encoder->data->gainMapSizeBytes += sample->data.size; |
| #endif |
| } |
| } |
| } else { |
| AVIF_CHECKRES(avifRWStreamWrite(s, item->metadataPayload.data, item->metadataPayload.size)); |
| } |
| } |
| |
| for (uint32_t fixupIndex = 0; fixupIndex < item->mdatFixups.count; ++fixupIndex) { |
| avifOffsetFixup * fixup = &item->mdatFixups.fixup[fixupIndex]; |
| size_t prevOffset = avifRWStreamOffset(s); |
| avifRWStreamSetOffset(s, fixup->offset); |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, (uint32_t)chunkOffset)); |
| avifRWStreamSetOffset(s, prevOffset); |
| } |
| } |
| } |
| |
| uint32_t layeredItemCount = AVIF_MAX(layeredColorItems->count, layeredAlphaItems->count); |
| if (layeredItemCount > 0) { |
| // Interleave samples of all AV1 items. |
| // We first write the first layer of all items, |
| // in which we write first layer of each cell, |
| // in which we write alpha first and then color. |
| avifBool hasMoreSample; |
| uint32_t layerIndex = 0; |
| do { |
| hasMoreSample = AVIF_FALSE; |
| for (uint32_t itemIndex = 0; itemIndex < layeredItemCount; ++itemIndex) { |
| for (int samplePass = 0; samplePass < 2; ++samplePass) { |
| // Alpha coming before color |
| avifEncoderItemReferenceArray * currentItems = (samplePass == 0) ? layeredAlphaItems : layeredColorItems; |
| if (itemIndex >= currentItems->count) { |
| continue; |
| } |
| |
| // TODO: Offer the ability for a user to specify which grid cell should be written first. |
| avifEncoderItem * item = currentItems->ref[itemIndex]; |
| if (item->encodeOutput->samples.count <= layerIndex) { |
| // We've already written all samples of this item |
| continue; |
| } else if (item->encodeOutput->samples.count > layerIndex + 1) { |
| hasMoreSample = AVIF_TRUE; |
| } |
| avifRWData * data = &item->encodeOutput->samples.sample[layerIndex].data; |
| size_t chunkOffset = avifEncoderFindExistingChunk(s, mdatStartOffset, data->data, data->size); |
| if (!chunkOffset) { |
| // We've never seen this chunk before; write it out |
| chunkOffset = avifRWStreamOffset(s); |
| AVIF_CHECKRES(avifRWStreamWrite(s, data->data, data->size)); |
| if (samplePass == 0) { |
| encoder->ioStats.alphaOBUSize += data->size; |
| } else { |
| encoder->ioStats.colorOBUSize += data->size; |
| } |
| } |
| |
| size_t prevOffset = avifRWStreamOffset(s); |
| avifRWStreamSetOffset(s, item->mdatFixups.fixup[layerIndex].offset); |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, (uint32_t)chunkOffset)); |
| avifRWStreamSetOffset(s, prevOffset); |
| } |
| } |
| ++layerIndex; |
| } while (hasMoreSample); |
| |
| AVIF_ASSERT_OR_RETURN(layerIndex <= AVIF_MAX_AV1_LAYER_COUNT); |
| } |
| avifRWStreamFinishBox(s, mdat); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifWriteAltrGroup(avifRWStream * s, const avifEncoderItemIdArray * itemIDs) |
| { |
| avifBoxMarker grpl; |
| AVIF_CHECKRES(avifRWStreamWriteBox(s, "grpl", AVIF_BOX_SIZE_TBD, &grpl)); |
| |
| avifBoxMarker altr; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "altr", AVIF_BOX_SIZE_TBD, 0, 0, &altr)); |
| |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, 1)); // unsigned int(32) group_id; |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, (uint32_t)itemIDs->count)); // unsigned int(32) num_entities_in_group; |
| for (uint32_t i = 0; i < itemIDs->count; ++i) { |
| AVIF_CHECKRES(avifRWStreamWriteU32(s, (uint32_t)itemIDs->item_id[i])); // unsigned int(32) entity_id; |
| } |
| |
| avifRWStreamFinishBox(s, altr); |
| |
| avifRWStreamFinishBox(s, grpl); |
| |
| return AVIF_RESULT_OK; |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_METAV1) |
| // Returns true if the image can be encoded with a slim MetaBox with version 1 |
| // instead of a full regular MetaBox with version 0. |
| static avifBool avifEncoderIsMetaBoxV1Compatible(const avifEncoder * encoder) |
| { |
| // The MetaBox with version 1 ("mif3" brand) only supports non-layered, still images. |
| if (encoder->extraLayerCount || (encoder->data->frames.count != 1)) { |
| return AVIF_FALSE; |
| } |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| if (encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE) { |
| return AVIF_FALSE; |
| } |
| #endif |
| |
| if (encoder->data->imageMetadata->width > (1 << 15) || encoder->data->imageMetadata->height > (1 << 15)) { |
| return AVIF_FALSE; |
| } |
| if (encoder->data->imageMetadata->icc.size > (1 << 20) || encoder->data->imageMetadata->exif.size > (1 << 20) || |
| encoder->data->imageMetadata->xmp.size > (1 << 20)) { |
| return AVIF_FALSE; |
| } |
| |
| // 4:4:4, 4:2:2, 4:2:0 and 4:0:0 are supported by a MetaBox with version 1. |
| if (encoder->data->imageMetadata->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { |
| return AVIF_FALSE; |
| } |
| |
| if (encoder->data->imageMetadata->colorPrimaries > 255 || encoder->data->imageMetadata->transferCharacteristics > 255 || |
| encoder->data->imageMetadata->matrixCoefficients > 255) { |
| return AVIF_FALSE; |
| } |
| |
| const avifEncoderItem * colorItem = NULL; |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| |
| // Grids are not supported by a MetaBox with version 1. |
| if (item->gridCols || item->gridRows) { |
| return AVIF_FALSE; |
| } |
| |
| if (item->id == encoder->data->primaryItemID) { |
| assert(!colorItem); |
| colorItem = item; |
| // main_item_data_size_minus_one so 2^28 inclusive. |
| if (item->encodeOutput->samples.count != 1 || item->encodeOutput->samples.sample[0].data.size > (1 << 28)) { |
| return AVIF_FALSE; |
| } |
| continue; // The primary item can be stored in the MetaBox with version 1. |
| } |
| if (item->itemCategory == AVIF_ITEM_ALPHA && item->irefToID == encoder->data->primaryItemID) { |
| // alpha_item_data_size so 2^28 exclusive. |
| if (item->encodeOutput->samples.count != 1 || item->encodeOutput->samples.sample[0].data.size >= (1 << 28)) { |
| return AVIF_FALSE; |
| } |
| continue; // The alpha auxiliary item can be stored in the MetaBox with version 1. |
| } |
| if (!memcmp(item->type, "mime", 4) && !memcmp(item->infeName, "XMP", item->infeNameSize)) { |
| assert(item->metadataPayload.size == encoder->data->imageMetadata->xmp.size); |
| continue; // XMP metadata can be stored in the MetaBox with version 1. |
| } |
| if (!memcmp(item->type, "Exif", 4) && !memcmp(item->infeName, "Exif", item->infeNameSize)) { |
| assert(item->metadataPayload.size == encoder->data->imageMetadata->exif.size + 4); |
| const uint32_t exif_tiff_header_offset = *(uint32_t *)item->metadataPayload.data; |
| if (exif_tiff_header_offset != 0) { |
| return AVIF_FALSE; |
| } |
| continue; // Exif metadata can be stored in the MetaBox with version 1 if exif_tiff_header_offset is 0. |
| } |
| |
| // Items besides the colorItem, the alphaItem and Exif/XMP/ICC |
| // metadata are not directly supported by the MetaBox with version 1. |
| return AVIF_FALSE; |
| } |
| // A primary item is necessary. |
| if (!colorItem) { |
| return AVIF_FALSE; |
| } |
| return AVIF_TRUE; |
| } |
| |
| static avifResult avifEncoderWriteMetaBoxV1(avifEncoder * encoder, avifRWStream * s); |
| |
| static avifResult avifEncoderWriteFileTypeBoxAndMetaBoxV1(avifEncoder * encoder, avifRWData * output) |
| { |
| avifRWStream s; |
| avifRWStreamStart(&s, output); |
| |
| avifBoxMarker ftyp; |
| AVIF_CHECKRES(avifRWStreamWriteBox(&s, "ftyp", AVIF_BOX_SIZE_TBD, &ftyp)); |
| AVIF_CHECKRES(avifRWStreamWriteChars(&s, "mif3", 4)); // unsigned int(32) major_brand; |
| AVIF_CHECKRES(avifRWStreamWriteChars(&s, "avif", 4)); // unsigned int(32) minor_version; |
| // unsigned int(32) compatible_brands[]; |
| avifRWStreamFinishBox(&s, ftyp); |
| |
| AVIF_CHECKRES(avifEncoderWriteMetaBoxV1(encoder, &s)); |
| |
| avifRWStreamFinishWrite(&s); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifEncoderWriteMetaBoxV1(avifEncoder * encoder, avifRWStream * s) |
| { |
| const avifEncoderItem * colorItem = NULL; |
| const avifEncoderItem * alphaItem = NULL; |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| if (item->id == encoder->data->primaryItemID) { |
| AVIF_ASSERT_OR_RETURN(!colorItem); |
| colorItem = item; |
| } else if (item->itemCategory == AVIF_ITEM_ALPHA && item->irefToID == encoder->data->primaryItemID) { |
| AVIF_ASSERT_OR_RETURN(!alphaItem); |
| alphaItem = item; |
| } |
| } |
| |
| AVIF_ASSERT_OR_RETURN(colorItem); |
| const avifRWData * colorData = &colorItem->encodeOutput->samples.sample[0].data; |
| const avifRWData * alphaData = alphaItem ? &alphaItem->encodeOutput->samples.sample[0].data : NULL; |
| |
| const avifImage * const image = encoder->data->imageMetadata; |
| |
| const avifBool hasAlpha = alphaItem != NULL; |
| const avifBool alphaIsPremultiplied = encoder->data->imageMetadata->alphaPremultiplied; |
| const avifBool hasHdr = AVIF_FALSE; // Not implemented. |
| const avifBool hasIcc = image->icc.size != 0; |
| const uint32_t chromaSubsampling = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? 0 |
| : image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 ? 1 |
| : image->yuvFormat == AVIF_PIXEL_FORMAT_YUV422 ? 2 |
| : 3; |
| |
| const avifColorPrimaries defaultColorPrimaries = hasIcc ? AVIF_COLOR_PRIMARIES_UNSPECIFIED : AVIF_COLOR_PRIMARIES_BT709; |
| const avifTransferCharacteristics defaultTransferCharacteristics = hasIcc ? AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED |
| : AVIF_TRANSFER_CHARACTERISTICS_SRGB; |
| const avifMatrixCoefficients defaultMatrixCoefficients = chromaSubsampling == 0 ? AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED |
| : AVIF_MATRIX_COEFFICIENTS_BT601; |
| const avifBool hasExplicitCicp = image->colorPrimaries != defaultColorPrimaries || |
| image->transferCharacteristics != defaultTransferCharacteristics || |
| image->matrixCoefficients != defaultMatrixCoefficients; |
| |
| const avifBool fullRange = image->yuvRange == AVIF_RANGE_FULL; |
| const uint32_t pixelFormat = image->depth == 8 ? AVIF_METAV1_PIXEL_FORMAT_UINT8 |
| : image->depth == 10 ? AVIF_METAV1_PIXEL_FORMAT_UINT10 |
| : AVIF_METAV1_PIXEL_FORMAT_UINT12; |
| |
| // In AV1, the chroma_sample_position syntax element is not present for the YUV 4:2:2 format. |
| // Assume that AV1 uses the same 4:2:2 chroma sample location as HEVC and VVC (colocated). |
| if (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV420 && image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN) { |
| avifDiagnosticsPrintf(&encoder->diag, |
| "YUV chroma sample position %d is only supported with 4:2:0 YUV format in AV1", |
| image->yuvChromaSamplePosition); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| // For the YUV 4:2:0 format, assume centered sample position unless specified otherwise. |
| // This is consistent with the behavior in read.c. |
| const avifBool isHorizontallyCentered = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && |
| image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_VERTICAL && |
| image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; |
| const avifBool isVerticallyCentered = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && |
| image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; |
| |
| const uint32_t orientation = avifImageIrotImirToExifOrientation(image) - 1; |
| |
| const avifBool hasExplicitCodecTypes = AVIF_FALSE; // 'av01' and 'av1C' known from 'avif' minor_version field of FileTypeBox. |
| |
| const uint32_t dimensionSize = image->width <= (1 << 7) && image->height <= (1 << 7) ? 7 : 15; |
| const uint32_t codecConfigSize = 4; // 'av1C' always uses 4 bytes. |
| const uint32_t codecConfigSizeSize = codecConfigSize < (1 << 3) ? 3 : 12; |
| const uint32_t itemDataSizeSize = colorData->size <= (1 << 15) && (!alphaData || alphaData->size < (1 << 15)) ? 15 : 28; |
| const uint32_t metadataSizeSize = |
| image->icc.size <= (1 << 10) && image->exif.size <= (1 << 10) && image->xmp.size <= (1 << 10) ? 10 : 20; |
| |
| uint32_t flags = 0; |
| flags |= alphaItem ? 0x000001 : 0; // has_alpha |
| flags |= alphaIsPremultiplied ? 0x000002 : 0; // alpha_is_premultiplied |
| flags |= hasHdr ? 0x000004 : 0; // has_hdr |
| flags |= hasExplicitCicp ? 0x000008 : 0; // has_explicit_cicp |
| flags |= hasIcc ? 0x000010 : 0; // has_icc |
| flags |= image->exif.size ? 0x000020 : 0; // has_exif |
| flags |= image->xmp.size ? 0x000040 : 0; // has_xmp |
| flags |= fullRange ? 0x000080 : 0; // full_range |
| flags |= pixelFormat << 8; // pixel_format (4 bits) |
| flags |= chromaSubsampling << 12; // chroma_subsampling (2 bits) |
| flags |= isHorizontallyCentered ? 0x004000 : 0; // is_horizontally_centered |
| flags |= isVerticallyCentered ? 0x008000 : 0; // is_vertically_centered |
| flags |= orientation << 16; // orientation (3 bits) |
| flags |= hasExplicitCodecTypes ? 0x080000 : 0; // has_explicit_codec_types |
| flags |= dimensionSize == 7 ? 0x100000 : 0; // dimension_size |
| flags |= codecConfigSizeSize == 3 ? 0x200000 : 0; // codec_config_size_size |
| flags |= itemDataSizeSize == 15 ? 0x400000 : 0; // item_data_size_size |
| flags |= metadataSizeSize == 10 ? 0x800000 : 0; // metadata_size_size |
| |
| avifBoxMarker meta; |
| AVIF_CHECKRES(avifRWStreamWriteFullBox(s, "meta", AVIF_BOX_SIZE_TBD, /*version=*/1, flags, &meta)); |
| |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, image->width - 1, dimensionSize)); // unsigned int(dimension_size) width_minus_one; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, image->height - 1, dimensionSize)); // unsigned int(dimension_size) height_minus_one; |
| |
| if (hasIcc) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)image->icc.size - 1, metadataSizeSize)); // unsigned int(metadata_size_size) icc_data_size_minus_one; |
| } |
| |
| if (hasExplicitCicp) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, image->colorPrimaries, 8)); // bit(8) colour_primaries; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, image->transferCharacteristics, 8)); // bit(8) transfer_characteristics; |
| } |
| if (chromaSubsampling == 0) { |
| AVIF_CHECKERR(image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED, AVIF_RESULT_ENCODE_COLOR_FAILED); |
| } else if (hasExplicitCicp) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, image->matrixCoefficients, 8)); // bit(8) matrix_coefficients; |
| } |
| |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigSize, codecConfigSizeSize)); // unsigned int(codec_config_size_size) main_item_codec_config_size; |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)colorData->size - 1, itemDataSizeSize)); // unsigned int(item_data_size_size) main_item_data_size_minus_one; |
| |
| if (hasAlpha) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)alphaData->size, itemDataSizeSize)); // unsigned int(item_data_size_size) alpha_item_data_size; |
| if (alphaData->size != 0) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigSize, codecConfigSizeSize)); // unsigned int(codec_config_size_size) alpha_item_codec_config_size; |
| } |
| } |
| |
| if (hasHdr) { |
| // bit(1) has_gainmap; |
| // bit(1) has_clli; |
| // bit(1) has_mdcv; |
| // bit(1) has_cclv; |
| // bit(1) has_amve; |
| // bit(1) has_reve; |
| // bit(1) has_ndwt; |
| // if (has_clli) |
| // ContentLightLevel clli; |
| // if (has_mdcv) |
| // MasteringDisplayColourVolume mdcv; |
| // if (has_cclv) |
| // ContentColourVolume cclv; |
| // if (has_amve) |
| // AmbientViewingEnvironment amve; |
| // if (has_reve) |
| // ReferenceViewingEnvironment reve; |
| // if (has_ndwt) |
| // NominalDiffuseWhite ndwt; |
| |
| // if (has_gainmap) { |
| // unsigned int(metadata_size_size) gainmap_metadata_size; |
| // unsigned int(item_data_size_size) gainmap_item_data_size; |
| // unsigned int(codec_config_size_size) gainmap_item_codec_config_size; |
| // unsigned int(dimension_size) gainmap_width_minus_one; |
| // unsigned int(dimension_size) gainmap_height_minus_one; |
| // bit(8) gainmap_matrix_coefficients; |
| // bit(1) gainmap_full_range; |
| // bit(2) gainmap_chroma_subsampling; |
| // bit(1) gainmap_is_horizontally_centered; |
| // bit(1) gainmap_is_vertically_centered; |
| // bit(4) gainmap_pixel_format; |
| |
| // bit(1) tmap_has_clli; |
| // bit(1) tmap_has_mdcv; |
| // bit(1) tmap_has_cclv; |
| // bit(1) tmap_has_amve; |
| // bit(1) tmap_has_reve; |
| // bit(1) tmap_has_ndwt; |
| // if (tmap_has_clli) |
| // ContentLightLevel tmap_clli; |
| // if (tmap_has_mdcv) |
| // MasteringDisplayColourVolume tmap_mdcv; |
| // if (tmap_has_cclv) |
| // ContentColourVolume tmap_cclv; |
| // if (tmap_has_amve) |
| // AmbientViewingEnvironment tmap_amve; |
| // if (tmap_has_reve) |
| // ReferenceViewingEnvironment tmap_reve; |
| // if (tmap_has_ndwt) |
| // NominalDiffuseWhite tmap_ndwt; |
| |
| // bit(1) tmap_has_icc; |
| // if (tmap_has_icc) |
| // unsigned int(metadata_size_size) tmap_icc_data_size_minus_one; |
| // bit(1) tmap_has_explicit_cicp; |
| // if (tmap_has_explicit_cicp) { |
| // bit(8) tmap_colour_primaries; |
| // bit(8) tmap_transfer_characteristics; |
| // bit(8) tmap_matrix_coefficients; |
| // bit(1) tmap_full_range; |
| // } else { |
| // tmap_colour_primaries = 1; |
| // tmap_transfer_characteristics = 13; |
| // tmap_matrix_coefficients = 6; |
| // tmap_full_range = 1; |
| // } |
| // } |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| |
| if (image->exif.size) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)image->exif.size - 1, metadataSizeSize)); // unsigned int(metadata_size_size) exif_data_size_minus_one; |
| } |
| if (image->xmp.size) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, (uint32_t)image->xmp.size - 1, metadataSizeSize)); // unsigned int(metadata_size_size) xmp_data_size_minus_one; |
| } |
| |
| // Padding to align with whole bytes if necessary. |
| if (s->numUsedBitsInPartialByte != 0) { |
| AVIF_CHECKRES(avifRWStreamWriteBits(s, 0, 8 - s->numUsedBitsInPartialByte)); |
| } |
| const size_t headerSize = avifRWStreamOffset(s); |
| |
| // Codec configs |
| if (hasAlpha) { |
| AVIF_CHECKRES(writeCodecConfig(s, &alphaItem->av1C)); // unsigned int(8) alpha_item_codec_config[alpha_item_codec_config_size]; |
| } |
| // if (has_hdr && has_gainmap && gainmap_item_codec_config_size > 0) |
| // unsigned int(8) gainmap_item_codec_config[gainmap_item_codec_config_size]; |
| AVIF_CHECKRES(writeCodecConfig(s, &colorItem->av1C)); // unsigned int(8) main_item_codec_config[main_item_codec_config_size]; |
| |
| // Color profiles |
| if (image->icc.size) { |
| AVIF_CHECKRES(avifRWStreamWrite(s, image->icc.data, image->icc.size)); // unsigned int(8) icc_data[icc_data_size_minus_one + 1]; |
| } |
| // if (has_hdr && has_gainmap && tmap_has_icc) |
| // unsigned int(8) tmap_icc_data[tmap_icc_data_size_minus_one + 1]; |
| // if (has_hdr && has_gainmap && gainmap_metadata_size > 0) |
| // unsigned int(8) gainmap_metadata[gainmap_metadata_size]; |
| |
| // Encoded samples |
| if (hasAlpha) { |
| AVIF_CHECKRES(avifRWStreamWrite(s, alphaData->data, alphaData->size)); // unsigned int(8) alpha_item_data[alpha_item_data_size]; |
| } |
| // if (has_hdr && has_gainmap && gainmap_item_data_size > 0) |
| // unsigned int(8) gainmap_item_data[gainmap_item_data_size]; |
| AVIF_CHECKRES(avifRWStreamWrite(s, colorData->data, colorData->size)); // unsigned int(8) main_item_data[main_item_data_size_minus_one + 1]; |
| |
| // Metadata |
| if (image->exif.size) { |
| AVIF_CHECKRES(avifRWStreamWrite(s, image->exif.data, image->exif.size)); // unsigned int(8) exif_data[exif_data_size_minus_one + 1]; |
| } |
| if (image->xmp.size) { |
| AVIF_CHECKRES(avifRWStreamWrite(s, image->xmp.data, image->xmp.size)); // unsigned int(8) xmp_data[xmp_data_size_minus_one + 1]; |
| } |
| |
| AVIF_ASSERT_OR_RETURN(avifRWStreamOffset(s) - headerSize == (hasAlpha ? codecConfigSize : 0) + codecConfigSize + |
| image->icc.size + (hasAlpha ? alphaData->size : 0) + |
| colorData->size + image->exif.size + image->xmp.size); |
| avifRWStreamFinishBox(s, meta); |
| return AVIF_RESULT_OK; |
| } |
| #endif // AVIF_ENABLE_EXPERIMENTAL_METAV1 |
| |
| static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedup, |
| avifRWStream * const s, |
| const avifEncoder * const encoder, |
| const avifImage * const imageMetadata, |
| const avifImage * const altImageMetadata) |
| { |
| for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { |
| avifEncoderItem * item = &encoder->data->items.item[itemIndex]; |
| const avifBool isGrid = (item->gridCols > 0); |
| // Whether there is ipma to write for this item. |
| avifBool hasIpmaToWrite = item->codec || isGrid; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| const avifBool isToneMappedImage = !memcmp(item->type, "tmap", 4); |
| if (isToneMappedImage) { |
| hasIpmaToWrite = AVIF_TRUE; |
| } |
| #endif |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) |
| const avifBool isSampleTransformImage = !memcmp(item->type, "sato", 4); |
| if (isSampleTransformImage) { |
| hasIpmaToWrite = AVIF_TRUE; |
| } |
| #endif |
| memset(&item->ipma, 0, sizeof(item->ipma)); |
| |