Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow odd clap dimensions and offsets #2426

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ The changes are relative to the previous release, unless the baseline is specifi
avifGainMapMetadataDouble structs.
* Add avif(Un)SignedFraction structs and avifDoubleTo(Un)SignedFraction
utility functions.
* Allow decoding subsampled images with odd Clean Aperture dimensions or offsets.
* Deprecate avifCropRectConvertCleanApertureBox(). Replace it with
avifCropRectFromCleanApertureBox().

## [1.1.1] - 2024-07-30

Expand Down
1 change: 1 addition & 0 deletions android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder,
avifDiagnostics diag;
// If the image does not have a valid 'clap' property, then we simply display
// the whole image.
// TODO: Use avifCropRectFromCleanApertureBox() instead.
if (!(decoder->decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) ||
!avifCropRectConvertCleanApertureBox(
&decoder->crop, &decoder->decoder->image->clap,
Expand Down
18 changes: 18 additions & 0 deletions apps/avifdec.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,24 @@ int main(int argc, char * argv[])
printf("Image details:\n");
avifImageDump(decoder->image, 0, 0, decoder->progressiveState);

if (decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) {
avifCropRect cropRect;
if (avifCropRectFromCleanApertureBox(&cropRect,
&decoder->image->clap,
decoder->image->width,
decoder->image->height,
&decoder->diag)) {
if (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != decoder->image->width ||
cropRect.height != decoder->image->height) {
// TODO: Implement, see https://github.com/AOMediaCodec/libavif/issues/2427
fprintf(stderr, "Warning: Clean Aperture values were ignored, the output image was NOT cropped\n");
}
} else {
// Should happen only if AVIF_STRICT_CLAP_VALID is disabled.
fprintf(stderr, "Warning: Invalid Clean Aperture values\n");
}
}

if (ignoreICC && (decoder->image->icc.size > 0)) {
printf("[--ignore-icc] Discarding ICC profile.\n");
// This cannot fail.
Expand Down
3 changes: 2 additions & 1 deletion apps/avifenc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2292,7 +2292,7 @@ int main(int argc, char * argv[])
avifCropRect cropRect;
avifDiagnostics diag;
avifDiagnosticsClearError(&diag);
if (!avifCropRectConvertCleanApertureBox(&cropRect, &image->clap, image->width, image->height, image->yuvFormat, &diag)) {
if (!avifCropRectFromCleanApertureBox(&cropRect, &image->clap, image->width, image->height, &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 All @@ -2306,6 +2306,7 @@ int main(int argc, char * argv[])
diag.error);
goto cleanup;
}
// avifCropRectRequiresUpsampling() does not need to be called for clap validation.
}
if (irotAngle != 0xff) {
image->transformFlags |= AVIF_TRANSFORM_IROT;
Expand Down
8 changes: 4 additions & 4 deletions apps/shared/avifutil.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uin
avifCropRect cropRect;
avifDiagnostics diag;
avifDiagnosticsClearError(&diag);
avifBool validClap =
avifCropRectConvertCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, avif->yuvFormat, &diag);
avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag);
if (validClap) {
printf(" * Valid, derived crop rect: X: %d, Y: %d, W: %d, H: %d\n",
printf(" * Valid, derived crop rect: X: %d, Y: %d, W: %d, H: %d%s\n",
cropRect.x,
cropRect.y,
cropRect.width,
cropRect.height);
cropRect.height,
avifCropRectRequiresUpsampling(&cropRect, avif->yuvFormat) ? " (upsample before cropping)" : "");
} else {
printf(" * Invalid: %s\n", diag.error);
}
Expand Down
22 changes: 15 additions & 7 deletions include/avif/avif.h
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ typedef struct avifPixelAspectRatioBox
typedef struct avifCleanApertureBox
{
// 'clap' from ISO/IEC 14496-12:2022 12.1.4.3
// Note that ISO/IEC 23000-22:2024 7.3.6.7 requires the decoded image to be upsampled to 4:4:4 before
// clean aperture is applied if a clean aperture size or offset is odd in a subsampled dimension.

// a fractional number which defines the width of the clean aperture image
uint32_t widthN;
Expand Down Expand Up @@ -544,18 +546,24 @@ typedef struct avifCropRect

// These will return AVIF_FALSE if the resultant values violate any standards, and if so, the output
// values are not guaranteed to be complete or correct and should not be used.
AVIF_NODISCARD AVIF_API avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect,
const avifCleanApertureBox * clap,
uint32_t imageW,
uint32_t imageH,
avifPixelFormat yuvFormat,
avifDiagnostics * diag);
AVIF_NODISCARD AVIF_API avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect,
const avifCleanApertureBox * clap,
uint32_t imageW,
uint32_t imageH,
avifDiagnostics * diag);
AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap,
const avifCropRect * cropRect,
uint32_t imageW,
uint32_t imageH,
avifPixelFormat yuvFormat,
avifDiagnostics * diag);
// If this function returns true, the image must be upsampled from 4:2:0 or 4:2:2 to 4:4:4 before
// Clean Aperture values are applied.
AVIF_NODISCARD AVIF_API avifBool avifCropRectRequiresUpsampling(const avifCropRect * cropRect, avifPixelFormat yuvFormat);

// Deprecated. Use avifCropRectFromCleanApertureBox() instead.
AVIF_NODISCARD AVIF_API avifBool
avifCropRectConvertCleanApertureBox(avifCropRect *, const avifCleanApertureBox *, uint32_t, uint32_t, avifPixelFormat, avifDiagnostics *);

// ---------------------------------------------------------------------------
// avifContentLightLevelInformationBox
Expand Down Expand Up @@ -1116,7 +1124,7 @@ typedef enum avifStrictFlag
AVIF_STRICT_PIXI_REQUIRED = (1 << 0),

// This demands that the values surfaced in the clap box are valid, determined by attempting to
// convert the clap box to a crop rect using avifCropRectConvertCleanApertureBox(). If this
// convert the clap box to a crop rect using avifCropRectFromCleanApertureBox(). If this
// function returns AVIF_FALSE and this strict flag is set, the decode will fail.
AVIF_STRICT_CLAP_VALID = (1 << 1),

Expand Down
84 changes: 48 additions & 36 deletions src/avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -735,19 +735,8 @@ static avifBool overflowsInt32(int64_t x)
return (x < INT32_MIN) || (x > INT32_MAX);
}

static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imageW, uint32_t imageH, avifPixelFormat yuvFormat, avifDiagnostics * diag)

static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imageW, uint32_t imageH, avifDiagnostics * diag)
{
// ISO/IEC 23000-22:2019/Amd. 2:2021, Section 7.3.6.7:
// The clean aperture property is restricted according to the chroma
// sampling format of the input image (4:4:4, 4:2:2:, 4:2:0, or 4:0:0) as
// follows:
// ...
// - If chroma is subsampled horizontally (i.e., 4:2:2 and 4:2:0), the
// leftmost pixel of the clean aperture shall be even numbers;
// - If chroma is subsampled vertically (i.e., 4:2:0), the topmost line
// of the clean aperture shall be even numbers.

if ((cropRect->width == 0) || (cropRect->height == 0)) {
avifDiagnosticsPrintf(diag, "[Strict] crop rect width and height must be nonzero");
return AVIF_FALSE;
Expand All @@ -757,28 +746,14 @@ static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imag
avifDiagnosticsPrintf(diag, "[Strict] crop rect is out of the image's bounds");
return AVIF_FALSE;
}

if ((yuvFormat == AVIF_PIXEL_FORMAT_YUV420) || (yuvFormat == AVIF_PIXEL_FORMAT_YUV422)) {
if ((cropRect->x % 2) != 0) {
avifDiagnosticsPrintf(diag, "[Strict] crop rect X offset must be even due to this image's YUV subsampling");
return AVIF_FALSE;
}
}
if (yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {
if ((cropRect->y % 2) != 0) {
avifDiagnosticsPrintf(diag, "[Strict] crop rect Y offset must be even due to this image's YUV subsampling");
return AVIF_FALSE;
}
}
return AVIF_TRUE;
}

avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect,
const avifCleanApertureBox * clap,
uint32_t imageW,
uint32_t imageH,
avifPixelFormat yuvFormat,
avifDiagnostics * diag)
avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect,
const avifCleanApertureBox * clap,
uint32_t imageW,
uint32_t imageH,
avifDiagnostics * diag)
{
avifDiagnosticsClearError(diag);

Expand All @@ -805,9 +780,6 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect,
}

// ISO/IEC 23000-22:2019/Amd. 2:2021, Section 7.3.6.7:
// The clean aperture property is restricted according to the chroma
// sampling format of the input image (4:4:4, 4:2:2:, 4:2:0, or 4:0:0) as
// follows:
// - cleanApertureWidth and cleanApertureHeight shall be integers;
// - The leftmost pixel and the topmost line of the clean aperture as
// defined in ISO/IEC 14496-12:2020, Section 12.1.4.1 shall be integers;
Expand Down Expand Up @@ -884,7 +856,46 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect,
cropRect->y = (uint32_t)(cropY.n / cropY.d);
cropRect->width = (uint32_t)clapW;
cropRect->height = (uint32_t)clapH;
return avifCropRectIsValid(cropRect, imageW, imageH, yuvFormat, diag);
return avifCropRectIsValid(cropRect, imageW, imageH, diag);
}

avifBool avifCropRectRequiresUpsampling(const avifCropRect * cropRect, avifPixelFormat yuvFormat)
{
// ISO/IEC 23000-22:2024 FDIS, Section 7.3.6.7:
// - If any of the following conditions hold true, the image is first implicitly upsampled to 4:4:4:
// - chroma is subsampled horizontally (i.e., 4:2:2 and 4:2:0) and cleanApertureWidth is odd
// - chroma is subsampled horizontally (i.e., 4:2:2 and 4:2:0) and left-most pixel is on an odd position
// - chroma is subsampled vertically (i.e., 4:2:0) and cleanApertureHeight is odd
// - chroma is subsampled vertically (i.e., 4:2:0) and topmost line is on an odd position

// AV1 supports odd dimensions with chroma subsampling in those directions, so only look for x and y.
return ((yuvFormat == AVIF_PIXEL_FORMAT_YUV420 || yuvFormat == AVIF_PIXEL_FORMAT_YUV422) && (cropRect->x % 2)) ||
(yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && (cropRect->y % 2));
}

avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect,
const avifCleanApertureBox * clap,
uint32_t imageW,
uint32_t imageH,
avifPixelFormat yuvFormat,
avifDiagnostics * diag)
{
if (avifCropRectFromCleanApertureBox(cropRect, clap, imageW, imageH, diag)) {
// Keep the same pre-deprecation behavior.

// ISO/IEC 23000-22:2019/Amd. 2:2021, Section 7.3.6.7:
// - If chroma is subsampled horizontally (i.e., 4:2:2 and 4:2:0),
// the leftmost pixel of the clean aperture shall be even numbers;
// - If chroma is subsampled vertically (i.e., 4:2:0),
// the topmost line of the clean aperture shall be even numbers.

if (avifCropRectRequiresUpsampling(cropRect, yuvFormat)) {
avifDiagnosticsPrintf(diag, "[Strict] crop rect X and Y offsets must be even due to this image's YUV subsampling");
return AVIF_FALSE;
}
return AVIF_TRUE;
}
return AVIF_FALSE;
}

avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap,
Expand All @@ -894,9 +905,10 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap,
avifPixelFormat yuvFormat,
avifDiagnostics * diag)
{
(void)yuvFormat;
y-guyon marked this conversation as resolved.
Show resolved Hide resolved
avifDiagnosticsClearError(diag);

if (!avifCropRectIsValid(cropRect, imageW, imageH, yuvFormat, diag)) {
if (!avifCropRectIsValid(cropRect, imageW, imageH, diag)) {
return AVIF_FALSE;
}

Expand Down
32 changes: 14 additions & 18 deletions src/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -367,19 +367,6 @@ static uint32_t avifCodecConfigurationBoxGetDepth(const avifCodecConfigurationBo
return 8;
}

// This is used as a hint to validating the clap box in avifDecoderItemValidateProperties.
static avifPixelFormat avifCodecConfigurationBoxGetFormat(const avifCodecConfigurationBox * av1C)
{
if (av1C->monochrome) {
return AVIF_PIXEL_FORMAT_YUV400;
} else if (av1C->chromaSubsamplingY == 1) {
return AVIF_PIXEL_FORMAT_YUV420;
} else if (av1C->chromaSubsamplingX == 1) {
return AVIF_PIXEL_FORMAT_YUV422;
}
return AVIF_PIXEL_FORMAT_YUV444;
}

static const avifPropertyArray * avifSampleTableGetProperties(const avifSampleTable * sampleTable, avifCodecType codecType)
{
for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) {
Expand Down Expand Up @@ -1231,9 +1218,19 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item
if (item->miniBoxPixelFormat != AVIF_PIXEL_FORMAT_NONE) {
// This is a MinimizedImageBox ('mini').

if (item->miniBoxPixelFormat != avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)) {
avifPixelFormat av1CPixelFormat;
if (configProp->u.av1C.monochrome) {
av1CPixelFormat = AVIF_PIXEL_FORMAT_YUV400;
} else if (configProp->u.av1C.chromaSubsamplingY == 1) {
av1CPixelFormat = AVIF_PIXEL_FORMAT_YUV420;
} else if (configProp->u.av1C.chromaSubsamplingX == 1) {
av1CPixelFormat = AVIF_PIXEL_FORMAT_YUV422;
} else {
av1CPixelFormat = AVIF_PIXEL_FORMAT_YUV444;
}
if (item->miniBoxPixelFormat != av1CPixelFormat) {
if (!memcmp(configPropName, "av2C", 4) && item->miniBoxPixelFormat == AVIF_PIXEL_FORMAT_YUV400 &&
avifCodecConfigurationBoxGetFormat(&configProp->u.av1C) == AVIF_PIXEL_FORMAT_YUV420) {
av1CPixelFormat == AVIF_PIXEL_FORMAT_YUV420) {
// avm does not handle monochrome as of research-v8.0.0.
// 4:2:0 is used instead.
} else {
Expand All @@ -1242,7 +1239,7 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item
item->id,
avifPixelFormatToString(item->miniBoxPixelFormat),
configPropName,
avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)));
avifPixelFormatToString(av1CPixelFormat));
return AVIF_RESULT_BMFF_PARSE_FAILED;
}
}
Expand Down Expand Up @@ -1279,8 +1276,7 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item
avifCropRect cropRect;
const uint32_t imageW = ispeProp->u.ispe.width;
const uint32_t imageH = ispeProp->u.ispe.height;
const avifPixelFormat configFormat = avifCodecConfigurationBoxGetFormat(&configProp->u.av1C);
avifBool validClap = avifCropRectConvertCleanApertureBox(&cropRect, &clapProp->u.clap, imageW, imageH, configFormat, diag);
const avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &clapProp->u.clap, imageW, imageH, diag);
if (!validClap) {
return AVIF_RESULT_BMFF_PARSE_FAILED;
}
Expand Down
Loading
Loading