Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The changes are relative to the previous release, unless the baseline is specifi

* Add the ignoreICC option to avifDecoder
* Support ignoring alpha in avifDecoder::imageContentToDecode
* Support encoding layered image with pre-scaled inputs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuan: Can you rebase this pull request to the tip of the main branch? This will fix the build-android-jni CI workflow failure. Do not make any other changes, because I am about to finish my review and send you the final review comments. Thanks!

Status update: I may not have time to send you the final review comments until this weekend. I have reviewed everything except avifenc.c. I only have some questions about avifImagePeek() and the new parameters of avifImageDump(). I will ask you to just add some comments so we can merge this PR with minimal changes. But I will also suggest possible follow-up cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me do a merge instead so the commits you are looking at won't change. You probably will squash when merging this PR, so there shouldn't be other differences.

@tongyuantongyu tongyuantongyu Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me guess your questions:

About avifPeekImage(): the semi-public API avifReadImage() is already getting a bit too many arguments, so I avoided overloading it further.

avifAppFileFormat avifReadImage(const char * filename,
avifAppFileFormat inputFormat,
avifPixelFormat requestedFormat,
int requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreAlpha,
avifBool ignoreGainMap,
uint32_t imageSizeLimit,
avifImage * image,
uint32_t * outDepth,
avifAppSourceTiming * sourceTiming,
struct y4mFrameIterator ** frameIter)


About avifImageDump(): since gain map info need gridCols/gridRows, we cannot precompute the image size and eliminate them:

uint32_t gainMapWidth = gainMapImage->width;
uint32_t gainMapHeight = gainMapImage->height;
if (gridCols && gridRows) {
gainMapWidth *= gridCols;
gainMapHeight *= gridRows;
}

* 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
Expand Down
120 changes: 114 additions & 6 deletions apps/avifenc.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ typedef struct
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
Expand Down Expand Up @@ -454,6 +458,58 @@ static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, uint32_t clapVal
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();
Expand Down Expand Up @@ -860,10 +916,11 @@ static avifBool avifEncodeUpdateEncoderSettings(avifEncoder * encoder, const avi
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,
Expand Down Expand Up @@ -954,7 +1011,11 @@ static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder,
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)) {
Expand Down Expand Up @@ -1061,14 +1122,21 @@ static avifBool avifEncodeRestOfLayeredImage(avifEncoder * encoder,
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,
"ERROR: Layered encoding does not support input with multiple frames: %s.\n",
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)) {
Expand Down Expand Up @@ -1129,6 +1197,8 @@ static avifBool avifEncodeImagesFixedQuality(const avifSettings * settings,
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;
}
Expand Down Expand Up @@ -2381,6 +2451,38 @@ int main(int argc, char * argv[])
goto cleanup;
}

uint32_t outputImageWidth = image->width;
uint32_t outputImageHeight = image->height;
Comment thread
wantehchang marked this conversation as resolved.
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
Expand Down Expand Up @@ -2433,7 +2535,7 @@ int main(int argc, char * argv[])
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;
Expand All @@ -2453,7 +2555,7 @@ int main(int argc, char * argv[])
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,
Expand Down Expand Up @@ -2634,8 +2736,14 @@ int main(int argc, char * argv[])
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;
Comment thread
wantehchang marked this conversation as resolved.
avifImageDump(avif,
cellWidth,
cellHeight,
settings.gridDims[0],
settings.gridDims[1],
settings.layers > 1 ? AVIF_PROGRESSIVE_STATE_AVAILABLE : AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
Expand Down
5 changes: 3 additions & 2 deletions apps/avifgainmaputil/imageio.cc
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
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,
Comment thread
tongyuantongyu marked this conversation as resolved.
/*gridCols=*/1,
/*gridRows=*/1, AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
PrintEncodingSettings(encoder, image->gainMap != nullptr);
Expand Down Expand Up @@ -407,7 +407,8 @@ avifResult WriteAvifGrid(const avifImage* image, int grid_cols, int grid_rows,

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,
Expand Down
45 changes: 39 additions & 6 deletions apps/shared/avifjpeg.c
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,8 @@ static avifBool avifJPEGReadInternal(FILE * f,
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
Expand Down Expand Up @@ -1024,7 +1025,8 @@ static avifBool avifJPEGExtractGainMapImageFromMpf(FILE * f,
/*ignoreExif=*/AVIF_TRUE,
/*ignoreXMP=*/AVIF_FALSE,
/*ignoreGainMap=*/AVIF_TRUE,
sizeLimit)) {
sizeLimit,
/*headerOnly=*/AVIF_FALSE)) {
continue;
}
if (avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size, NULL)) {
Expand Down Expand Up @@ -1261,7 +1263,8 @@ static avifBool avifJPEGReadInternal(FILE * f,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit)
uint32_t sizeLimit,
avifBool headerOnly)
{
volatile avifBool ret = AVIF_FALSE;
uint8_t * volatile iccData = NULL;
Expand Down Expand Up @@ -1305,6 +1308,8 @@ static avifBool avifJPEGReadInternal(FILE * f,
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;
Comment thread
wantehchang marked this conversation as resolved.

if (!ignoreColorProfile) {
uint8_t * iccDataTmp;
Expand Down Expand Up @@ -1336,6 +1341,12 @@ static avifBool avifJPEGReadInternal(FILE * f,
// 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.

Expand All @@ -1351,8 +1362,6 @@ static avifBool avifJPEGReadInternal(FILE * f,
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;
Expand Down Expand Up @@ -1672,13 +1681,37 @@ avifBool avifJPEGRead(const char * inputFilename,
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;
Expand Down
5 changes: 5 additions & 0 deletions apps/shared/avifjpeg.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ avifBool avifJPEGRead(const char * inputFilename,
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)
Expand Down
Loading
Loading