diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7e84c48c..a0d8fb012b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ avifEncoder struct. minQuantizer, maxQuantizer, minQuantizerAlpha, and maxQuantizerAlpha initialized to the default values. * Add the public API function avifImageIsOpaque() in avif.h. +* Add experimental API for progressive AVIF encoding. ### Changed * Exif and XMP metadata is exported to PNG and JPEG files by default, diff --git a/include/avif/avif.h b/include/avif/avif.h index e4350d01dc..f8c9a73c14 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -99,6 +99,9 @@ typedef int avifBool; // to handle this case however they want. #define AVIF_REPETITION_COUNT_UNKNOWN -2 +// The number of spatial layers in AV1, with spatial_id = 0..3. +#define AVIF_MAX_AV1_LAYER_COUNT 4 + typedef enum avifPlanesFlag { AVIF_PLANES_YUV = (1 << 0), @@ -344,6 +347,15 @@ typedef struct avifDiagnostics AVIF_API void avifDiagnosticsClearError(avifDiagnostics * diag); +// --------------------------------------------------------------------------- +// Fraction utility + +typedef struct avifFraction +{ + int32_t n; + int32_t d; +} avifFraction; + // --------------------------------------------------------------------------- // Optional transformation structs @@ -1067,6 +1079,12 @@ AVIF_API avifResult avifDecoderNthImageMaxExtent(const avifDecoder * decoder, ui struct avifEncoderData; struct avifCodecSpecificOptions; +typedef struct avifScalingMode +{ + avifFraction horizontal; + avifFraction vertical; +} avifScalingMode; + // Notes: // * If avifEncoderWrite() returns AVIF_RESULT_OK, output must be freed with avifRWDataFree() // * If (maxThreads < 2), multithreading is disabled @@ -1087,6 +1105,8 @@ struct avifCodecSpecificOptions; // image in less bytes. AVIF_SPEED_DEFAULT means "Leave the AV1 codec to its default speed settings"./ // If avifEncoder uses rav1e, the speed value is directly passed through (0-10). If libaom is used, // a combination of settings are tweaked to simulate this speed range. +// * Extra layer count: [0 - (AVIF_MAX_AV1_LAYER_COUNT-1)]. Non-zero value indicates a layered +// (progressive) image. // * Some encoder settings can be changed after encoding starts. Changes will take effect in the next // call to avifEncoderAddImage(). typedef struct avifEncoder @@ -1097,12 +1117,13 @@ typedef struct avifEncoder // settings (see Notes above) int maxThreads; int speed; - int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default). - uint64_t timescale; // timescale of the media (Hz) - int repetitionCount; // Number of times the image sequence should be repeated. This can also be set to - // AVIF_REPETITION_COUNT_INFINITE for infinite repetitions. Only applicable for image sequences. - // Essentially, if repetitionCount is a non-negative integer `n`, then the image sequence should be - // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE. + int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default). + uint64_t timescale; // timescale of the media (Hz) + int repetitionCount; // Number of times the image sequence should be repeated. This can also be set to + // AVIF_REPETITION_COUNT_INFINITE for infinite repetitions. Only applicable for image sequences. + // Essentially, if repetitionCount is a non-negative integer `n`, then the image sequence should be + // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE. + uint32_t extraLayerCount; // EXPERIMENTAL: Non-zero value encodes layered image. // changeable encoder settings int quality; @@ -1114,6 +1135,7 @@ typedef struct avifEncoder int tileRowsLog2; int tileColsLog2; avifBool autoTiling; + avifScalingMode scalingMode; // stats from the most recent write avifIOStats ioStats; @@ -1137,26 +1159,36 @@ typedef enum avifAddImageFlag // Force this frame to be a keyframe (sync frame). AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME = (1 << 0), - // Use this flag when encoding a single image. Signals "still_picture" to AV1 encoders, which - // tweaks various compression rules. This is enabled automatically when using the - // avifEncoderWrite() single-image encode path. + // Use this flag when encoding a single frame, single layer image. + // Signals "still_picture" to AV1 encoders, which tweaks various compression rules. + // This is enabled automatically when using the avifEncoderWrite() single-image encode path. AVIF_ADD_IMAGE_FLAG_SINGLE = (1 << 1) } avifAddImageFlag; typedef uint32_t avifAddImageFlags; -// Multi-function alternative to avifEncoderWrite() for image sequences. +// Multi-function alternative to avifEncoderWrite() for advanced features. // // Usage / function call order is: // * avifEncoderCreate() -// * Set encoder->timescale (Hz) correctly -// * avifEncoderAddImage() ... [repeatedly; at least once] -// OR -// * avifEncoderAddImageGrid() [exactly once, AVIF_ADD_IMAGE_FLAG_SINGLE is assumed] +// - Still image: +// * avifEncoderAddImage() [exactly once] +// - Still image grid: +// * avifEncoderAddImageGrid() [exactly once, AVIF_ADD_IMAGE_FLAG_SINGLE is assumed] +// - Image sequence: +// * Set encoder->timescale (Hz) correctly +// * avifEncoderAddImage() ... [repeatedly; at least once] +// - Still layered image: +// * Set encoder->extraLayerCount correctly +// * avifEncoderAddImage() ... [exactly encoder->extraLayerCount+1 times] +// - Still layered grid: +// * Set encoder->extraLayerCount correctly +// * avifEncoderAddImageGrid() ... [exactly encoder->extraLayerCount+1 times] // * avifEncoderFinish() // * avifEncoderDestroy() // -// durationInTimescales is ignored if AVIF_ADD_IMAGE_FLAG_SINGLE is set in addImageFlags. +// durationInTimescales is ignored if AVIF_ADD_IMAGE_FLAG_SINGLE is set in addImageFlags, +// or if we are encoding a layered image. AVIF_API avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags); AVIF_API avifResult avifEncoderAddImageGrid(avifEncoder * encoder, uint32_t gridCols, diff --git a/include/avif/internal.h b/include/avif/internal.h index ae59f7931b..d8833e603f 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -72,6 +72,11 @@ void avifArrayPush(void * arrayStruct, void * element); void avifArrayPop(void * arrayStruct); void avifArrayDestroy(void * arrayStruct); +void avifFractionSimplify(avifFraction * f); +avifBool avifFractionCD(avifFraction * a, avifFraction * b); +avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result); +avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result); + void avifImageSetDefaults(avifImage * image); // Copies all fields that do not need to be freed/allocated from srcImage to dstImage. void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage); @@ -288,6 +293,7 @@ typedef enum avifEncoderChange AVIF_ENCODER_CHANGE_TILE_COLS_LOG2 = (1u << 5), AVIF_ENCODER_CHANGE_QUANTIZER = (1u << 6), AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA = (1u << 7), + AVIF_ENCODER_CHANGE_SCALING_MODE = (1u << 8), AVIF_ENCODER_CHANGE_CODEC_SPECIFIC = (1u << 31) } avifEncoderChange; diff --git a/src/avif.c b/src/avif.c index 62eba20792..2aff76fda9 100644 --- a/src/avif.c +++ b/src/avif.c @@ -563,15 +563,9 @@ void avifRGBImageFreePixels(avifRGBImage * rgb) // --------------------------------------------------------------------------- // avifCropRect -typedef struct clapFraction +static avifFraction calcCenter(int32_t dim) { - int32_t n; - int32_t d; -} clapFraction; - -static clapFraction calcCenter(int32_t dim) -{ - clapFraction f; + avifFraction f; f.n = dim >> 1; f.d = 1; if ((dim % 2) != 0) { @@ -581,95 +575,11 @@ static clapFraction calcCenter(int32_t dim) return f; } -// |a| and |b| hold int32_t values. The int64_t type is used so that we can negate INT32_MIN without -// overflowing int32_t. -static int64_t calcGCD(int64_t a, int64_t b) -{ - if (a < 0) { - a *= -1; - } - if (b < 0) { - b *= -1; - } - while (b != 0) { - int64_t r = a % b; - a = b; - b = r; - } - return a; -} - -static void clapFractionSimplify(clapFraction * f) -{ - int64_t gcd = calcGCD(f->n, f->d); - if (gcd > 1) { - f->n = (int32_t)(f->n / gcd); - f->d = (int32_t)(f->d / gcd); - } -} - static avifBool overflowsInt32(int64_t x) { return (x < INT32_MIN) || (x > INT32_MAX); } -// Make the fractions have a common denominator -static avifBool clapFractionCD(clapFraction * a, clapFraction * b) -{ - clapFractionSimplify(a); - clapFractionSimplify(b); - if (a->d != b->d) { - const int64_t ad = a->d; - const int64_t bd = b->d; - const int64_t anNew = a->n * bd; - const int64_t adNew = a->d * bd; - const int64_t bnNew = b->n * ad; - const int64_t bdNew = b->d * ad; - if (overflowsInt32(anNew) || overflowsInt32(adNew) || overflowsInt32(bnNew) || overflowsInt32(bdNew)) { - return AVIF_FALSE; - } - a->n = (int32_t)anNew; - a->d = (int32_t)adNew; - b->n = (int32_t)bnNew; - b->d = (int32_t)bdNew; - } - return AVIF_TRUE; -} - -static avifBool clapFractionAdd(clapFraction a, clapFraction b, clapFraction * result) -{ - if (!clapFractionCD(&a, &b)) { - return AVIF_FALSE; - } - - const int64_t resultN = (int64_t)a.n + b.n; - if (overflowsInt32(resultN)) { - return AVIF_FALSE; - } - result->n = (int32_t)resultN; - result->d = a.d; - - clapFractionSimplify(result); - return AVIF_TRUE; -} - -static avifBool clapFractionSub(clapFraction a, clapFraction b, clapFraction * result) -{ - if (!clapFractionCD(&a, &b)) { - return AVIF_FALSE; - } - - const int64_t resultN = (int64_t)a.n - b.n; - if (overflowsInt32(resultN)) { - return AVIF_FALSE; - } - result->n = (int32_t)resultN; - result->d = a.d; - - clapFractionSimplify(result); - return AVIF_TRUE; -} - static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imageW, uint32_t imageH, avifPixelFormat yuvFormat, avifDiagnostics * diag) { @@ -755,32 +665,32 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect, avifDiagnosticsPrintf(diag, "[Strict] image width %u or height %u is greater than INT32_MAX", imageW, imageH); return AVIF_FALSE; } - clapFraction uncroppedCenterX = calcCenter((int32_t)imageW); - clapFraction uncroppedCenterY = calcCenter((int32_t)imageH); + avifFraction uncroppedCenterX = calcCenter((int32_t)imageW); + avifFraction uncroppedCenterY = calcCenter((int32_t)imageH); - clapFraction horizOff; + avifFraction horizOff; horizOff.n = horizOffN; horizOff.d = horizOffD; - clapFraction croppedCenterX; - if (!clapFractionAdd(uncroppedCenterX, horizOff, &croppedCenterX)) { + avifFraction croppedCenterX; + if (!avifFractionAdd(uncroppedCenterX, horizOff, &croppedCenterX)) { avifDiagnosticsPrintf(diag, "[Strict] croppedCenterX overflowed"); return AVIF_FALSE; } - clapFraction vertOff; + avifFraction vertOff; vertOff.n = vertOffN; vertOff.d = vertOffD; - clapFraction croppedCenterY; - if (!clapFractionAdd(uncroppedCenterY, vertOff, &croppedCenterY)) { + avifFraction croppedCenterY; + if (!avifFractionAdd(uncroppedCenterY, vertOff, &croppedCenterY)) { avifDiagnosticsPrintf(diag, "[Strict] croppedCenterY overflowed"); return AVIF_FALSE; } - clapFraction halfW; + avifFraction halfW; halfW.n = clapW; halfW.d = 2; - clapFraction cropX; - if (!clapFractionSub(croppedCenterX, halfW, &cropX)) { + avifFraction cropX; + if (!avifFractionSub(croppedCenterX, halfW, &cropX)) { avifDiagnosticsPrintf(diag, "[Strict] cropX overflowed"); return AVIF_FALSE; } @@ -789,11 +699,11 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect, return AVIF_FALSE; } - clapFraction halfH; + avifFraction halfH; halfH.n = clapH; halfH.d = 2; - clapFraction cropY; - if (!clapFractionSub(croppedCenterY, halfH, &cropY)) { + avifFraction cropY; + if (!avifFractionSub(croppedCenterY, halfH, &cropY)) { avifDiagnosticsPrintf(diag, "[Strict] cropY overflowed"); return AVIF_FALSE; } @@ -831,8 +741,8 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, avifDiagnosticsPrintf(diag, "[Strict] image width %u or height %u is greater than INT32_MAX", imageW, imageH); return AVIF_FALSE; } - clapFraction uncroppedCenterX = calcCenter((int32_t)imageW); - clapFraction uncroppedCenterY = calcCenter((int32_t)imageH); + avifFraction uncroppedCenterX = calcCenter((int32_t)imageW); + avifFraction uncroppedCenterY = calcCenter((int32_t)imageH); if ((cropRect->width > INT32_MAX) || (cropRect->height > INT32_MAX)) { avifDiagnosticsPrintf(diag, @@ -841,14 +751,14 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, cropRect->height); return AVIF_FALSE; } - clapFraction croppedCenterX = calcCenter((int32_t)cropRect->width); + avifFraction croppedCenterX = calcCenter((int32_t)cropRect->width); const int64_t croppedCenterXN = croppedCenterX.n + (int64_t)cropRect->x * croppedCenterX.d; if (overflowsInt32(croppedCenterXN)) { avifDiagnosticsPrintf(diag, "[Strict] croppedCenterX overflowed"); return AVIF_FALSE; } croppedCenterX.n = (int32_t)croppedCenterXN; - clapFraction croppedCenterY = calcCenter((int32_t)cropRect->height); + avifFraction croppedCenterY = calcCenter((int32_t)cropRect->height); const int64_t croppedCenterYN = croppedCenterY.n + (int64_t)cropRect->y * croppedCenterY.d; if (overflowsInt32(croppedCenterYN)) { avifDiagnosticsPrintf(diag, "[Strict] croppedCenterY overflowed"); @@ -856,13 +766,13 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, } croppedCenterY.n = (int32_t)croppedCenterYN; - clapFraction horizOff; - if (!clapFractionSub(croppedCenterX, uncroppedCenterX, &horizOff)) { + avifFraction horizOff; + if (!avifFractionSub(croppedCenterX, uncroppedCenterX, &horizOff)) { avifDiagnosticsPrintf(diag, "[Strict] horizOff overflowed"); return AVIF_FALSE; } - clapFraction vertOff; - if (!clapFractionSub(croppedCenterY, uncroppedCenterY, &vertOff)) { + avifFraction vertOff; + if (!avifFractionSub(croppedCenterY, uncroppedCenterY, &vertOff)) { avifDiagnosticsPrintf(diag, "[Strict] vertOff overflowed"); return AVIF_FALSE; } diff --git a/src/codec_aom.c b/src/codec_aom.c index 586f1e5c34..dde3ad4a57 100644 --- a/src/codec_aom.c +++ b/src/codec_aom.c @@ -67,6 +67,7 @@ struct avifCodecInternal // Whether 'tuning' (of the specified distortion metric) was set with an // avifEncoderSetCodecSpecificOption(encoder, "tune", value) call. avifBool tuningSet; + uint32_t currentLayer; #endif }; @@ -507,6 +508,33 @@ static avifBool avifProcessAOMOptionsPostInit(avifCodec * codec, avifBool alpha) return AVIF_TRUE; } +struct aomScalingModeMapList +{ + avifFraction avifMode; + AOM_SCALING_MODE aomMode; +}; + +static const struct aomScalingModeMapList scalingModeMap[] = { + { { 1, 1 }, AOME_NORMAL }, { { 1, 2 }, AOME_ONETWO }, { { 1, 4 }, AOME_ONEFOUR }, { { 1, 8 }, AOME_ONEEIGHT }, + { { 3, 4 }, AOME_THREEFOUR }, { { 3, 5 }, AOME_THREEFIVE }, { { 4, 5 }, AOME_FOURFIVE }, +}; + +static const int scalingModeMapSize = sizeof(scalingModeMap) / sizeof(scalingModeMap[0]); + +static avifBool avifFindAOMScalingMode(const avifFraction * avifMode, AOM_SCALING_MODE * aomMode) +{ + avifFraction simplifiedFraction = *avifMode; + avifFractionSimplify(&simplifiedFraction); + for (int i = 0; i < scalingModeMapSize; ++i) { + if (scalingModeMap[i].avifMode.n == simplifiedFraction.n && scalingModeMap[i].avifMode.d == simplifiedFraction.d) { + *aomMode = scalingModeMap[i].aomMode; + return AVIF_TRUE; + } + } + + return AVIF_FALSE; +} + static avifBool aomCodecEncodeFinish(avifCodec * codec, avifCodecEncodeOutput * output); static avifResult aomCodecEncodeImage(avifCodec * codec, @@ -523,6 +551,11 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, struct aom_codec_enc_cfg * cfg = &codec->internal->cfg; avifBool quantizerUpdated = AVIF_FALSE; + // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AOM + // encoder, config should be applied for each frame, so we don't care about changes on these + // two fields. + encoderChanges &= ~AVIF_ENCODER_CHANGE_SCALING_MODE; + if (!codec->internal->encoderInitialized) { // Map encoder speed to AOM usage + CpuUsed: // Speed 0: GoodQuality CpuUsed 0 @@ -673,6 +706,11 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, // Tell libaom that all frames will be key frames. cfg->kf_max_dist = 0; } + if (encoder->extraLayerCount > 0) { + // For layered image, disable lagged encoding to always get output + // frame for each input frame. + cfg->g_lag_in_frames = 0; + } if (encoder->maxThreads > 1) { cfg->g_threads = encoder->maxThreads; } @@ -752,6 +790,12 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, if (tileColsLog2 != 0) { aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2); } + if (encoder->extraLayerCount > 0) { + int layerCount = encoder->extraLayerCount + 1; + if (aom_codec_control(&codec->internal->encoder, AOME_SET_NUMBER_SPATIAL_LAYERS, layerCount) != AOM_CODEC_OK) { + return AVIF_RESULT_UNKNOWN_ERROR; + }; + } if (aomCpuUsed != -1) { if (aom_codec_control(&codec->internal->encoder, AOME_SET_CPUUSED, aomCpuUsed) != AOM_CODEC_OK) { return AVIF_RESULT_UNKNOWN_ERROR; @@ -838,6 +882,30 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, } } + if (codec->internal->currentLayer > encoder->extraLayerCount) { + avifDiagnosticsPrintf(codec->diag, + "Too many layers sent. Expected %u layers, but got %u layers.", + encoder->extraLayerCount + 1, + codec->internal->currentLayer + 1); + return AVIF_RESULT_INVALID_ARGUMENT; + } + if (encoder->extraLayerCount > 0) { + aom_codec_control(&codec->internal->encoder, AOME_SET_SPATIAL_LAYER_ID, codec->internal->currentLayer); + ++codec->internal->currentLayer; + } + + aom_scaling_mode_t aomScalingMode; + if (!avifFindAOMScalingMode(&encoder->scalingMode.horizontal, &aomScalingMode.h_scaling_mode)) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + if (!avifFindAOMScalingMode(&encoder->scalingMode.vertical, &aomScalingMode.v_scaling_mode)) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + if ((aomScalingMode.h_scaling_mode != AOME_NORMAL) || (aomScalingMode.v_scaling_mode != AOME_NORMAL)) { + // AOME_SET_SCALEMODE only applies to next frame (layer), so we have to set it every time. + aom_codec_control(&codec->internal->encoder, AOME_SET_SCALEMODE, &aomScalingMode); + } + aom_image_t aomImage; // We prefer to simply set the aomImage.planes[] pointers to the plane buffers in 'image'. When // doing this, we set aomImage.w equal to aomImage.d_w and aomImage.h equal to aomImage.d_h and @@ -988,6 +1056,10 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, if (addImageFlags & AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME) { encodeFlags |= AOM_EFLAG_FORCE_KF; } + if (codec->internal->currentLayer > 0) { + encodeFlags |= AOM_EFLAG_NO_REF_GF | AOM_EFLAG_NO_REF_ARF | AOM_EFLAG_NO_REF_BWD | AOM_EFLAG_NO_REF_ARF2 | + AOM_EFLAG_NO_UPD_GF | AOM_EFLAG_NO_UPD_ARF; + } aom_codec_err_t encodeErr = aom_codec_encode(&codec->internal->encoder, &aomImage, 0, 1, encodeFlags); avifFree(monoUVPlane); if (aomImageAllocated) { @@ -1012,8 +1084,10 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, } } - if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) { - // Flush and clean up encoder resources early to save on overhead when encoding alpha or grid images + if ((addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) || + ((encoder->extraLayerCount > 0) && (encoder->extraLayerCount + 1 == codec->internal->currentLayer))) { + // Flush and clean up encoder resources early to save on overhead when encoding alpha or grid images, + // as encoding is finished now. For layered image, encoding finishes when the last layer is encoded. if (!aomCodecEncodeFinish(codec, output)) { return AVIF_RESULT_UNKNOWN_ERROR; diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c index 4b93b79cbd..534dd5d024 100644 --- a/src/codec_rav1e.c +++ b/src/codec_rav1e.c @@ -73,6 +73,11 @@ static avifResult rav1eCodecEncodeImage(avifCodec * codec, return AVIF_RESULT_NOT_IMPLEMENTED; } + // rav1e does not support encoding layered image. + if (encoder->extraLayerCount > 0) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + avifResult result = AVIF_RESULT_UNKNOWN_ERROR; RaConfig * rav1eConfig = NULL; diff --git a/src/codec_svt.c b/src/codec_svt.c index 158b2a4051..4e13450a07 100644 --- a/src/codec_svt.c +++ b/src/codec_svt.c @@ -65,6 +65,11 @@ static avifResult svtCodecEncodeImage(avifCodec * codec, } } + // SVT-AV1 does not support encoding layered image. + if (encoder->extraLayerCount > 0) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + avifResult result = AVIF_RESULT_UNKNOWN_ERROR; EbColorFormat color_format = EB_YUV420; EbBufferHeaderType * input_buffer = NULL; diff --git a/src/read.c b/src/read.c index e5b569c2c5..0fafd8fd4a 100644 --- a/src/read.c +++ b/src/read.c @@ -37,8 +37,6 @@ static const size_t xmpContentTypeSize = sizeof(xmpContentType); // can't be more than 4 unique tuples right now. #define MAX_IPMA_VERSION_AND_FLAGS_SEEN 4 -#define MAX_AV1_LAYER_COUNT 4 - // --------------------------------------------------------------------------- // Box data structures @@ -577,7 +575,7 @@ static avifBool avifCodecDecodeInputFillFromDecoderItem(avifCodecDecodeInput * d sample->itemID = item->id; sample->offset = 0; sample->size = sampleSize; - assert(lselProp->u.lsel.layerID < MAX_AV1_LAYER_COUNT); + assert(lselProp->u.lsel.layerID < AVIF_MAX_AV1_LAYER_COUNT); sample->spatialID = (uint8_t)lselProp->u.lsel.layerID; sample->sync = AVIF_TRUE; } else if (allowProgressive && item->progressive) { @@ -1899,7 +1897,7 @@ static avifBool avifParseLayerSelectorProperty(avifProperty * prop, const uint8_ avifLayerSelectorProperty * lsel = &prop->u.lsel; AVIF_CHECK(avifROStreamReadU16(&s, &lsel->layerID)); - if ((lsel->layerID != 0xFFFF) && (lsel->layerID >= MAX_AV1_LAYER_COUNT)) { + if ((lsel->layerID != 0xFFFF) && (lsel->layerID >= AVIF_MAX_AV1_LAYER_COUNT)) { avifDiagnosticsPrintf(diag, "Box[lsel] contains an unsupported layer [%u]", lsel->layerID); return AVIF_FALSE; } diff --git a/src/utils.c b/src/utils.c index 697cb0eb86..5fc1cb68af 100644 --- a/src/utils.c +++ b/src/utils.c @@ -146,3 +146,92 @@ void avifArrayDestroy(void * arrayStruct) } memset(arr, 0, sizeof(avifArrayInternal)); } + +// |a| and |b| hold int32_t values. The int64_t type is used so that we can negate INT32_MIN without +// overflowing int32_t. +static int64_t calcGCD(int64_t a, int64_t b) +{ + if (a < 0) { + a *= -1; + } + if (b < 0) { + b *= -1; + } + while (b != 0) { + int64_t r = a % b; + a = b; + b = r; + } + return a; +} + +void avifFractionSimplify(avifFraction * f) +{ + int64_t gcd = calcGCD(f->n, f->d); + if (gcd > 1) { + f->n = (int32_t)(f->n / gcd); + f->d = (int32_t)(f->d / gcd); + } +} + +static avifBool overflowsInt32(int64_t x) +{ + return (x < INT32_MIN) || (x > INT32_MAX); +} + +// Make the fractions have a common denominator +avifBool avifFractionCD(avifFraction * a, avifFraction * b) +{ + avifFractionSimplify(a); + avifFractionSimplify(b); + if (a->d != b->d) { + const int64_t ad = a->d; + const int64_t bd = b->d; + const int64_t anNew = a->n * bd; + const int64_t adNew = a->d * bd; + const int64_t bnNew = b->n * ad; + const int64_t bdNew = b->d * ad; + if (overflowsInt32(anNew) || overflowsInt32(adNew) || overflowsInt32(bnNew) || overflowsInt32(bdNew)) { + return AVIF_FALSE; + } + a->n = (int32_t)anNew; + a->d = (int32_t)adNew; + b->n = (int32_t)bnNew; + b->d = (int32_t)bdNew; + } + return AVIF_TRUE; +} + +avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result) +{ + if (!avifFractionCD(&a, &b)) { + return AVIF_FALSE; + } + + const int64_t resultN = (int64_t)a.n + b.n; + if (overflowsInt32(resultN)) { + return AVIF_FALSE; + } + result->n = (int32_t)resultN; + result->d = a.d; + + avifFractionSimplify(result); + return AVIF_TRUE; +} + +avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result) +{ + if (!avifFractionCD(&a, &b)) { + return AVIF_FALSE; + } + + const int64_t resultN = (int64_t)a.n - b.n; + if (overflowsInt32(resultN)) { + return AVIF_FALSE; + } + result->n = (int32_t)resultN; + result->d = a.d; + + avifFractionSimplify(result); + return AVIF_TRUE; +} diff --git a/src/write.c b/src/write.c index ff9117ccc5..649183e798 100644 --- a/src/write.c +++ b/src/write.c @@ -181,12 +181,21 @@ typedef struct avifEncoderItem 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 @@ -383,6 +392,8 @@ static uint8_t avifItemPropertyDedupFinish(avifItemPropertyDedup * dedup, avifRW // --------------------------------------------------------------------------- +static const avifScalingMode noScaling = { { 1, 1 }, { 1, 1 } }; + avifEncoder * avifEncoderCreate(void) { avifEncoder * encoder = (avifEncoder *)avifAlloc(sizeof(avifEncoder)); @@ -401,6 +412,7 @@ avifEncoder * avifEncoderCreate(void) encoder->tileRowsLog2 = 0; encoder->tileColsLog2 = 0; encoder->autoTiling = AVIF_FALSE; + encoder->scalingMode = noScaling; encoder->data = avifEncoderDataCreate(); encoder->csOptions = avifCodecSpecificOptionsCreate(); return encoder; @@ -431,6 +443,7 @@ static void avifEncoderBackupSettings(avifEncoder * encoder) 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; @@ -439,6 +452,7 @@ static void avifEncoderBackupSettings(avifEncoder * encoder) encoder->data->lastQuantizerAlpha = encoder->data->quantizerAlpha; encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2; encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2; + lastEncoder->scalingMode = encoder->scalingMode; } // This function detects changes made on avifEncoder. It returns true on success (i.e., if every @@ -456,7 +470,8 @@ static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncode 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->timescale != encoder->timescale) || (lastEncoder->repetitionCount != encoder->repetitionCount) || + (lastEncoder->extraLayerCount != encoder->extraLayerCount)) { return AVIF_FALSE; } @@ -484,6 +499,9 @@ static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncode 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; } @@ -858,6 +876,10 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, return AVIF_RESULT_NO_CODEC_AVAILABLE; } + if (encoder->extraLayerCount >= AVIF_MAX_AV1_LAYER_COUNT) { + return AVIF_RESULT_INVALID_ARGUMENT; + } + // ----------------------------------------------------------------------- // Validate images @@ -940,6 +962,11 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, 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; @@ -1009,6 +1036,7 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, } item->codec->csOptions = encoder->csOptions; item->codec->diag = &encoder->diag; + item->extraLayerCount = encoder->extraLayerCount; if (cellCount > 1) { item->dimgFromID = gridColorID; @@ -1070,6 +1098,7 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, item->codec->csOptions = encoder->csOptions; item->codec->diag = &encoder->diag; item->alpha = AVIF_TRUE; + item->extraLayerCount = encoder->extraLayerCount; if (cellCount > 1) { item->dimgFromID = gridAlphaID; @@ -1105,7 +1134,7 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, } } } else { - // Another frame in an image sequence + // Another frame in an image sequence, or layer in a layered image const avifImage * imageMetadata = encoder->data->imageMetadata; // If the first image in the sequence had an alpha plane (even if fully opaque), all @@ -1203,7 +1232,10 @@ avifResult avifEncoderAddImageGrid(avifEncoder * encoder, if ((gridCols == 0) || (gridCols > 256) || (gridRows == 0) || (gridRows > 256)) { return AVIF_RESULT_INVALID_IMAGE_GRID; } - return avifEncoderAddImageInternal(encoder, gridCols, gridRows, cellImages, 1, addImageFlags | AVIF_ADD_IMAGE_FLAG_SINGLE); // image grids cannot be image sequences + 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) @@ -1242,6 +1274,15 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) if (item->encodeOutput->samples.count != encoder->data->frames.count) { return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED; } + + if ((item->extraLayerCount > 0) && (item->encodeOutput->samples.count != item->extraLayerCount + 1)) { + // Check whether user has sent enough frames to encoder. + avifDiagnosticsPrintf(&encoder->diag, + "Expected %u frames given to avifEncoderAddImage() to encode this layered image according to extraLayerCount, but got %u frames.", + item->extraLayerCount + 1, + item->encodeOutput->samples.count); + return AVIF_RESULT_INVALID_ARGUMENT; + } } } @@ -1277,8 +1318,11 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) // ----------------------------------------------------------------------- // Write ftyp + // Layered sequence is not supported for now. + const avifBool isSequence = (encoder->extraLayerCount == 0) && (encoder->data->frames.count > 1); + const char * majorBrand = "avif"; - if (encoder->data->frames.count > 1) { + if (isSequence) { majorBrand = "avis"; } @@ -1286,7 +1330,7 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) avifRWStreamWriteChars(&s, majorBrand, 4); // unsigned int(32) major_brand; avifRWStreamWriteU32(&s, 0); // unsigned int(32) minor_version; avifRWStreamWriteChars(&s, "avif", 4); // unsigned int(32) compatible_brands[]; - if (encoder->data->frames.count > 1) { // + if (isSequence) { // avifRWStreamWriteChars(&s, "avis", 4); // ... compatible_brands[] avifRWStreamWriteChars(&s, "msf1", 4); // ... compatible_brands[] avifRWStreamWriteChars(&s, "iso8", 4); // ... compatible_brands[] @@ -1339,6 +1383,20 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { avifEncoderItem * item = &encoder->data->items.item[itemIndex]; + avifRWStreamWriteU16(&s, item->id); // unsigned int(16) item_ID; + avifRWStreamWriteU16(&s, 0); // unsigned int(16) data_reference_index; + + // Layered Image, write location for all samples + if (item->extraLayerCount > 0) { + uint32_t layerCount = item->extraLayerCount + 1; + avifRWStreamWriteU16(&s, (uint16_t)layerCount); // unsigned int(16) extent_count; + for (uint32_t i = 0; i < layerCount; ++i) { + avifEncoderItemAddMdatFixup(item, &s); + avifRWStreamWriteU32(&s, 0 /* set later */); // unsigned int(offset_size*8) extent_offset; + avifRWStreamWriteU32(&s, (uint32_t)item->encodeOutput->samples.sample[i].data.size); // unsigned int(length_size*8) extent_length; + } + continue; + } uint32_t contentSize = (uint32_t)item->metadataPayload.size; if (item->encodeOutput->samples.count > 0) { @@ -1353,8 +1411,6 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) contentSize = (uint32_t)item->encodeOutput->samples.sample[0].data.size; } - avifRWStreamWriteU16(&s, item->id); // unsigned int(16) item_ID; - avifRWStreamWriteU16(&s, 0); // unsigned int(16) data_reference_index; avifRWStreamWriteU16(&s, 1); // unsigned int(16) extent_count; avifEncoderItemAddMdatFixup(item, &s); // avifRWStreamWriteU32(&s, 0 /* set later */); // unsigned int(offset_size*8) extent_offset; @@ -1449,15 +1505,16 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) continue; } - if (item->dimgFromID) { - // All image cells from a grid should share the exact same properties, so see if we've - // already written properties out for another cell in this grid, and if so, just steal - // their ipma and move on. This is a sneaky way to provide iprp deduplication. + if (item->dimgFromID && (item->extraLayerCount == 0)) { + // All image cells from a grid should share the exact same properties unless they are + // layered image which have different al1x, so see if we've already written properties + // out for another cell in this grid, and if so, just steal their ipma and move on. + // This is a sneaky way to provide iprp deduplication. avifBool foundPreviousCell = AVIF_FALSE; for (uint32_t dedupIndex = 0; dedupIndex < itemIndex; ++dedupIndex) { avifEncoderItem * dedupItem = &encoder->data->items.item[dedupIndex]; - if (item->dimgFromID == dedupItem->dimgFromID) { + if ((item->dimgFromID == dedupItem->dimgFromID) && (dedupItem->extraLayerCount == 0)) { // We've already written dedup's items out. Steal their ipma indices and move on! item->ipma = dedupItem->ipma; foundPreviousCell = AVIF_TRUE; @@ -1514,6 +1571,38 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) avifEncoderWriteColorProperties(&s, imageMetadata, &item->ipma, dedup); } + + if (item->extraLayerCount > 0) { + // Layered Image Indexing Property + + avifItemPropertyDedupStart(dedup); + avifBoxMarker a1lx = avifRWStreamWriteBox(&dedup->s, "a1lx", AVIF_BOX_SIZE_TBD); + uint32_t layerSize[AVIF_MAX_AV1_LAYER_COUNT - 1] = { 0 }; + avifBool largeSize = AVIF_FALSE; + + for (uint32_t validLayer = 0; validLayer < item->extraLayerCount; ++validLayer) { + uint32_t size = (uint32_t)item->encodeOutput->samples.sample[validLayer].data.size; + layerSize[validLayer] = size; + if (size > 0xffff) { + largeSize = AVIF_TRUE; + } + } + + avifRWStreamWriteU8(&dedup->s, (uint8_t)largeSize); // unsigned int(7) reserved = 0; + // unsigned int(1) large_size; + + // FieldLength = (large_size + 1) * 16; + // unsigned int(FieldLength) layer_size[3]; + for (uint32_t layer = 0; layer < AVIF_MAX_AV1_LAYER_COUNT - 1; ++layer) { + if (largeSize) { + avifRWStreamWriteU32(&dedup->s, layerSize[layer]); + } else { + avifRWStreamWriteU16(&dedup->s, (uint16_t)layerSize[layer]); + } + } + avifRWStreamFinishBox(&dedup->s, a1lx); + ipmaPush(&item->ipma, avifItemPropertyDedupFinish(dedup, &s), AVIF_FALSE); + } } avifRWStreamFinishBox(&s, ipco); avifItemPropertyDedupDestroy(dedup); @@ -1559,7 +1648,7 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) // ----------------------------------------------------------------------- // Write tracks (if an image sequence) - if (encoder->data->frames.count > 1) { + if (isSequence) { static const uint8_t unityMatrix[9][4] = { /* clang-format off */ { 0x00, 0x01, 0x00, 0x00 }, @@ -1827,6 +1916,13 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) encoder->ioStats.colorOBUSize = 0; encoder->ioStats.alphaOBUSize = 0; + avifEncoderItemReferenceArray layeredColorItems; + avifEncoderItemReferenceArray layeredAlphaItems; + if (!avifArrayCreate(&layeredColorItems, sizeof(avifEncoderItemReference), 1) || + !avifArrayCreate(&layeredAlphaItems, sizeof(avifEncoderItemReference), 1)) { + return AVIF_RESULT_OUT_OF_MEMORY; + } + avifBoxMarker mdat = avifRWStreamWriteBox(&s, "mdat", AVIF_BOX_SIZE_TBD); const size_t mdatStartOffset = avifRWStreamOffset(&s); for (uint32_t itemPasses = 0; itemPasses < 3; ++itemPasses) { @@ -1861,6 +1957,17 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) 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. + assert(item->encodeOutput->samples.count == item->mdatFixups.count); + + avifEncoderItemReference * ref = item->alpha ? avifArrayPushPtr(&layeredAlphaItems) + : avifArrayPushPtr(&layeredColorItems); + *ref = item; + continue; + } + size_t chunkOffset = 0; // Deduplication - See if an identical chunk to this has already been written @@ -1899,6 +2006,61 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) } } } + + 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); + 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); + avifRWStreamWriteU32(&s, (uint32_t)chunkOffset); + avifRWStreamSetOffset(&s, prevOffset); + } + } + ++layerIndex; + } while (hasMoreSample); + + assert(layerIndex <= AVIF_MAX_AV1_LAYER_COUNT); + } + + avifArrayDestroy(&layeredColorItems); + avifArrayDestroy(&layeredAlphaItems); + avifRWStreamFinishBox(&s, mdat); // ----------------------------------------------------------------------- diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 247ed7a27c..f6101bc8b5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -118,6 +118,11 @@ if(AVIF_ENABLE_GTEST) target_include_directories(avifpng16bittest PRIVATE ${GTEST_INCLUDE_DIRS}) add_test(NAME avifpng16bittest COMMAND avifpng16bittest ${CMAKE_CURRENT_SOURCE_DIR}/data/) + add_executable(avifprogressivetest gtest/avifprogressivetest.cc) + target_link_libraries(avifprogressivetest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) + target_include_directories(avifprogressivetest PRIVATE ${GTEST_INCLUDE_DIRS}) + add_test(NAME avifprogressivetest COMMAND avifprogressivetest) + add_executable(avifreadimagetest gtest/avifreadimagetest.cc) target_link_libraries(avifreadimagetest aviftest_helpers ${GTEST_LIBRARIES}) target_include_directories(avifreadimagetest PRIVATE ${GTEST_INCLUDE_DIRS}) diff --git a/tests/gtest/avifprogressivetest.cc b/tests/gtest/avifprogressivetest.cc new file mode 100644 index 0000000000..16d90e336b --- /dev/null +++ b/tests/gtest/avifprogressivetest.cc @@ -0,0 +1,167 @@ +// Copyright 2022 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace libavif { +namespace { + +class ProgressiveTest : public testing::Test { + protected: + void SetUp() override { + if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) == + nullptr) { + GTEST_SKIP() << "ProgressiveTest requires the AOM encoder."; + } + + ASSERT_NE(encoder_, nullptr); + encoder_->codecChoice = AVIF_CODEC_CHOICE_AOM; + // The fastest speed that uses AOM_USAGE_GOOD_QUALITY. + encoder_->speed = 6; + + ASSERT_NE(decoder_, nullptr); + decoder_->allowProgressive = true; + + ASSERT_NE(image_, nullptr); + testutil::FillImageGradient(image_.get()); + } + + void TestDecode(uint32_t expect_width, uint32_t expect_height) { + ASSERT_EQ(avifDecoderSetIOMemory(decoder_.get(), encoded_avif_.data, + encoded_avif_.size), + AVIF_RESULT_OK); + ASSERT_EQ(avifDecoderParse(decoder_.get()), AVIF_RESULT_OK); + ASSERT_EQ(decoder_->progressiveState, AVIF_PROGRESSIVE_STATE_ACTIVE); + ASSERT_EQ(static_cast(decoder_->imageCount), + encoder_->extraLayerCount + 1); + + for (uint32_t layer = 0; layer < encoder_->extraLayerCount + 1; ++layer) { + ASSERT_EQ(avifDecoderNextImage(decoder_.get()), AVIF_RESULT_OK); + // libavif scales frame automatically. + ASSERT_EQ(decoder_->image->width, expect_width); + ASSERT_EQ(decoder_->image->height, expect_height); + // TODO(wtc): Check avifDecoderNthImageMaxExtent(). + } + + // TODO(wtc): Check decoder_->image and image_ are similar, and better + // quality layer is more similar. + } + + static constexpr uint32_t kImageSize = 256; + + testutil::AvifEncoderPtr encoder_{avifEncoderCreate(), avifEncoderDestroy}; + testutil::AvifDecoderPtr decoder_{avifDecoderCreate(), avifDecoderDestroy}; + + testutil::AvifImagePtr image_ = + testutil::CreateImage(kImageSize, kImageSize, 8, AVIF_PIXEL_FORMAT_YUV444, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + + testutil::AvifRwData encoded_avif_; +}; + +TEST_F(ProgressiveTest, QualityChange) { + encoder_->extraLayerCount = 1; + encoder_->minQuantizer = 50; + encoder_->maxQuantizer = 50; + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + encoder_->minQuantizer = 0; + encoder_->maxQuantizer = 0; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(kImageSize, kImageSize); +} + +TEST_F(ProgressiveTest, DimensionChange) { + if (avifLibYUVVersion() == 0) { + GTEST_SKIP() << "libyuv not available, skip test."; + } + + encoder_->extraLayerCount = 1; + encoder_->minQuantizer = 0; + encoder_->maxQuantizer = 0; + encoder_->scalingMode = {{1, 2}, {1, 2}}; + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + encoder_->scalingMode = {{1, 1}, {1, 1}}; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(kImageSize, kImageSize); +} + +TEST_F(ProgressiveTest, LayeredGrid) { + encoder_->extraLayerCount = 1; + encoder_->minQuantizer = 50; + encoder_->maxQuantizer = 50; + + avifImage* image_grid[2] = {image_.get(), image_.get()}; + ASSERT_EQ(avifEncoderAddImageGrid(encoder_.get(), 2, 1, image_grid, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + encoder_->minQuantizer = 0; + encoder_->maxQuantizer = 0; + ASSERT_EQ(avifEncoderAddImageGrid(encoder_.get(), 2, 1, image_grid, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(2 * kImageSize, kImageSize); +} + +TEST_F(ProgressiveTest, SameLayers) { + encoder_->extraLayerCount = 3; + for (uint32_t layer = 0; layer < encoder_->extraLayerCount + 1; ++layer) { + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + } + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(kImageSize, kImageSize); +} + +TEST_F(ProgressiveTest, TooManyLayers) { + encoder_->extraLayerCount = 1; + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_INVALID_ARGUMENT); +} + +TEST_F(ProgressiveTest, TooFewLayers) { + encoder_->extraLayerCount = 1; + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), + AVIF_RESULT_INVALID_ARGUMENT); +} + +} // namespace +} // namespace libavif