Support encoding layered image with pre-scaled inputs
Add the width and height encoding options to avifEncoder.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7c8307..206d2aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
* Add the ignoreICC option to avifDecoder
* Support ignoring alpha in avifDecoder::imageContentToDecode
+* Support encoding layered image with pre-scaled inputs
* avifenc: add --ignore-alpha flag to discard alpha channel on encode
* avifgainmaputil: add --ignore-alpha flag to discard alpha channel
* avifgainmaputil: add --ignore-exif and --ignore-xmp flags
diff --git a/apps/avifenc.c b/apps/avifenc.c
index e169839..c3b37d0 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -78,6 +78,10 @@
avifMatrixCoefficients matrixCoefficients;
avifChromaDownsampling chromaDownsampling;
avifAppFileFormat inputFormat;
+
+ // The last layer's size. Inferred from the last input, only needed when using --layered.
+ uint32_t width;
+ uint32_t height;
} avifSettings;
typedef struct
@@ -454,6 +458,58 @@
return AVIF_TRUE;
}
+static avifBool avifVerifyImageFitsLastLayerSize(const avifSettings * settings, const avifImage * image, const char * filename)
+{
+ if (settings->width == 0) {
+ return AVIF_TRUE;
+ }
+ if ((image->width > settings->width) || (image->height > settings->height)) {
+ fprintf(stderr,
+ "ERROR: Input image dimensions [%ux%u] exceed the last layer's size [%ux%u]: %s\n",
+ image->width,
+ image->height,
+ settings->width,
+ settings->height,
+ filename);
+ return AVIF_FALSE;
+ }
+ return AVIF_TRUE;
+}
+
+// Checks, before encoding and in terms of CLI, for the settings that the library would reject once
+// combined with --layered inputs of different sizes (--layered sets avifEncoder.width/height, two
+// fields avifenc's users never set directly). Only checks conditions actually reachable via
+// avifenc's CLI. For example, grids and non-layered images can't reach this point at all, so they
+// are not checked here.
+static avifBool avifVerifyLastLayerSizeCompatibility(const avifSettings * settings, const avifInput * input, const avifImage * firstImage)
+{
+ if (settings->width == 0) {
+ return AVIF_TRUE;
+ }
+ for (int i = 0; i < settings->layers; ++i) {
+ const avifScalingMode * scalingMode = &input->files[i].settings.scalingMode.value;
+ const avifBool isNoScaling = (scalingMode->horizontal.n == scalingMode->horizontal.d) &&
+ (scalingMode->vertical.n == scalingMode->vertical.d);
+ if (input->files[i].settings.scalingMode.set && !isNoScaling) {
+ fprintf(stderr, "ERROR: --scaling-mode cannot be used with --layered inputs of different sizes\n");
+ return AVIF_FALSE;
+ }
+ }
+ if (input->requestedDepthExtension != 0) {
+ fprintf(stderr, "ERROR: --depth with bit depth extension cannot be used with --layered inputs of different sizes\n");
+ return AVIF_FALSE;
+ }
+#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
+ if (firstImage->gainMap && firstImage->gainMap->image) {
+ fprintf(stderr, "ERROR: A gain map cannot be used with --layered inputs of different sizes (use --ignore-gain-map)\n");
+ return AVIF_FALSE;
+ }
+#else
+ (void)firstImage;
+#endif
+ return AVIF_TRUE;
+}
+
static avifBool avifInputAddCachedImage(avifInput * input)
{
avifImage * newImage = avifImageCreateEmpty();
@@ -860,10 +916,11 @@
static avifBool avifEncoderVerifyImageCompatibility(const avifImage * refImage,
const avifImage * testImage,
const char * seriesType,
- const char * filename)
+ const char * filename,
+ avifBool allowDimensionChange)
{
// Verify that this frame's properties matches the first frame's properties
- if ((refImage->width != testImage->width) || (refImage->height != testImage->height)) {
+ if (!allowDimensionChange && ((refImage->width != testImage->width) || (refImage->height != testImage->height))) {
fprintf(stderr,
"ERROR: Image %s dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
seriesType,
@@ -954,7 +1011,11 @@
settings->inputFormat)) {
goto cleanup;
}
- if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "sequence", avifPrettyFilename(nextFile->filename))) {
+ if (!avifEncoderVerifyImageCompatibility(firstImage,
+ nextImage,
+ "sequence",
+ avifPrettyFilename(nextFile->filename),
+ /*allowDimensionChange=*/AVIF_FALSE)) {
goto cleanup;
}
if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) {
@@ -1061,6 +1122,9 @@
settings->inputFormat)) {
goto cleanup;
}
+ if (!avifVerifyImageFitsLastLayerSize(settings, nextImage, avifPrettyFilename(nextFile->filename))) {
+ goto cleanup;
+ }
// frameIter is NULL if y4m reached end, so single frame y4m is still supported.
if (input->frameIter) {
fprintf(stderr,
@@ -1068,7 +1132,11 @@
avifPrettyFilename(nextFile->filename));
goto cleanup;
}
- if (!avifEncoderVerifyImageCompatibility(firstImage, nextImage, "layer", avifPrettyFilename(nextFile->filename))) {
+ if (!avifEncoderVerifyImageCompatibility(firstImage,
+ nextImage,
+ "layer",
+ avifPrettyFilename(nextFile->filename),
+ /*allowDimensionChange=*/AVIF_TRUE)) {
goto cleanup;
}
if (!avifEncodeUpdateEncoderSettings(encoder, nextSettings)) {
@@ -1129,6 +1197,8 @@
encoder->creationTime = settings->creationTime;
encoder->modificationTime = settings->modificationTime;
encoder->extraLayerCount = settings->layers - 1;
+ encoder->width = settings->width;
+ encoder->height = settings->height;
if (!avifEncodeUpdateEncoderSettings(encoder, &firstFile->settings)) {
goto cleanup;
}
@@ -2381,6 +2451,38 @@
goto cleanup;
}
+ uint32_t outputImageWidth = image->width;
+ uint32_t outputImageHeight = image->height;
+ if (settings.layered) {
+ // Get the resolution of the last layer without decoding it, to fill the output image size
+ // in advance.
+ // Only fill the output image size if the last layer's resolution differs from the first
+ // layer's, to not interfere with --scaling-mode.
+ const avifInputFile * lastFile = &input.files[input.filesCount - 1];
+ avifImage * lastImage = avifImageCreateEmpty();
+ if (!lastImage) {
+ fprintf(stderr, "ERROR: Out of memory\n");
+ goto cleanup;
+ }
+ const avifBool lastImageOk = avifPeekImage(lastFile->filename, settings.inputFormat, lastImage) != AVIF_APP_FILE_FORMAT_UNKNOWN;
+ if (lastImageOk && ((lastImage->width != image->width) || (lastImage->height != image->height))) {
+ outputImageWidth = settings.width = lastImage->width;
+ outputImageHeight = settings.height = lastImage->height;
+ }
+ avifImageDestroy(lastImage);
+ if (!lastImageOk) {
+ fprintf(stderr, "ERROR: Failed to peek last layer: %s\n", avifPrettyFilename(lastFile->filename));
+ goto cleanup;
+ }
+ }
+
+ if (!avifVerifyImageFitsLastLayerSize(&settings, image, avifPrettyFilename(firstFile->filename))) {
+ goto cleanup;
+ }
+ if (!avifVerifyLastLayerSizeCompatibility(&settings, &input, image)) {
+ goto cleanup;
+ }
+
printf("Successfully loaded: %s\n", avifPrettyFilename(firstFile->filename));
// Prepare image timings
@@ -2433,7 +2535,7 @@
image->pasp.vSpacing = settings.paspValues[1];
}
if (cropConversionRequired) {
- if (!convertCropToClap(image->width, image->height, settings.clapValues)) {
+ if (!convertCropToClap(outputImageWidth, outputImageHeight, settings.clapValues)) {
goto cleanup;
}
settings.clapValid = AVIF_TRUE;
@@ -2453,7 +2555,7 @@
avifCropRect cropRect;
avifDiagnostics diag;
avifDiagnosticsClearError(&diag);
- if (!avifCropRectFromCleanApertureBox(&cropRect, &image->clap, image->width, image->height, &diag)) {
+ if (!avifCropRectFromCleanApertureBox(&cropRect, &image->clap, outputImageWidth, outputImageHeight, &diag)) {
fprintf(stderr,
"ERROR: Invalid clap: width:[%d / %d], height:[%d / %d], horizOff:[%d / %d], vertOff:[%d / %d] - %s\n",
(int32_t)image->clap.widthN,
@@ -2634,8 +2736,14 @@
lossyHint = " (Lossless)";
}
printf("AVIF to be written:%s\n", lossyHint);
+ // avifImageDump wants the info of one cell, but image, outputImageWidth, and outputImageHeight
+ // are the before-split image and its info when avifImageSplitGrid is called.
const avifImage * avif = gridCells ? gridCells[0] : image;
+ const uint32_t cellWidth = gridCells ? avif->width : outputImageWidth;
+ const uint32_t cellHeight = gridCells ? avif->height : outputImageHeight;
avifImageDump(avif,
+ cellWidth,
+ cellHeight,
settings.gridDims[0],
settings.gridDims[1],
settings.layers > 1 ? AVIF_PROGRESSIVE_STATE_AVAILABLE : AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 2333733..8b3ed6f 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -362,7 +362,7 @@
const std::string& output_filename) {
avifRWData encoded = AVIF_DATA_EMPTY;
std::cout << "AVIF to be written:\n";
- avifImageDump(image,
+ avifImageDump(image, image->width, image->height,
/*gridCols=*/1,
/*gridRows=*/1, AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
PrintEncodingSettings(encoder, image->gainMap != nullptr);
@@ -407,7 +407,8 @@
avifRWData encoded = AVIF_DATA_EMPTY;
std::cout << "AVIF to be written:\n";
- avifImageDump(grid_cells_ptrs[0], grid_cols, grid_rows,
+ avifImageDump(grid_cells_ptrs[0], grid_cells_ptrs[0]->width,
+ grid_cells_ptrs[0]->height, grid_cols, grid_rows,
AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
PrintEncodingSettings(encoder, image->gainMap != nullptr);
avifResult result = avifEncoderAddImageGrid(encoder, grid_cols, grid_rows,
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 0126cd2..cd9e061 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -486,7 +486,8 @@
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
- uint32_t sizeLimit);
+ uint32_t sizeLimit,
+ avifBool headerOnly);
// Arbitrary max number of jpeg segments to parse before giving up.
#define MAX_JPEG_SEGMENTS 100
@@ -1024,7 +1025,8 @@
/*ignoreExif=*/AVIF_TRUE,
/*ignoreXMP=*/AVIF_FALSE,
/*ignoreGainMap=*/AVIF_TRUE,
- sizeLimit)) {
+ sizeLimit,
+ /*headerOnly=*/AVIF_FALSE)) {
continue;
}
if (avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size, NULL)) {
@@ -1261,7 +1263,8 @@
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
- uint32_t sizeLimit)
+ uint32_t sizeLimit,
+ avifBool headerOnly)
{
volatile avifBool ret = AVIF_FALSE;
uint8_t * volatile iccData = NULL;
@@ -1305,6 +1308,8 @@
fprintf(stderr, "Too big JPEG dimensions (%u x %u > %u px): %s\n", cinfo.output_width, cinfo.output_height, sizeLimit, inputFilename);
goto cleanup;
}
+ avif->width = cinfo.output_width;
+ avif->height = cinfo.output_height;
if (!ignoreColorProfile) {
uint8_t * iccDataTmp;
@@ -1336,6 +1341,12 @@
// JPEG doesn't have alpha. Prevent confusion.
avif->alphaPremultiplied = AVIF_FALSE;
+ if (headerOnly) {
+ // No real decoding needed. Stop here.
+ ret = AVIF_TRUE;
+ goto cleanup;
+ }
+
if (avifJPEGReadCopy(avif, sizeLimit, &cinfo)) {
// JPEG pixels were successfully copied without conversion. Notify the enduser.
@@ -1351,8 +1362,6 @@
int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
- avif->width = cinfo.output_width;
- avif->height = cinfo.output_height;
if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with JPEG because it has an even bit depth.\n");
goto cleanup;
@@ -1672,13 +1681,37 @@
ignoreExif,
ignoreXMP,
ignoreGainMap,
- sizeLimit);
+ sizeLimit,
+ /*headerOnly=*/AVIF_FALSE);
if (f && f != stdin) {
fclose(f);
}
return res;
}
+avifBool avifJPEGPeek(const char * inputFilename, avifImage * avif)
+{
+ FILE * f = fopen(inputFilename, "rb");
+ if (!f) {
+ fprintf(stderr, "Can't open JPEG file for read: %s\n", inputFilename);
+ return AVIF_FALSE;
+ }
+ const avifBool res = avifJPEGReadInternal(f,
+ inputFilename,
+ avif,
+ AVIF_PIXEL_FORMAT_NONE,
+ /*requestedDepth=*/0,
+ AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
+ /*ignoreColorProfile=*/AVIF_TRUE,
+ /*ignoreExif=*/AVIF_TRUE,
+ /*ignoreXMP=*/AVIF_TRUE,
+ /*ignoreGainMap=*/AVIF_TRUE,
+ /*sizeLimit=*/UINT32_MAX,
+ /*headerOnly=*/AVIF_TRUE);
+ fclose(f);
+ return res;
+}
+
avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling)
{
avifBool ret = AVIF_FALSE;
diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h
index 4cfc1e5..ad035d7 100644
--- a/apps/shared/avifjpeg.h
+++ b/apps/shared/avifjpeg.h
@@ -26,6 +26,11 @@
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit);
+
+// Parse the jpeg file at path 'inputFilename' and write its metadata into 'avif'
+// without decoding the pixels.
+avifBool avifJPEGPeek(const char * inputFilename, avifImage * avif);
+
avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling);
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 9d7fee5..cfb5049 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -279,7 +279,8 @@
avifBool ignoreXMP,
avifBool ignoreAlpha,
uint32_t imageSizeLimit,
- uint32_t * outPNGDepth)
+ uint32_t * outPNGDepth,
+ avifBool headerOnly)
{
volatile avifBool readResult = AVIF_FALSE;
png_structp png = NULL;
@@ -505,15 +506,23 @@
// Note: There is no support for the rare "Raw profile type icc" or "Raw profile type icm" text chunks.
}
+ if (avif->width > imageSizeLimit / avif->height) {
+ fprintf(stderr, "Too big PNG dimensions (%u x %u > %u px): %s\n", avif->width, avif->height, imageSizeLimit, inputFilename);
+ goto cleanup;
+ }
+
+ if (headerOnly) {
+ // All the metadata read so far (dimensions, depth, yuvFormat, ICC/color primaries if
+ // requested) is already set on avif. Stop here instead of decoding any pixel data.
+ readResult = AVIF_TRUE;
+ goto cleanup;
+ }
+
const int numChannels = png_get_channels(png, info);
if (numChannels < 1 || numChannels > 4) {
fprintf(stderr, "png_get_channels() should return 1, 2, 3 or 4 but returns %d.\n", numChannels);
goto cleanup;
}
- if (avif->width > imageSizeLimit / avif->height) {
- fprintf(stderr, "Too big PNG dimensions (%u x %u > %u px): %s\n", avif->width, avif->height, imageSizeLimit, inputFilename);
- goto cleanup;
- }
avifRGBImageSetDefaults(&rgb, avif);
rgb.chromaDownsampling = chromaDownsampling;
@@ -613,7 +622,8 @@
ignoreXMP,
ignoreAlpha,
imageSizeLimit,
- outPNGDepth);
+ outPNGDepth,
+ /*headerOnly=*/AVIF_FALSE);
if (f != stdin) {
fclose(f);
@@ -621,6 +631,31 @@
return res;
}
+avifBool avifPNGPeek(const char * inputFilename, avifImage * avif)
+{
+ FILE * f = fopen(inputFilename, "rb");
+ if (!f) {
+ fprintf(stderr, "Can't open PNG file for read: %s\n", inputFilename);
+ return AVIF_FALSE;
+ }
+
+ const avifBool res = avifPNGReadImpl(f,
+ inputFilename,
+ avif,
+ /*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE,
+ /*requestedDepth=*/0,
+ AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
+ /*ignoreColorProfile=*/AVIF_TRUE,
+ /*ignoreExif=*/AVIF_TRUE,
+ /*ignoreXMP=*/AVIF_TRUE,
+ /*ignoreAlpha=*/AVIF_TRUE,
+ /*imageSizeLimit=*/UINT32_MAX,
+ /*outPNGDepth=*/NULL,
+ /*headerOnly=*/AVIF_TRUE);
+ fclose(f);
+ return res;
+}
+
//------------------------------------------------------------------------------
// Writing
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index b50d2b7..cbe3774 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -22,6 +22,11 @@
avifBool ignoreAlpha,
uint32_t imageSizeLimit,
uint32_t * outPNGDepth);
+
+// Parse the PNG file at path 'inputFilename' and write its metadata into 'avif'
+// without decoding the pixels.
+avifBool avifPNGPeek(const char * inputFilename, avifImage * avif);
+
avifBool avifPNGWrite(const char * outputFilename,
const avifImage * avif,
uint32_t requestedDepth,
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index bd0eaf6..6a370fc 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -61,10 +61,16 @@
}
}
-static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifBool alphaPresent, avifProgressiveState progressiveState)
+static void avifImageDumpInternal(const avifImage * avif,
+ uint32_t cellWidth,
+ uint32_t cellHeight,
+ uint32_t gridCols,
+ uint32_t gridRows,
+ avifBool alphaPresent,
+ avifProgressiveState progressiveState)
{
- uint32_t width = avif->width;
- uint32_t height = avif->height;
+ uint32_t width = cellWidth;
+ uint32_t height = cellHeight;
if (gridCols && gridRows) {
width *= gridCols;
height *= gridRows;
@@ -120,7 +126,7 @@
avifCropRect cropRect;
avifDiagnostics diag;
avifDiagnosticsClearError(&diag);
- avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag);
+ avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, cellWidth, cellHeight, &diag);
if (validClap) {
printf(" * Valid, derived crop rect: X: %d, Y: %d, W: %d, H: %d%s\n",
cropRect.x,
@@ -193,15 +199,15 @@
}
}
-void avifImageDump(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState)
+void avifImageDump(const avifImage * avif, uint32_t cellWidth, uint32_t cellHeight, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState)
{
const avifBool alphaPresent = avif->alphaPlane && (avif->alphaRowBytes > 0);
- avifImageDumpInternal(avif, gridCols, gridRows, alphaPresent, progressiveState);
+ avifImageDumpInternal(avif, cellWidth, cellHeight, gridCols, gridRows, alphaPresent, progressiveState);
}
void avifContainerDump(const avifDecoder * decoder)
{
- avifImageDumpInternal(decoder->image, 0, 0, decoder->alphaPresent, decoder->progressiveState);
+ avifImageDumpInternal(decoder->image, decoder->image->width, decoder->image->height, 0, 0, decoder->alphaPresent, decoder->progressiveState);
if (decoder->imageSequenceTrackPresent) {
if (decoder->repetitionCount == AVIF_REPETITION_COUNT_INFINITE) {
printf(" * Repeat Count : Infinite\n");
@@ -364,6 +370,34 @@
return inputFormat;
}
+avifAppFileFormat avifPeekImage(const char * filename, avifAppFileFormat inputFormat, avifImage * image)
+{
+ if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+ inputFormat = avifGuessFileFormat(filename);
+ }
+
+ if (inputFormat == AVIF_APP_FILE_FORMAT_Y4M) {
+ if (!y4mPeek(filename, image)) {
+ return AVIF_APP_FILE_FORMAT_UNKNOWN;
+ }
+ } else if (inputFormat == AVIF_APP_FILE_FORMAT_JPEG) {
+ if (!avifJPEGPeek(filename, image)) {
+ return AVIF_APP_FILE_FORMAT_UNKNOWN;
+ }
+ } else if (inputFormat == AVIF_APP_FILE_FORMAT_PNG) {
+ if (!avifPNGPeek(filename, image)) {
+ return AVIF_APP_FILE_FORMAT_UNKNOWN;
+ }
+ } else if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+ fprintf(stderr, "Unrecognized file format for input file: %s\n", filename);
+ return AVIF_APP_FILE_FORMAT_UNKNOWN;
+ } else {
+ fprintf(stderr, "Unsupported file format %s for input file: %s\n", avifFileFormatToString(inputFormat), filename);
+ return AVIF_APP_FILE_FORMAT_UNKNOWN;
+ }
+ return inputFormat;
+}
+
avifBool avifReadEntireFile(const char * filename, avifRWData * raw)
{
FILE * f = fopen(filename, "rb");
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 5b01d85..bec09ab 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -28,7 +28,9 @@
#define AVIF_FMT_ZU "zu"
#endif
-void avifImageDump(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState);
+// Dumps the information of an AVIF file that has gridCols*gridRows cells (1*1 for non-grid image)
+// and 'avif' as the first cell that's encoded to have size cellWidth*cellHeight.
+void avifImageDump(const avifImage * avif, uint32_t cellWidth, uint32_t cellHeight, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState);
void avifContainerDump(const avifDecoder * decoder);
void avifPrintVersions(void);
void avifDumpDiagnostics(const avifDiagnostics * diag);
@@ -118,6 +120,16 @@
avifAppSourceTiming * sourceTiming,
struct y4mFrameIterator ** frameIter);
+// Determines only the metadata available before pixel decoding starts (currently width, height,
+// depth, yuvFormat and, for JPEG/PNG, color profile information) for an image file, without
+// decoding any pixel data.
+// If 'inputFormat' is AVIF_APP_FILE_FORMAT_UNKNOWN, the image format is guessed
+// based on the filename or first few bytes.
+// Returns the format of the file, or AVIF_APP_FILE_FORMAT_UNKNOWN in case of error.
+// Note: This function reuses the code that reads an image. The current user of this function only
+// uses the width and height reported. Double check first when using the other info returned.
+avifAppFileFormat avifPeekImage(const char * filename, avifAppFileFormat inputFormat, avifImage * image);
+
// Copies all the bytes from the file at filename to a newly allocated memory chunk.
avifBool avifReadEntireFile(const char * filename, avifRWData * raw);
diff --git a/apps/shared/y4m.c b/apps/shared/y4m.c
index 9a604ed..791aa82 100644
--- a/apps/shared/y4m.c
+++ b/apps/shared/y4m.c
@@ -253,12 +253,13 @@
goto cleanup; \
} while (0)
-avifBool y4mRead(const char * inputFilename,
- avifBool ignoreAlpha,
- uint32_t imageSizeLimit,
- avifImage * avif,
- avifAppSourceTiming * sourceTiming,
- struct y4mFrameIterator ** iter)
+static avifBool y4mReadInternal(const char * inputFilename,
+ avifBool ignoreAlpha,
+ uint32_t imageSizeLimit,
+ avifBool headerOnly,
+ avifImage * avif,
+ avifAppSourceTiming * sourceTiming,
+ struct y4mFrameIterator ** iter)
{
avifBool result = AVIF_FALSE;
@@ -412,6 +413,14 @@
avif->yuvFormat = frame.format;
avif->yuvRange = frame.range;
avif->yuvChromaSamplePosition = frame.chromaSamplePosition;
+
+ if (headerOnly) {
+ // All the metadata (dimensions, depth, format, range, chroma sample position) is already
+ // known. Stop here instead of reading any frame data.
+ result = AVIF_TRUE;
+ goto cleanup;
+ }
+
avifResult allocationResult = avifImageAllocatePlanes(avif, frame.hasAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
if (allocationResult != AVIF_RESULT_OK) {
fprintf(stderr, "Failed to allocate the planes: %s\n", avifResultToString(allocationResult));
@@ -478,6 +487,27 @@
return result;
}
+avifBool y4mRead(const char * inputFilename,
+ avifBool ignoreAlpha,
+ uint32_t imageSizeLimit,
+ avifImage * avif,
+ avifAppSourceTiming * sourceTiming,
+ struct y4mFrameIterator ** iter)
+{
+ return y4mReadInternal(inputFilename, ignoreAlpha, imageSizeLimit, /*headerOnly=*/AVIF_FALSE, avif, sourceTiming, iter);
+}
+
+avifBool y4mPeek(const char * inputFilename, avifImage * avif)
+{
+ return y4mReadInternal(inputFilename,
+ /*ignoreAlpha=*/AVIF_TRUE,
+ /*imageSizeLimit=*/UINT32_MAX,
+ /*headerOnly=*/AVIF_TRUE,
+ avif,
+ /*sourceTiming=*/NULL,
+ /*iter=*/NULL);
+}
+
avifBool y4mWrite(const char * outputFilename, const avifImage * avif)
{
avifBool hasAlpha = (avif->alphaPlane != NULL) && (avif->alphaRowBytes > 0);
diff --git a/apps/shared/y4m.h b/apps/shared/y4m.h
index bf9d645..4b4630e 100644
--- a/apps/shared/y4m.h
+++ b/apps/shared/y4m.h
@@ -24,6 +24,11 @@
avifImage * avif,
avifAppSourceTiming * sourceTiming,
struct y4mFrameIterator ** iter);
+
+// Parse the y4m file at path 'inputFilename' and write its metadata into 'avif'
+// without decoding the pixels.
+avifBool y4mPeek(const char * inputFilename, avifImage * avif);
+
avifBool y4mWrite(const char * outputFilename, const avifImage * avif);
#ifdef __cplusplus
diff --git a/include/avif/avif.h b/include/avif/avif.h
index bd81044..92ea161 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -821,6 +821,12 @@
// To encode any of these boxes, set the values in the associated box, then enable the flag in
// transformFlags. On decode, only honor the values in boxes with the associated transform flag set.
// These also apply to gainMap->image, if any.
+ //
+ // When encoding with avifEncoder.width/height set (see their comment), these transformations
+ // are interpreted relative to that size rather than to the current layer's own
+ // width/height. No special handling is needed during decode: the decoded layer is
+ // automatically scaled to the configured size, so these transformations are relative to the
+ // decoded avifImage's width/height as usual.
avifTransformFlags transformFlags;
avifPixelAspectRatioBox pasp;
avifCleanApertureBox clap;
@@ -1582,6 +1588,7 @@
avifBool autoTiling;
// Up/down scaling of the image to perform before encoding.
+ // This cannot be used together with encoder->width / encoder->height.
avifScalingMode scalingMode;
// --------------------------------------------------------------------------------------------
@@ -1628,6 +1635,13 @@
// Version 1.4.0 ends here. Add any new members after this line.
// --------------------------------------------------------------------------------------------
+
+ // Only for layered image (extraLayerCount > 0), otherwise must be the default value 0.
+ // Declares the size of the encoded image beforehand, which shall be exactly the size of the
+ // last layer. This allows adding smaller images as earlier layers to avoid a wasted scaling
+ // round trip, or scale ratios that the encoder does not support via scalingMode.
+ uint32_t width;
+ uint32_t height;
} avifEncoder;
// Creates an encoder initialized with default settings values.
diff --git a/src/codec_aom.c b/src/codec_aom.c
index ec4a50a..dd6205a 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -871,6 +871,8 @@
cfg->g_input_bit_depth = image->depth;
cfg->g_w = image->width;
cfg->g_h = image->height;
+ cfg->g_forced_max_frame_width = encoder->width;
+ cfg->g_forced_max_frame_height = encoder->height;
// Detect the libaom v3.6.0 bug described in
// https://crbug.com/aomedia/2871#c12. See the changes to
@@ -933,10 +935,30 @@
if (disableLaggedOutput) {
cfg->g_lag_in_frames = 0;
}
+ if ((encoder->width || encoder->height) && (cfg->g_lag_in_frames > 1)) {
+ // aom_codec_enc_config_set() does not allow changing frame dimensions if
+ // g_lag_in_frames > 1.
+ cfg->g_lag_in_frames = 1;
+ }
if (encoder->maxThreads > 1) {
// libaom fails if cfg->g_threads is greater than 64 threads. See MAX_NUM_THREADS in
// aom/aom_util/aom_thread.h.
cfg->g_threads = AVIF_MIN(encoder->maxThreads, 64);
+
+ // Detect the libaom bug before v3.15.2 described in
+ // https://issues.oss-fuzz.com/issues/559019046. See the changes in
+ // https://aomedia-review.googlesource.com/c/aom/+/216921.
+ static const int aomVersion_3_15_2 = (3 << 16) | (15 << 8) | 2;
+ if (aom_codec_version() < aomVersion_3_15_2) {
+ // When creating extra worker threads during encoding (which can
+ // happen when the new frame is larger), libaom may skip
+ // allocating the pixel_gradient_info buffers needed by
+ // GOOD_QUALITY mode. Work around the bug by disabling
+ // multithreading.
+ if (aomUsage == AOM_USAGE_GOOD_QUALITY && (encoder->width || encoder->height)) {
+ cfg->g_threads = 1;
+ }
+ }
}
// Encode alpha as 4:0:0.
@@ -1082,8 +1104,9 @@
} else {
avifBool dimensionsChanged = AVIF_FALSE;
if ((cfg->g_w != image->width) || (cfg->g_h != image->height)) {
- // We are not ready for dimension change for now.
- return AVIF_RESULT_NOT_IMPLEMENTED;
+ cfg->g_w = image->width;
+ cfg->g_h = image->height;
+ dimensionsChanged = AVIF_TRUE;
}
if (alpha) {
if (encoderChanges & (AVIF_ENCODER_CHANGE_MIN_QUANTIZER_ALPHA | AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA)) {
diff --git a/src/codec_avm.c b/src/codec_avm.c
index 6222f7f..77b795a 100644
--- a/src/codec_avm.c
+++ b/src/codec_avm.c
@@ -444,6 +444,11 @@
avifAddImageFlags addImageFlags,
avifCodecEncodeOutput * output)
{
+ // AVM does not support encoding a layered image.
+ if (encoder->width || encoder->height) {
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
struct avm_codec_enc_cfg * cfg = &codec->internal->cfg;
avifBool quantizerUpdated = AVIF_FALSE;
const int quantizer = avmQualityToQuantizer(quality, image->depth);
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 97c92be..7fc064e 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -82,8 +82,8 @@
return AVIF_RESULT_NOT_IMPLEMENTED;
}
- // rav1e does not support encoding layered image.
- if (encoder->extraLayerCount > 0) {
+ // rav1e does not support encoding a layered image.
+ if (encoder->width || encoder->height || encoder->extraLayerCount > 0) {
return AVIF_RESULT_NOT_IMPLEMENTED;
}
diff --git a/src/codec_svt.c b/src/codec_svt.c
index a76fb5b..395afc8 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -75,8 +75,8 @@
}
}
- // SVT-AV1 does not support encoding layered image.
- if (encoder->extraLayerCount > 0) {
+ // SVT-AV1 does not support encoding a layered image.
+ if (encoder->width || encoder->height || encoder->extraLayerCount > 0) {
return AVIF_RESULT_NOT_IMPLEMENTED;
}
diff --git a/src/write.c b/src/write.c
index 2070600..f6653d1 100644
--- a/src/write.c
+++ b/src/write.c
@@ -477,6 +477,45 @@
static const avifScalingMode noScaling = { { 1, 1 }, { 1, 1 } };
+static avifBool avifEncoderSizeIsSet(const avifEncoder * encoder)
+{
+ return (encoder->width != 0) || (encoder->height != 0);
+}
+
+static avifBool avifScalingModeIsNoScaling(const avifScalingMode * scalingMode)
+{
+ return (scalingMode->horizontal.n == scalingMode->horizontal.d) && (scalingMode->vertical.n == scalingMode->vertical.d);
+}
+
+static avifBool avifImageHasEquivalentTransformProperties(const avifImage * lhs, const avifImage * rhs)
+{
+ const uint32_t lhsTransformFlags = lhs->transformFlags &
+ (AVIF_TRANSFORM_PASP | AVIF_TRANSFORM_CLAP | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+ const uint32_t rhsTransformFlags = rhs->transformFlags &
+ (AVIF_TRANSFORM_PASP | AVIF_TRANSFORM_CLAP | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+ if (lhsTransformFlags != rhsTransformFlags) {
+ return AVIF_FALSE;
+ }
+ if ((lhsTransformFlags & AVIF_TRANSFORM_PASP) &&
+ ((lhs->pasp.hSpacing != rhs->pasp.hSpacing) || (lhs->pasp.vSpacing != rhs->pasp.vSpacing))) {
+ return AVIF_FALSE;
+ }
+ if ((lhsTransformFlags & AVIF_TRANSFORM_CLAP) &&
+ ((lhs->clap.widthN != rhs->clap.widthN) || (lhs->clap.widthD != rhs->clap.widthD) ||
+ (lhs->clap.heightN != rhs->clap.heightN) || (lhs->clap.heightD != rhs->clap.heightD) ||
+ (lhs->clap.horizOffN != rhs->clap.horizOffN) || (lhs->clap.horizOffD != rhs->clap.horizOffD) ||
+ (lhs->clap.vertOffN != rhs->clap.vertOffN) || (lhs->clap.vertOffD != rhs->clap.vertOffD))) {
+ return AVIF_FALSE;
+ }
+ if ((lhsTransformFlags & AVIF_TRANSFORM_IROT) && (lhs->irot.angle != rhs->irot.angle)) {
+ return AVIF_FALSE;
+ }
+ if ((lhsTransformFlags & AVIF_TRANSFORM_IMIR) && (lhs->imir.axis != rhs->imir.axis)) {
+ return AVIF_FALSE;
+ }
+ return AVIF_TRUE;
+}
+
avifEncoder * avifEncoderCreate(void)
{
avifEncoder * encoder = (avifEncoder *)avifAlloc(sizeof(avifEncoder));
@@ -511,6 +550,8 @@
encoder->creationTime = 0;
encoder->modificationTime = 0;
encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_NONE;
+ encoder->width = 0;
+ encoder->height = 0;
return encoder;
}
@@ -554,6 +595,8 @@
encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2;
lastEncoder->scalingMode = encoder->scalingMode;
lastEncoder->sampleTransformRecipe = encoder->sampleTransformRecipe;
+ lastEncoder->width = encoder->width;
+ lastEncoder->height = encoder->height;
}
// This function detects changes made on avifEncoder. It returns true on success (i.e., if every
@@ -572,7 +615,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->extraLayerCount != encoder->extraLayerCount)) {
+ (lastEncoder->extraLayerCount != encoder->extraLayerCount) || (lastEncoder->width != encoder->width) ||
+ (lastEncoder->height != encoder->height)) {
return AVIF_FALSE;
}
@@ -1587,6 +1631,129 @@
return avifCodecTypeFromChoice(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
}
+static avifResult avifEncoderValidateSize(avifEncoder * encoder, uint32_t gridCols, uint32_t gridRows, const avifImage * firstCell)
+{
+ if (!avifEncoderSizeIsSet(encoder)) {
+ // First image, encoder->data->imageMetadata not initialized yet, nothing to check against.
+ if (encoder->data->items.count == 0) {
+ return AVIF_RESULT_OK;
+ }
+
+ // For animation, the encoder verifies the input size never changes, so this check is redundant.
+ // But for a layered image, the encoder relaxes the check to allow layers to have different sizes.
+ // We support 2 ways to produce different sized layers:
+ // 1. Send full sized inputs and let the encoder scale them via scalingMode
+ // 2. Declare the full size via width/height and send pre-scaled inputs
+ // To avoid confusion, we require the user to consistently use only one of the 2 ways,
+ // and this check blocks the attempt to start with method 1 and try to switch to method 2.
+ if ((firstCell->width != encoder->data->imageMetadata->width) || (firstCell->height != encoder->data->imageMetadata->height)) {
+ avifDiagnosticsPrintf(&encoder->diag, "All images must have the same width/height unless avifEncoder.width/height is set");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
+ // According to section 2.2.2 of AV1 Image File Format specification v1.2.0:
+ // [...] the values of image_width and image_height shall respectively equal the values of
+ // UpscaledWidth and FrameHeight as defined in [AV1] but for a specific frame in the item
+ // payload. [...]
+ // In the absence of a 'lsel' property associated with the item, or if it is present and its
+ // layer_id value is set to 0xFFFF:
+ // If no OperatingPointSelectorProperty is associated with the item, the 'ispe' property
+ // shall document the dimensions of the last frame decoded when processing the operating
+ // point whose index is 0.
+ // NOTE: The dimensions of possible intermediate output images might not match the ones given
+ // in the 'ispe' property. If renderers display these intermediate images, they are expected
+ // to scale the output image to match the 'ispe' property.
+ // See https://aomediacodec.github.io/av1-avif/v1.2.0.html#image-spatial-extents-property.
+ //
+ // Therefore the last layer must not have any scaling.
+ if ((encoder->data->frames.count == encoder->extraLayerCount) && !avifScalingModeIsNoScaling(&encoder->scalingMode)) {
+ avifDiagnosticsPrintf(&encoder->diag,
+ "The last layer must not be scaled, but got scalingMode=(%d/%d, %d/%d)",
+ encoder->scalingMode.horizontal.n,
+ encoder->scalingMode.horizontal.d,
+ encoder->scalingMode.vertical.n,
+ encoder->scalingMode.vertical.d);
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+ return AVIF_RESULT_OK;
+ }
+
+ if ((encoder->width == 0) || (encoder->height == 0)) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width and avifEncoder.height must either both be zero or both be nonzero");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
+ if ((gridCols > 1) || (gridRows > 1)) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width/height cannot be set with grid images");
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
+ // This blocks the attempt to use both ways to produce different sized layers together,
+ // or start with method 2 and try to switch to method 1,
+ // so the result is INVALID_ARGUMENT. See the comment above for the detail.
+ if (!avifScalingModeIsNoScaling(&encoder->scalingMode)) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width/height cannot be set together with encoder->scalingMode");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
+ if (encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width/height cannot be set with sample transforms");
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
+ if (firstCell->gainMap && firstCell->gainMap->image) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width/height cannot be set with gain maps");
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
+ // These 3 checks below are based on section 2.2.2 of AV1 Image File Format specification v1.2.0.
+ // See the comment above for the detail.
+ // Only a layered image can have frames of different sizes, so reject otherwise.
+ if (encoder->extraLayerCount == 0) {
+ avifDiagnosticsPrintf(&encoder->diag, "avifEncoder.width/height can only be set for layered images (extraLayerCount > 0)");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
+ // The spec allows arbitrary sizes. We tighten the rule to "earlier layers shall be smaller" due to encoder restrictions.
+ if ((encoder->width < firstCell->width) || (encoder->height < firstCell->height)) {
+ avifDiagnosticsPrintf(&encoder->diag,
+ "avifEncoder.width/height %ux%u must be at least the coded image size %ux%u",
+ encoder->width,
+ encoder->height,
+ firstCell->width,
+ firstCell->height);
+ return AVIF_RESULT_INCOMPATIBLE_IMAGE;
+ }
+
+ // The declared width/height must exactly match the coded size of the last layer.
+ if ((encoder->data->frames.count == encoder->extraLayerCount) &&
+ ((encoder->width != firstCell->width) || (encoder->height != firstCell->height))) {
+ avifDiagnosticsPrintf(&encoder->diag,
+ "avifEncoder.width/height %ux%u must exactly match the coded size %ux%u of the last layer",
+ encoder->width,
+ encoder->height,
+ firstCell->width,
+ firstCell->height);
+ return AVIF_RESULT_INCOMPATIBLE_IMAGE;
+ }
+
+ if (firstCell->transformFlags & AVIF_TRANSFORM_CLAP) {
+ avifCropRect cropRect;
+ if (!avifCropRectFromCleanApertureBox(&cropRect, &firstCell->clap, encoder->width, encoder->height, &encoder->diag)) {
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+ }
+
+ if ((encoder->data->items.count > 0) && (encoder->extraLayerCount > 0) &&
+ !avifImageHasEquivalentTransformProperties(firstCell, encoder->data->imageMetadata)) {
+ avifDiagnosticsPrintf(&encoder->diag,
+ "When avifEncoder.width/height is set, 'pasp', 'clap', 'irot' and 'imir' must match across layers");
+ return AVIF_RESULT_INCOMPATIBLE_IMAGE;
+ }
+
+ return AVIF_RESULT_OK;
+}
+
// 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,
@@ -1769,6 +1936,8 @@
return AVIF_RESULT_NO_CONTENT;
}
+ AVIF_CHECKRES(avifEncoderValidateSize(encoder, gridCols, gridRows, firstCell));
+
AVIF_CHECKRES(avifValidateGrid(gridCols, gridRows, cellImages, /*validateGainMap=*/AVIF_FALSE, &encoder->diag));
const avifBool hasGainMap = (firstCell->gainMap && firstCell->gainMap->image != NULL);
@@ -1822,7 +1991,7 @@
encoder->data->singleImage = AVIF_TRUE;
if (encoder->extraLayerCount > 0) {
- // AVIF_ADD_IMAGE_FLAG_SINGLE may not be set for layered image.
+ // AVIF_ADD_IMAGE_FLAG_SINGLE may not be set for a layered image.
return AVIF_RESULT_INVALID_ARGUMENT;
}
@@ -1899,6 +2068,10 @@
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 (avifEncoderSizeIsSet(encoder)) {
+ encoder->data->imageMetadata->width = encoder->width;
+ encoder->data->imageMetadata->height = encoder->height;
+ }
const uint32_t gridWidth = avifGridWidth(gridCols, firstCell, bottomRightCell);
const uint32_t gridHeight = avifGridHeight(gridRows, firstCell, bottomRightCell);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 96ca5d7..8a3dd79 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -220,6 +220,7 @@
add_avif_fuzztest(avif_fuzztest_dec_incr gtest/avifincrtest_helpers.cc)
add_avif_fuzztest(avif_fuzztest_enc_dec)
add_avif_fuzztest(avif_fuzztest_enc_dec_anim)
+ add_avif_fuzztest(avif_fuzztest_enc_dec_layered)
add_avif_fuzztest(avif_fuzztest_enc_dec_incr gtest/avifincrtest_helpers.cc)
add_avif_fuzztest(avif_fuzztest_properties)
add_avif_fuzztest(avif_fuzztest_read_image)
diff --git a/tests/data/kodim03_yuv420_8bpc_384x256.y4m b/tests/data/kodim03_yuv420_8bpc_384x256.y4m
new file mode 100644
index 0000000..3301162
--- /dev/null
+++ b/tests/data/kodim03_yuv420_8bpc_384x256.y4m
Binary files differ
diff --git a/tests/gtest/avif_fuzztest_enc_dec_layered.cc b/tests/gtest/avif_fuzztest_enc_dec_layered.cc
new file mode 100644
index 0000000..ddc2eb0
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_enc_dec_layered.cc
@@ -0,0 +1,217 @@
+// Copyright 2026 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+#include "avif/avif.h"
+#include "avif_fuzztest_helpers.h"
+#include "aviftest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+namespace avif {
+namespace testutil {
+namespace {
+
+constexpr size_t kLayerCount = kMaxNumLayers;
+
+constexpr avifFraction kNoScale = {1, 1};
+constexpr size_t kRandomScalingModeCount = kLayerCount - 1;
+static_assert(kRandomScalingModeCount == 3);
+constexpr std::array<avifFraction, 6> kSupportedScalingModes = {
+ avifFraction{1, 8}, avifFraction{1, 4}, avifFraction{1, 2},
+ avifFraction{3, 5}, avifFraction{3, 4}, avifFraction{4, 5}};
+constexpr avifScalingMode kNoScalingMode = {kNoScale, kNoScale};
+constexpr std::array<avifScalingMode, kLayerCount> kNoScalingModes = {
+ kNoScalingMode, kNoScalingMode, kNoScalingMode, kNoScalingMode};
+
+constexpr avifScalingMode MakeScalingMode(avifFraction horizontal,
+ avifFraction vertical) {
+ return {horizontal, vertical};
+}
+
+constexpr size_t BinomialCoefficient(size_t n, size_t k) {
+ if (k > n) {
+ return 0;
+ }
+ if (k > (n - k)) {
+ k = n - k;
+ }
+ size_t coefficient = 1;
+ for (size_t i = 1; i <= k; ++i) {
+ coefficient = (coefficient * (n - k + i)) / i;
+ }
+ return coefficient;
+}
+
+constexpr size_t CombinationCountWithRepetition(size_t value_count,
+ size_t pick_count) {
+ return BinomialCoefficient(value_count + pick_count - 1, pick_count);
+}
+
+constexpr size_t kScalingModeCombinationCount = CombinationCountWithRepetition(
+ kSupportedScalingModes.size(), kRandomScalingModeCount);
+
+std::array<avifFraction, kRandomScalingModeCount> GetScalingModeCombination(
+ size_t combination_index) {
+ std::array<avifFraction, kRandomScalingModeCount> combination = {};
+ size_t remaining_index = combination_index;
+ size_t next_candidate = 0;
+ for (size_t slot = 0; slot < combination.size(); ++slot) {
+ const size_t remaining_slots = combination.size() - slot - 1;
+ for (size_t candidate = next_candidate;
+ candidate < kSupportedScalingModes.size(); ++candidate) {
+ const size_t combinations_with_candidate = CombinationCountWithRepetition(
+ kSupportedScalingModes.size() - candidate, remaining_slots);
+ if (remaining_index < combinations_with_candidate) {
+ combination[slot] = kSupportedScalingModes[candidate];
+ next_candidate = candidate;
+ break;
+ }
+ remaining_index -= combinations_with_candidate;
+ }
+ }
+ return combination;
+}
+
+std::array<avifScalingMode, kLayerCount> MakeRandomScalingModes(
+ int horizontal_combination_index, int vertical_combination_index) {
+ const auto horizontal_combination =
+ GetScalingModeCombination(horizontal_combination_index);
+ const auto vertical_combination =
+ GetScalingModeCombination(vertical_combination_index);
+ return std::array<avifScalingMode, kLayerCount>{
+ MakeScalingMode(horizontal_combination[0], vertical_combination[0]),
+ MakeScalingMode(horizontal_combination[1], vertical_combination[1]),
+ MakeScalingMode(horizontal_combination[2], vertical_combination[2]),
+ kNoScalingMode};
+}
+
+inline auto ArbitraryScalingModes() {
+ constexpr int kMaxCombinationIndex =
+ static_cast<int>(kScalingModeCombinationCount - 1);
+ return fuzztest::Map(MakeRandomScalingModes,
+ fuzztest::InRange<int>(0, kMaxCombinationIndex),
+ fuzztest::InRange<int>(0, kMaxCombinationIndex));
+}
+
+void EncodeDecodeLayered(
+ std::vector<ImagePtr> layers,
+ const std::array<avifScalingMode, kLayerCount>& scaling_modes,
+ uint32_t expected_width, uint32_t expected_height, bool set_last_layer_size,
+ EncoderPtr encoder, DecoderPtr decoder) {
+ ASSERT_EQ(layers.size(), kLayerCount);
+ ASSERT_NE(encoder, nullptr);
+ ASSERT_NE(decoder, nullptr);
+
+ if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) ==
+ nullptr) {
+ return;
+ }
+ if (avifLibYUVVersion() == 0) {
+ return;
+ }
+
+ const avifImage* const reference = layers.front().get();
+ ASSERT_NE(reference, nullptr);
+
+ encoder->codecChoice = AVIF_CODEC_CHOICE_AOM;
+ encoder->extraLayerCount = static_cast<uint32_t>(layers.size() - 1);
+ if (set_last_layer_size) {
+ encoder->width = expected_width;
+ encoder->height = expected_height;
+ }
+
+ for (size_t i = 0; i < layers.size(); ++i) {
+ ASSERT_NE(layers[i].get(), nullptr);
+ encoder->scalingMode = scaling_modes[i];
+ const avifResult result = avifEncoderAddImage(
+ encoder.get(), layers[i].get(),
+ /*durationInTimescales=*/1, AVIF_ADD_IMAGE_FLAG_NONE);
+ ASSERT_EQ(result, AVIF_RESULT_OK)
+ << " layer " << i << ": " << avifResultToString(result) << ": "
+ << encoder->diag.error;
+ }
+
+ AvifRwData encoded_data;
+ avifResult result = avifEncoderFinish(encoder.get(), &encoded_data);
+ ASSERT_EQ(result, AVIF_RESULT_OK)
+ << avifResultToString(result) << ": " << encoder->diag.error;
+
+ result = avifDecoderSetIOMemory(decoder.get(), encoded_data.data,
+ encoded_data.size);
+ ASSERT_EQ(result, AVIF_RESULT_OK)
+ << avifResultToString(result) << ": " << decoder->diag.error;
+
+ result = avifDecoderParse(decoder.get());
+ ASSERT_EQ(result, AVIF_RESULT_OK)
+ << avifResultToString(result) << ": " << decoder->diag.error;
+
+ const int num_decodes =
+ decoder->allowProgressive ? static_cast<int>(layers.size()) : 1;
+ if (decoder->allowProgressive) {
+ EXPECT_EQ(decoder->progressiveState, AVIF_PROGRESSIVE_STATE_ACTIVE);
+ ASSERT_EQ(decoder->imageCount, static_cast<int>(layers.size()));
+ } else {
+ EXPECT_EQ(decoder->progressiveState, AVIF_PROGRESSIVE_STATE_AVAILABLE);
+ ASSERT_EQ(decoder->imageCount, 1);
+ }
+
+ for (int i = 0; i < num_decodes; ++i) {
+ result = avifDecoderNextImage(decoder.get());
+ ASSERT_EQ(result, AVIF_RESULT_OK)
+ << " layer " << i << ": " << avifResultToString(result) << ": "
+ << decoder->diag.error;
+ EXPECT_EQ(decoder->image->width, expected_width);
+ EXPECT_EQ(decoder->image->height, expected_height);
+ EXPECT_EQ(decoder->image->depth, reference->depth);
+ EXPECT_EQ(decoder->image->yuvFormat, reference->yuvFormat);
+ }
+ result = avifDecoderNextImage(decoder.get());
+ ASSERT_EQ(result, AVIF_RESULT_NO_IMAGES_REMAINING)
+ << avifResultToString(result) << ": " << decoder->diag.error;
+}
+
+void EncodeDecodeDimensionChange(
+ std::vector<ImagePtr> layers,
+ const std::array<avifScalingMode, kLayerCount>& scaling_modes,
+ EncoderPtr encoder, DecoderPtr decoder) {
+ ASSERT_EQ(layers.size(), kLayerCount);
+ ASSERT_NE(layers.back().get(), nullptr);
+ const uint32_t expected_width = layers.back()->width;
+ const uint32_t expected_height = layers.back()->height;
+ EncodeDecodeLayered(std::move(layers), scaling_modes,
+ /*expected_width=*/expected_width,
+ /*expected_height=*/expected_height,
+ /*set_last_layer_size=*/false, std::move(encoder),
+ std::move(decoder));
+}
+
+void EncodeDecodeDimensionChangeExternal(std::vector<ImagePtr> layers,
+ EncoderPtr encoder,
+ DecoderPtr decoder) {
+ ASSERT_EQ(layers.size(), kLayerCount);
+ ASSERT_NE(layers.back().get(), nullptr);
+ const uint32_t expected_width = layers.back()->width;
+ const uint32_t expected_height = layers.back()->height;
+ EncodeDecodeLayered(std::move(layers), kNoScalingModes,
+ /*expected_width=*/expected_width,
+ /*expected_height=*/expected_height,
+ /*set_last_layer_size=*/true, std::move(encoder),
+ std::move(decoder));
+}
+
+FUZZ_TEST(LayeredEncodeDecodeAvifFuzzTest, EncodeDecodeDimensionChange)
+ .WithDomains(ArbitraryAvifLayered(), ArbitraryScalingModes(),
+ ArbitraryAvifEncoder(), ArbitraryAvifDecoder());
+
+FUZZ_TEST(LayeredEncodeDecodeAvifFuzzTest, EncodeDecodeDimensionChangeExternal)
+ .WithDomains(ArbitraryAvifLayeredRandDim(), ArbitraryAvifEncoder(),
+ ArbitraryAvifDecoder());
+
+} // namespace
+} // namespace testutil
+} // namespace avif
diff --git a/tests/gtest/avif_fuzztest_helpers.cc b/tests/gtest/avif_fuzztest_helpers.cc
index 95d7693..e73ab73 100644
--- a/tests/gtest/avif_fuzztest_helpers.cc
+++ b/tests/gtest/avif_fuzztest_helpers.cc
@@ -4,6 +4,7 @@
#include "avif_fuzztest_helpers.h"
#include <algorithm>
+#include <array>
#include <cassert>
#include <cstdint>
#include <cstdlib>
@@ -22,6 +23,27 @@
//------------------------------------------------------------------------------
+constexpr size_t kNumLayeredRandDimSeeds = 2 * (kMaxNumLayers - 1);
+
+template <typename Sample>
+std::array<std::pair<size_t, size_t>, kMaxNumLayers> GetAvifLayeredRandDims(
+ size_t display_width, size_t display_height, const Sample* seeds) {
+ assert(display_width > 1);
+ assert(display_height > 1);
+ std::array<std::pair<size_t, size_t>, kMaxNumLayers> sizes = {};
+ for (size_t i = 0; i + 1 < kMaxNumLayers; ++i) {
+ const size_t width =
+ 1 + (static_cast<size_t>(seeds[2 * i]) % (display_width - 1));
+ const size_t height =
+ 1 + (static_cast<size_t>(seeds[2 * i + 1]) % (display_height - 1));
+ assert(width < display_width);
+ assert(height < display_height);
+ sizes[i] = {width, height};
+ }
+ sizes.back() = {display_width, display_height};
+ return sizes;
+}
+
ImagePtr CreateAvifImage(size_t width, size_t height, int depth,
avifPixelFormat pixel_format, bool has_alpha,
const uint8_t* samples) {
@@ -104,6 +126,76 @@
return frames;
}
+std::vector<ImagePtr> CreateAvifLayered8b(size_t width, size_t height,
+ avifPixelFormat pixel_format,
+ bool has_alpha,
+ const std::vector<uint8_t>& samples) {
+ std::vector<ImagePtr> layers = CreateAvifAnim8b(
+ kMaxNumLayers, width, height, pixel_format, has_alpha, samples);
+ return layers;
+}
+
+std::vector<ImagePtr> CreateAvifLayered16b(
+ size_t width, size_t height, int depth, avifPixelFormat pixel_format,
+ bool has_alpha, const std::vector<uint16_t>& samples) {
+ std::vector<ImagePtr> layers = CreateAvifAnim16b(
+ kMaxNumLayers, width, height, depth, pixel_format, has_alpha, samples);
+ return layers;
+}
+
+size_t GetNumSamplesLayeredRandDim(size_t display_width, size_t display_height,
+ avifPixelFormat pixel_format,
+ bool has_alpha) {
+ return kNumLayeredRandDimSeeds + GetNumSamples(kMaxNumLayers, display_width,
+ display_height, pixel_format,
+ has_alpha);
+}
+
+std::vector<ImagePtr> CreateAvifLayeredRandDim8b(
+ size_t display_width, size_t display_height, avifPixelFormat pixel_format,
+ bool has_alpha, const std::vector<uint8_t>& samples) {
+ assert(samples.size() >= kNumLayeredRandDimSeeds);
+ const auto dims =
+ GetAvifLayeredRandDims(display_width, display_height, samples.data());
+ std::vector<ImagePtr> layers;
+ layers.reserve(kMaxNumLayers);
+
+ size_t offset = kNumLayeredRandDimSeeds;
+ for (const auto& [width, height] : dims) {
+ const size_t num_samples = GetNumSamples(
+ /*num_frames=*/1, width, height, pixel_format, has_alpha);
+ layers.push_back(CreateAvifImage8b(
+ width, height, pixel_format, has_alpha,
+ std::vector<uint8_t>(samples.begin() + offset,
+ samples.begin() + offset + num_samples)));
+ offset += num_samples;
+ }
+ return layers;
+}
+
+std::vector<ImagePtr> CreateAvifLayeredRandDim16b(
+ size_t display_width, size_t display_height, int depth,
+ avifPixelFormat pixel_format, bool has_alpha,
+ const std::vector<uint16_t>& samples) {
+ assert(samples.size() >= kNumLayeredRandDimSeeds);
+ const auto dims =
+ GetAvifLayeredRandDims(display_width, display_height, samples.data());
+ std::vector<ImagePtr> layers;
+ layers.reserve(kMaxNumLayers);
+
+ size_t offset = kNumLayeredRandDimSeeds;
+ for (const auto& [width, height] : dims) {
+ const size_t num_samples = GetNumSamples(
+ /*num_frames=*/1, width, height, pixel_format, has_alpha);
+ layers.push_back(CreateAvifImage16b(
+ width, height, depth, pixel_format, has_alpha,
+ std::vector<uint16_t>(samples.begin() + offset,
+ samples.begin() + offset + num_samples)));
+ offset += num_samples;
+ }
+ return layers;
+}
+
EncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads,
int quality, int quality_alpha, int tile_rows_log2,
int tile_cols_log2, int speed) {
diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h
index 6a8fbf5..849f368 100644
--- a/tests/gtest/avif_fuzztest_helpers.h
+++ b/tests/gtest/avif_fuzztest_helpers.h
@@ -58,6 +58,20 @@
avifPixelFormat pixel_format,
bool has_alpha,
const std::vector<uint16_t>& samples);
+std::vector<ImagePtr> CreateAvifLayered8b(size_t width, size_t height,
+ avifPixelFormat pixel_format,
+ bool has_alpha,
+ const std::vector<uint8_t>& samples);
+std::vector<ImagePtr> CreateAvifLayered16b(
+ size_t width, size_t height, int depth, avifPixelFormat pixel_format,
+ bool has_alpha, const std::vector<uint16_t>& samples);
+std::vector<ImagePtr> CreateAvifLayeredRandDim8b(
+ size_t display_width, size_t display_height, avifPixelFormat pixel_format,
+ bool has_alpha, const std::vector<uint8_t>& samples);
+std::vector<ImagePtr> CreateAvifLayeredRandDim16b(
+ size_t display_width, size_t display_height, int depth,
+ avifPixelFormat pixel_format, bool has_alpha,
+ const std::vector<uint16_t>& samples);
EncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads,
int quality, int quality_alpha, int tile_rows_log2,
int tile_cols_log2, int speed);
@@ -84,9 +98,14 @@
// Do not generate animations with more than this number of frames.
inline constexpr size_t kMaxNumFrames =
kMaxNumFramesSquareRoot * kMaxNumFramesSquareRoot;
+// Do not generate layered images with more than this number of layers.
+inline constexpr size_t kMaxNumLayers = AVIF_MAX_AV1_LAYER_COUNT;
size_t GetNumSamples(size_t num_frames, size_t width, size_t height,
avifPixelFormat pixel_format, bool has_alpha);
+size_t GetNumSamplesLayeredRandDim(size_t display_width, size_t display_height,
+ avifPixelFormat pixel_format,
+ bool has_alpha);
// To avoid using fuzztest::internal, the return type of the functions below is
// auto.
@@ -186,6 +205,107 @@
return fuzztest::OneOf(ArbitraryAvifAnim8b(), ArbitraryAvifAnim16b());
}
+// Layered avifImage generator type: fixed number of layers, same width/height,
+// pixel format and 8-bit samples.
+inline auto ArbitraryAvifLayered8b() {
+ constexpr uint16_t kMinLayerDimension = 8;
+ constexpr uint16_t kMaxLayerDimension =
+ kMaxDimension / kMaxNumFramesSquareRoot;
+ return fuzztest::FlatMap(
+ [](size_t width, size_t height, avifPixelFormat pixel_format,
+ bool has_alpha) {
+ return fuzztest::Map(
+ CreateAvifLayered8b, fuzztest::Just(width), fuzztest::Just(height),
+ fuzztest::Just(pixel_format), fuzztest::Just(has_alpha),
+ fuzztest::Arbitrary<std::vector<uint8_t>>().WithSize(GetNumSamples(
+ kMaxNumLayers, width, height, pixel_format, has_alpha)));
+ },
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ ArbitraryPixelFormat(), fuzztest::Arbitrary<bool>());
+}
+
+// Layered avifImage generator type: fixed number of layers, same width/height,
+// depth, pixel format and 16-bit samples.
+inline auto ArbitraryAvifLayered16b() {
+ constexpr uint16_t kMinLayerDimension = 8;
+ constexpr uint16_t kMaxLayerDimension =
+ kMaxDimension / kMaxNumFramesSquareRoot;
+ return fuzztest::FlatMap(
+ [](size_t width, size_t height, int depth, avifPixelFormat pixel_format,
+ bool has_alpha) {
+ return fuzztest::Map(
+ CreateAvifLayered16b, fuzztest::Just(width), fuzztest::Just(height),
+ fuzztest::Just(depth), fuzztest::Just(pixel_format),
+ fuzztest::Just(has_alpha),
+ fuzztest::ContainerOf<std::vector<uint16_t>>(
+ fuzztest::InRange<uint16_t>(0, (1 << depth) - 1))
+ .WithSize(GetNumSamples(kMaxNumLayers, width, height,
+ pixel_format, has_alpha)));
+ },
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::ElementOf({10, 12}), ArbitraryPixelFormat(),
+ fuzztest::Arbitrary<bool>());
+}
+
+// Generator for an arbitrary layered still image with same-sized layers.
+inline auto ArbitraryAvifLayered() {
+ return fuzztest::OneOf(ArbitraryAvifLayered8b(), ArbitraryAvifLayered16b());
+}
+
+// Layered avifImage generator type: fixed number of layers, display size,
+// pixel format and 8-bit samples.
+inline auto ArbitraryAvifLayeredRandDim8b() {
+ constexpr uint16_t kMinLayerDimension = 8;
+ constexpr uint16_t kMaxLayerDimension =
+ kMaxDimension / kMaxNumFramesSquareRoot;
+ return fuzztest::FlatMap(
+ [](size_t display_width, size_t display_height,
+ avifPixelFormat pixel_format, bool has_alpha) {
+ return fuzztest::Map(
+ CreateAvifLayeredRandDim8b, fuzztest::Just(display_width),
+ fuzztest::Just(display_height), fuzztest::Just(pixel_format),
+ fuzztest::Just(has_alpha),
+ fuzztest::Arbitrary<std::vector<uint8_t>>().WithSize(
+ GetNumSamplesLayeredRandDim(display_width, display_height,
+ pixel_format, has_alpha)));
+ },
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ ArbitraryPixelFormat(), fuzztest::Arbitrary<bool>());
+}
+
+// Layered avifImage generator type: fixed number of layers, display size,
+// depth, pixel format and 16-bit samples.
+inline auto ArbitraryAvifLayeredRandDim16b() {
+ constexpr uint16_t kMinLayerDimension = 8;
+ constexpr uint16_t kMaxLayerDimension =
+ kMaxDimension / kMaxNumFramesSquareRoot;
+ return fuzztest::FlatMap(
+ [](size_t display_width, size_t display_height, int depth,
+ avifPixelFormat pixel_format, bool has_alpha) {
+ return fuzztest::Map(
+ CreateAvifLayeredRandDim16b, fuzztest::Just(display_width),
+ fuzztest::Just(display_height), fuzztest::Just(depth),
+ fuzztest::Just(pixel_format), fuzztest::Just(has_alpha),
+ fuzztest::ContainerOf<std::vector<uint16_t>>(
+ fuzztest::InRange<uint16_t>(0, (1 << depth) - 1))
+ .WithSize(GetNumSamplesLayeredRandDim(
+ display_width, display_height, pixel_format, has_alpha)));
+ },
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::InRange<uint16_t>(kMinLayerDimension, kMaxLayerDimension),
+ fuzztest::ElementOf({10, 12}), ArbitraryPixelFormat(),
+ fuzztest::Arbitrary<bool>());
+}
+
+// Generator for an arbitrary layered still image with display size override.
+inline auto ArbitraryAvifLayeredRandDim() {
+ return fuzztest::OneOf(ArbitraryAvifLayeredRandDim8b(),
+ ArbitraryAvifLayeredRandDim16b());
+}
+
// Generates two signed fractions where the first one is smaller than or equal
// to the second one.
inline auto ArbitraryMinMaxSignedFraction() {
diff --git a/tests/gtest/avifchangesettingtest.cc b/tests/gtest/avifchangesettingtest.cc
index 485a87b..de4ffe4 100644
--- a/tests/gtest/avifchangesettingtest.cc
+++ b/tests/gtest/avifchangesettingtest.cc
@@ -241,6 +241,42 @@
AVIF_RESULT_INCOMPATIBLE_IMAGE);
}
+TEST(ChangeSettingTest, UnchangeableEncoderSize) {
+ if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) ==
+ nullptr) {
+ GTEST_SKIP() << "Codec unavailable, skip test.";
+ }
+
+ ImagePtr image = testutil::CreateImage(/*width=*/64, /*height=*/64,
+ /*depth=*/8, AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+ ASSERT_NE(image, nullptr);
+ testutil::FillImageGradient(image.get());
+
+ EncoderPtr encoder(avifEncoderCreate());
+ ASSERT_NE(encoder, nullptr);
+ encoder->codecChoice = AVIF_CODEC_CHOICE_AOM;
+ encoder->speed = AVIF_SPEED_FASTEST;
+ encoder->extraLayerCount = 1;
+ encoder->width = 128;
+ encoder->height = 128;
+
+ ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_OK);
+
+ ImagePtr image2 = testutil::CreateImage(/*width=*/256, /*height=*/128,
+ /*depth=*/8, AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+ ASSERT_NE(image2, nullptr);
+ testutil::FillImageGradient(image2.get());
+
+ encoder->width = 256;
+ ASSERT_EQ(avifEncoderAddImage(encoder.get(), image2.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_CANNOT_CHANGE_SETTING);
+}
+
void EncodeAnimation(const char* key, const char* value_before_first_frame,
const char* value_after_first_frame,
const char* value_before_second_frame,
diff --git a/tests/gtest/avifprogressivetest.cc b/tests/gtest/avifprogressivetest.cc
index 3071b56..641a9d9 100644
--- a/tests/gtest/avifprogressivetest.cc
+++ b/tests/gtest/avifprogressivetest.cc
@@ -129,7 +129,7 @@
// `ms_params->ms_buffers.ref->stride == ms_params->search_sites->stride'
// failed.
// See https://aomedia.googlesource.com/aom/+/945edd671.
-TEST_F(ProgressiveTest, DimensionChange) {
+TEST_F(ProgressiveTest, ResolutionChange) {
if (avifLibYUVVersion() == 0) {
GTEST_SKIP() << "libyuv not available, skip test.";
}
@@ -152,7 +152,7 @@
TestDecode(kImageSize, kImageSize);
}
-TEST_F(ProgressiveTest, DimensionChangeWithAlpha) {
+TEST_F(ProgressiveTest, ResolutionChangeWithAlpha) {
if (avifLibYUVVersion() == 0) {
GTEST_SKIP() << "libyuv not available, skip test.";
}
@@ -187,6 +187,112 @@
TestDecode(kImageSize, kImageSize, /*expect_alpha=*/true);
}
+TEST_F(ProgressiveTest, ResolutionChangeExternal) {
+ encoder_->extraLayerCount = 1;
+ encoder_->width = kImageSize;
+ encoder_->height = kImageSize;
+
+ const auto smallImage = testutil::CreateImage(
+ kImageSize / 2, kImageSize / 2, 8, AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+ ASSERT_NE(smallImage, nullptr);
+ testutil::FillImageGradient(smallImage.get(), /*offset=*/17);
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), smallImage.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(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+ TestDecode(256, 256);
+}
+
+TEST_F(ProgressiveTest, ResolutionChangeExternalWithAlpha) {
+ const auto image =
+ testutil::CreateImage(kImageSize, kImageSize, 8, AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_ALL, AVIF_RANGE_FULL);
+ ASSERT_NE(image, nullptr);
+ testutil::FillImageGradient(image.get());
+
+ encoder_->extraLayerCount = 2;
+ encoder_->width = kImageSize;
+ encoder_->height = kImageSize;
+
+ auto smallImage = testutil::CreateImage(kImageSize / 2, kImageSize / 2, 8,
+ AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_ALL, AVIF_RANGE_FULL);
+ ASSERT_NE(smallImage, nullptr);
+ testutil::FillImageGradient(smallImage.get(), /*offset=*/17);
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), smallImage.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_OK);
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), smallImage.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(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+ TestDecode(kImageSize, kImageSize, /*expect_alpha=*/true);
+}
+
+TEST_F(ProgressiveTest, ResolutionChangeExternalBadSize) {
+ encoder_->extraLayerCount = 1;
+ encoder_->width = kImageSize * 2;
+ encoder_->height = kImageSize * 2;
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_OK);
+
+ // Last layer does not match the pre-declared size
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_INCOMPATIBLE_IMAGE);
+}
+
+// Reject mixed usage of internal and external resolution change.
+TEST_F(ProgressiveTest, ResolutionChangeMixedRejected) {
+ encoder_->extraLayerCount = 1;
+ encoder_->width = kImageSize;
+ encoder_->height = kImageSize;
+ encoder_->scalingMode = {{1, 2}, {1, 2}};
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_INVALID_ARGUMENT);
+}
+
+TEST_F(ProgressiveTest, ResolutionChangeMixedDuringEncodingRejected) {
+ encoder_->extraLayerCount = 2;
+ encoder_->width = kImageSize;
+ encoder_->height = kImageSize;
+
+ const auto smallImage = testutil::CreateImage(
+ kImageSize / 2, kImageSize / 2, 8, AVIF_PIXEL_FORMAT_YUV444,
+ AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+ ASSERT_NE(smallImage, nullptr);
+ testutil::FillImageGradient(smallImage.get(), /*offset=*/17);
+
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), smallImage.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_OK);
+
+ encoder_->scalingMode = {{1, 2}, {1, 2}};
+ ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+ AVIF_ADD_IMAGE_FLAG_NONE),
+ AVIF_RESULT_INVALID_ARGUMENT);
+}
+
TEST_F(ProgressiveTest, LayeredGrid) {
encoder_->extraLayerCount = 1;
encoder_->quality = 21;
diff --git a/tests/test_cmd_progressive.sh b/tests/test_cmd_progressive.sh
index 6492ee0..c2e1583 100755
--- a/tests/test_cmd_progressive.sh
+++ b/tests/test_cmd_progressive.sh
@@ -22,14 +22,16 @@
# Input file paths.
INPUT_Y4M="${TESTDATA_DIR}/kodim03_yuv420_8bpc.y4m"
+INPUT_SMALL_Y4M="${TESTDATA_DIR}/kodim03_yuv420_8bpc_384x256.y4m"
# Output file names.
ENCODED_FILE="avif_test_cmd_encoded.avif"
DECODED_FILE="avif_test_cmd_decoded.png"
+INFO_FILE="avif_test_cmd_info.txt"
# Cleanup
cleanup() {
pushd ${TMP_DIR}
- rm -- "${ENCODED_FILE}" "${DECODED_FILE}"
+ rm -- "${ENCODED_FILE}" "${DECODED_FILE}" "${INFO_FILE}"
popd
}
trap cleanup EXIT
@@ -55,6 +57,22 @@
done
fi
+ echo "Testing manual layered encoding with pre-scaled input"
+ "${AVIFENC}" -s 8 --layered \
+ "${INPUT_SMALL_Y4M}" "${INPUT_Y4M}" -o "${ENCODED_FILE}"
+ "${AVIFDEC}" --info "${ENCODED_FILE}" > "${INFO_FILE}"
+ grep -F "[768x512]" "${INFO_FILE}"
+ "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
+ "${AVIFDEC}" --progressive "${ENCODED_FILE}" "${DECODED_FILE}"
+
+ echo "Testing an earlier layer larger than the last layer"
+ "${AVIFENC}" -s 8 --layered \
+ "${INPUT_Y4M}" "${INPUT_SMALL_Y4M}" -o "${ENCODED_FILE}" && exit 1
+
+ echo "Testing layered encoding with pre-scaled input and frame scaling (rejected)"
+ "${AVIFENC}" -s 8 --layered --scaling-mode:u 1/2 \
+ "${INPUT_SMALL_Y4M}" "${INPUT_Y4M}" -o "${ENCODED_FILE}" && exit 1
+
echo "Testing too few layers"
"${AVIFENC}" -s 8 --layered -q:u 60 "${INPUT_Y4M}" -o "${ENCODED_FILE}" && exit 1