Layered encoding support
Layered image can be encoded by calling avifEncoderAddImage() multiple
times, one layer per call.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d7e84c..a0d8fb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
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 e4350d0..f8c9a73 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -99,6 +99,9 @@
// 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),
@@ -345,6 +348,15 @@
AVIF_API void avifDiagnosticsClearError(avifDiagnostics * diag);
// ---------------------------------------------------------------------------
+// Fraction utility
+
+typedef struct avifFraction
+{
+ int32_t n;
+ int32_t d;
+} avifFraction;
+
+// ---------------------------------------------------------------------------
// Optional transformation structs
typedef enum avifTransformFlag
@@ -1067,6 +1079,12 @@
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 @@
// 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 @@
// 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 @@
int tileRowsLog2;
int tileColsLog2;
avifBool autoTiling;
+ avifScalingMode scalingMode;
// stats from the most recent write
avifIOStats ioStats;
@@ -1137,26 +1159,36 @@
// 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 ae59f79..d8833e6 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -72,6 +72,11 @@
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 @@
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 62eba20..2aff76f 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -563,15 +563,9 @@
// ---------------------------------------------------------------------------
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 586f1e5..dde3ad4 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -67,6 +67,7 @@
// 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 @@
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 @@
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 @@
// 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 @@
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 @@
}
}
+ 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 @@
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 @@
}
}
- 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 4b93b79..534dd5d 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -73,6 +73,11 @@
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 158b2a4..4e13450 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -65,6 +65,11 @@
}
}
+ // 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 e5b569c..0fafd8f 100644
--- a/src/read.c
+++ b/src/read.c
@@ -37,8 +37,6 @@
// 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 @@
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 @@
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 697cb0e..5fc1cb6 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -146,3 +146,92 @@
}
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 ff9117c..649183e 100644
--- a/src/write.c
+++ b/src/write.c
@@ -181,6 +181,8 @@
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;
@@ -188,6 +190,13 @@
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
@@ -383,6 +392,8 @@
// ---------------------------------------------------------------------------
+static const avifScalingMode noScaling = { { 1, 1 }, { 1, 1 } };
+
avifEncoder * avifEncoderCreate(void)
{
avifEncoder * encoder = (avifEncoder *)avifAlloc(sizeof(avifEncoder));
@@ -401,6 +412,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
item->codec->csOptions = encoder->csOptions;
item->codec->diag = &encoder->diag;
+ item->extraLayerCount = encoder->extraLayerCount;
if (cellCount > 1) {
item->dimgFromID = gridColorID;
@@ -1070,6 +1098,7 @@
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 @@
}
}
} 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 @@
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 @@
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 @@
// -----------------------------------------------------------------------
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// -----------------------------------------------------------------------
// 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 @@
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 @@
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 @@
}
}
}
+
+ 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 247ed7a..f6101bc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -118,6 +118,11 @@
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 0000000..16d90e3
--- /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<uint32_t>(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