From 67025a1fd3dd49bf64f5128a983ab4b116504227 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Tue, 10 Sep 2024 17:08:02 +0200 Subject: [PATCH 01/12] Allow odd clap dimensions and offsets The constraint in MIAF was replaced by an upsampling step before cropping. --- CHANGELOG.md | 3 + .../src/main/jni/libavif_jni.cc | 1 + apps/avifenc.c | 3 +- apps/shared/avifutil.c | 8 +- include/avif/avif.h | 23 ++++-- src/avif.c | 74 ++++++++++--------- src/read.c | 4 +- tests/gtest/avifclaptest.cc | 45 ++++++++--- 8 files changed, 105 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e29ba8e2..f08bf3cbb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,9 @@ The changes are relative to the previous release, unless the baseline is specifi * For dependencies, the deprecated way of setting AVIF_LOCAL_* to ON is removed. Dependency options can now only be set to OFF/LOCAL/SYSTEM. * Change the default quality for alpha to be the same as the quality for color. +* Allow decoding subsampled images with odd Clean Aperture dimensions or offsets. +* Deprecate avifCropRectConvertCleanApertureBox(). Replace it with + avifCropRectFromCleanApertureBox(). ## [1.1.1] - 2024-07-30 diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 66f6e902dd..9c75071a62 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -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, diff --git a/apps/avifenc.c b/apps/avifenc.c index 66054c5e2a..b050e89e26 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -2316,9 +2316,10 @@ int main(int argc, char * argv[]) // Validate clap avifCropRect cropRect; + avifBool upsampleBeforeCropping; avifDiagnostics diag; avifDiagnosticsClearError(&diag); - if (!avifCropRectConvertCleanApertureBox(&cropRect, &image->clap, image->width, image->height, image->yuvFormat, &diag)) { + if (!avifCropRectFromCleanApertureBox(&cropRect, &upsampleBeforeCropping, &image->clap, image->width, image->height, image->yuvFormat, &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, diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index 2e6a756b30..51ac20696b 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -99,16 +99,18 @@ static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uin printf("\n"); avifCropRect cropRect; + avifBool upsampleBeforeCropping; avifDiagnostics diag; avifDiagnosticsClearError(&diag); avifBool validClap = - avifCropRectConvertCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, avif->yuvFormat, &diag); + avifCropRectFromCleanApertureBox(&cropRect, &upsampleBeforeCropping, &avif->clap, avif->width, avif->height, avif->yuvFormat, &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, + upsampleBeforeCropping ? " (upsample before cropping)" : ""); } else { printf(" * Invalid: %s\n", diag.error); } diff --git a/include/avif/avif.h b/include/avif/avif.h index db971b13fd..4633f26093 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -484,6 +484,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; @@ -542,12 +544,15 @@ 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); +// If upsampleBeforeCropping is 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 avifCropRectFromCleanApertureBox(avifCropRect * cropRect, + avifBool * upsampleBeforeCropping, + const avifCleanApertureBox * clap, + uint32_t imageW, + uint32_t imageH, + avifPixelFormat yuvFormat, + avifDiagnostics * diag); AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, const avifCropRect * cropRect, uint32_t imageW, @@ -555,6 +560,10 @@ AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxConvertCropRect(avifCleanAp avifPixelFormat yuvFormat, avifDiagnostics * diag); +// Deprecated. Use avifCropRectFromCleanApertureBox() instead. +AVIF_NODISCARD AVIF_API avifBool +avifCropRectConvertCleanApertureBox(avifCropRect *, const avifCleanApertureBox *, uint32_t, uint32_t, avifPixelFormat, avifDiagnostics *); + // --------------------------------------------------------------------------- // avifContentLightLevelInformationBox @@ -1124,7 +1133,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), diff --git a/src/avif.c b/src/avif.c index b3339ae9de..46aabca290 100644 --- a/src/avif.c +++ b/src/avif.c @@ -749,19 +749,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; @@ -771,28 +760,16 @@ 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, + avifBool * upsampleBeforeCropping, + const avifCleanApertureBox * clap, + uint32_t imageW, + uint32_t imageH, + avifPixelFormat yuvFormat, + avifDiagnostics * diag) { avifDiagnosticsClearError(diag); @@ -801,6 +778,16 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect, // positive or negative. For cleanApertureWidth and cleanApertureHeight, // N shall be positive and D shall be strictly positive. + // ISO/IEC 23000-22:2024, 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; + // - 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 + const int32_t widthN = (int32_t)clap->widthN; const int32_t widthD = (int32_t)clap->widthD; const int32_t heightN = (int32_t)clap->heightN; @@ -898,7 +885,27 @@ 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); + if (!avifCropRectIsValid(cropRect, imageW, imageH, diag)) { + return AVIF_FALSE; + } + + *upsampleBeforeCropping = ((yuvFormat == AVIF_PIXEL_FORMAT_YUV420 || yuvFormat == AVIF_PIXEL_FORMAT_YUV422) && + (cropRect->width % 2 || cropRect->x % 2)) || + (yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && (cropRect->height % 2 || cropRect->y % 2)); + return AVIF_TRUE; +} + +avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect, + const avifCleanApertureBox * clap, + uint32_t imageW, + uint32_t imageH, + avifPixelFormat yuvFormat, + avifDiagnostics * diag) +{ + // Keep the same pre-deprecation behavior. + avifBool upsampleBeforeCropping; + return avifCropRectFromCleanApertureBox(cropRect, &upsampleBeforeCropping, clap, imageW, imageH, yuvFormat, diag) && + !upsampleBeforeCropping; } avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, @@ -908,9 +915,10 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, avifPixelFormat yuvFormat, avifDiagnostics * diag) { + (void)yuvFormat; avifDiagnosticsClearError(diag); - if (!avifCropRectIsValid(cropRect, imageW, imageH, yuvFormat, diag)) { + if (!avifCropRectIsValid(cropRect, imageW, imageH, diag)) { return AVIF_FALSE; } diff --git a/src/read.c b/src/read.c index e1d1a2f036..22508d90b9 100644 --- a/src/read.c +++ b/src/read.c @@ -1285,10 +1285,12 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item } avifCropRect cropRect; + avifBool upsampleBeforeCropping; 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, &upsampleBeforeCropping, &clapProp->u.clap, imageW, imageH, configFormat, diag); if (!validClap) { return AVIF_RESULT_BMFF_PARSE_FAILED; } diff --git a/tests/gtest/avifclaptest.cc b/tests/gtest/avifclaptest.cc index 77dfaf26e2..130134b7c2 100644 --- a/tests/gtest/avifclaptest.cc +++ b/tests/gtest/avifclaptest.cc @@ -82,14 +82,15 @@ using InvalidClapPropertyTest = INSTANTIATE_TEST_SUITE_P(Parameterized, InvalidClapPropertyTest, ::testing::ValuesIn(kInvalidClapPropertyTestParams)); -// Negative tests for the avifCropRectConvertCleanApertureBox() function. +// Negative tests for the avifCropRectFromCleanApertureBox() function. TEST_P(InvalidClapPropertyTest, ValidateClapProperty) { const InvalidClapPropertyParam& param = GetParam(); avifCropRect crop_rect; + avifBool upsampleBeforeCropping; avifDiagnostics diag; - EXPECT_FALSE(avifCropRectConvertCleanApertureBox(&crop_rect, ¶m.clap, - param.width, param.height, - param.yuv_format, &diag)); + EXPECT_FALSE(avifCropRectFromCleanApertureBox( + &crop_rect, &upsampleBeforeCropping, ¶m.clap, param.width, + param.height, param.yuv_format, &diag)); } struct ValidClapPropertyParam { @@ -99,6 +100,7 @@ struct ValidClapPropertyParam { avifCleanApertureBox clap; avifCropRect expected_crop_rect; + bool expected_upsample_before_cropping; }; constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { @@ -110,7 +112,8 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { 160, AVIF_PIXEL_FORMAT_YUV420, {96, 1, 132, 1, 0, 1, 0, 1}, - {12, 14, 96, 132}}, + {12, 14, 96, 132}, + false}, // pcX = -30 + (120 - 1)/2 = 29.5 // pcY = -40 + (160 - 1)/2 = 39.5 // leftmost = 29.5 - (60 - 1)/2 = 0 @@ -120,7 +123,8 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { AVIF_PIXEL_FORMAT_YUV420, {60, 1, 80, 1, static_cast(-30), 1, static_cast(-40), 1}, - {0, 0, 60, 80}}, + {0, 0, 60, 80}, + false}, // pcX = -1/2 + (100 - 1)/2 = 49 // pcY = -1/2 + (100 - 1)/2 = 49 // leftmost = 49 - (99 - 1)/2 = 0 @@ -129,7 +133,18 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { 100, AVIF_PIXEL_FORMAT_YUV420, {99, 1, 99, 1, static_cast(-1), 2, static_cast(-1), 2}, - {0, 0, 99, 99}}, + {0, 0, 99, 99}, + true}, + // pcX = -1/2 + (100 - 1)/2 = 49 + // pcY = -1/2 + (100 - 1)/2 = 49 + // leftmost = 49 - (99 - 1)/2 = 0 + // topmost = 49 - (99 - 1)/2 = 0 + {100, + 100, + AVIF_PIXEL_FORMAT_YUV420, + {99, 1, 99, 1, 1, 2, 1, 2}, + {1, 1, 99, 99}, + true}, }; using ValidClapPropertyTest = ::testing::TestWithParam; @@ -137,19 +152,27 @@ using ValidClapPropertyTest = ::testing::TestWithParam; INSTANTIATE_TEST_SUITE_P(Parameterized, ValidClapPropertyTest, ::testing::ValuesIn(kValidClapPropertyTestParams)); -// Positive tests for the avifCropRectConvertCleanApertureBox() function. +// Positive tests for the avifCropRectFromCleanApertureBox() function. TEST_P(ValidClapPropertyTest, ValidateClapProperty) { const ValidClapPropertyParam& param = GetParam(); avifCropRect crop_rect; + avifBool upsampleBeforeCropping; avifDiagnostics diag; - EXPECT_TRUE(avifCropRectConvertCleanApertureBox(&crop_rect, ¶m.clap, - param.width, param.height, - param.yuv_format, &diag)) + EXPECT_TRUE(avifCropRectFromCleanApertureBox( + &crop_rect, &upsampleBeforeCropping, ¶m.clap, param.width, + param.height, param.yuv_format, &diag)) << diag.error; EXPECT_EQ(crop_rect.x, param.expected_crop_rect.x); EXPECT_EQ(crop_rect.y, param.expected_crop_rect.y); EXPECT_EQ(crop_rect.width, param.expected_crop_rect.width); EXPECT_EQ(crop_rect.height, param.expected_crop_rect.height); + EXPECT_EQ(upsampleBeforeCropping, param.expected_upsample_before_cropping); + + // Deprecated function coverage. + EXPECT_EQ(avifCropRectConvertCleanApertureBox(&crop_rect, ¶m.clap, + param.width, param.height, + param.yuv_format, &diag), + !upsampleBeforeCropping); } } // namespace From 62b1d99dae476e26baa3b6960fbd367c966bc5c0 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Fri, 22 Nov 2024 16:46:33 +0100 Subject: [PATCH 02/12] Move oddness check to CropRectRequiresUpsampling() Remove yuvFormat arg from avifCropRectFromCleanApertureBox(). Make avifCropRectConvertCleanApertureBox() behave exactly as before. Fix comments. Update avifclaptest. Print warnings in avifdec. --- apps/avifdec.c | 18 ++++++++++++ apps/avifenc.c | 4 +-- apps/shared/avifutil.c | 5 ++-- include/avif/avif.h | 7 ++--- src/avif.c | 56 ++++++++++++++++++++----------------- src/read.c | 5 +--- tests/gtest/avifclaptest.cc | 18 ++++++------ 7 files changed, 64 insertions(+), 49 deletions(-) diff --git a/apps/avifdec.c b/apps/avifdec.c index f79c31f712..f4b0aed69c 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c @@ -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. diff --git a/apps/avifenc.c b/apps/avifenc.c index b050e89e26..699cc84612 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -2316,10 +2316,9 @@ int main(int argc, char * argv[]) // Validate clap avifCropRect cropRect; - avifBool upsampleBeforeCropping; avifDiagnostics diag; avifDiagnosticsClearError(&diag); - if (!avifCropRectFromCleanApertureBox(&cropRect, &upsampleBeforeCropping, &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, @@ -2333,6 +2332,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; diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index 51ac20696b..a80c5849f7 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -102,15 +102,14 @@ static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uin avifBool upsampleBeforeCropping; avifDiagnostics diag; avifDiagnosticsClearError(&diag); - avifBool validClap = - avifCropRectFromCleanApertureBox(&cropRect, &upsampleBeforeCropping, &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%s\n", cropRect.x, cropRect.y, cropRect.width, cropRect.height, - upsampleBeforeCropping ? " (upsample before cropping)" : ""); + avifCropRectRequiresUpsampling(&cropRect, avif->yuvFormat) ? " (upsample before cropping)" : ""); } else { printf(" * Invalid: %s\n", diag.error); } diff --git a/include/avif/avif.h b/include/avif/avif.h index 4633f26093..b319917ec7 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -544,14 +544,10 @@ 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. -// If upsampleBeforeCropping is 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 avifCropRectFromCleanApertureBox(avifCropRect * cropRect, - avifBool * upsampleBeforeCropping, const avifCleanApertureBox * clap, uint32_t imageW, uint32_t imageH, - avifPixelFormat yuvFormat, avifDiagnostics * diag); AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, const avifCropRect * cropRect, @@ -559,6 +555,9 @@ AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxConvertCropRect(avifCleanAp 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 diff --git a/src/avif.c b/src/avif.c index 46aabca290..f543abb01d 100644 --- a/src/avif.c +++ b/src/avif.c @@ -764,11 +764,9 @@ static avifBool avifCropRectIsValid(const avifCropRect * cropRect, uint32_t imag } avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect, - avifBool * upsampleBeforeCropping, const avifCleanApertureBox * clap, uint32_t imageW, uint32_t imageH, - avifPixelFormat yuvFormat, avifDiagnostics * diag) { avifDiagnosticsClearError(diag); @@ -778,16 +776,6 @@ avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect, // positive or negative. For cleanApertureWidth and cleanApertureHeight, // N shall be positive and D shall be strictly positive. - // ISO/IEC 23000-22:2024, 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; - // - 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 - const int32_t widthN = (int32_t)clap->widthN; const int32_t widthD = (int32_t)clap->widthD; const int32_t heightN = (int32_t)clap->heightN; @@ -806,9 +794,6 @@ avifBool avifCropRectFromCleanApertureBox(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; @@ -885,14 +870,21 @@ avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect, cropRect->y = (uint32_t)(cropY.n / cropY.d); cropRect->width = (uint32_t)clapW; cropRect->height = (uint32_t)clapH; - if (!avifCropRectIsValid(cropRect, imageW, imageH, diag)) { - return AVIF_FALSE; - } + return avifCropRectIsValid(cropRect, imageW, imageH, diag); +} - *upsampleBeforeCropping = ((yuvFormat == AVIF_PIXEL_FORMAT_YUV420 || yuvFormat == AVIF_PIXEL_FORMAT_YUV422) && - (cropRect->width % 2 || cropRect->x % 2)) || - (yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && (cropRect->height % 2 || cropRect->y % 2)); - return AVIF_TRUE; +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, @@ -902,10 +894,22 @@ avifBool avifCropRectConvertCleanApertureBox(avifCropRect * cropRect, avifPixelFormat yuvFormat, avifDiagnostics * diag) { - // Keep the same pre-deprecation behavior. - avifBool upsampleBeforeCropping; - return avifCropRectFromCleanApertureBox(cropRect, &upsampleBeforeCropping, clap, imageW, imageH, yuvFormat, diag) && - !upsampleBeforeCropping; + 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, diff --git a/src/read.c b/src/read.c index 22508d90b9..23daa8469e 100644 --- a/src/read.c +++ b/src/read.c @@ -1285,12 +1285,9 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item } avifCropRect cropRect; - avifBool upsampleBeforeCropping; const uint32_t imageW = ispeProp->u.ispe.width; const uint32_t imageH = ispeProp->u.ispe.height; - const avifPixelFormat configFormat = avifCodecConfigurationBoxGetFormat(&configProp->u.av1C); - const avifBool validClap = - avifCropRectFromCleanApertureBox(&cropRect, &upsampleBeforeCropping, &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; } diff --git a/tests/gtest/avifclaptest.cc b/tests/gtest/avifclaptest.cc index 130134b7c2..a1d01599b1 100644 --- a/tests/gtest/avifclaptest.cc +++ b/tests/gtest/avifclaptest.cc @@ -86,11 +86,9 @@ INSTANTIATE_TEST_SUITE_P(Parameterized, InvalidClapPropertyTest, TEST_P(InvalidClapPropertyTest, ValidateClapProperty) { const InvalidClapPropertyParam& param = GetParam(); avifCropRect crop_rect; - avifBool upsampleBeforeCropping; avifDiagnostics diag; EXPECT_FALSE(avifCropRectFromCleanApertureBox( - &crop_rect, &upsampleBeforeCropping, ¶m.clap, param.width, - param.height, param.yuv_format, &diag)); + &crop_rect, ¶m.clap, param.width, param.height, &diag)); } struct ValidClapPropertyParam { @@ -134,7 +132,7 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { AVIF_PIXEL_FORMAT_YUV420, {99, 1, 99, 1, static_cast(-1), 2, static_cast(-1), 2}, {0, 0, 99, 99}, - true}, + false}, // pcX = -1/2 + (100 - 1)/2 = 49 // pcY = -1/2 + (100 - 1)/2 = 49 // leftmost = 49 - (99 - 1)/2 = 0 @@ -156,23 +154,23 @@ INSTANTIATE_TEST_SUITE_P(Parameterized, ValidClapPropertyTest, TEST_P(ValidClapPropertyTest, ValidateClapProperty) { const ValidClapPropertyParam& param = GetParam(); avifCropRect crop_rect; - avifBool upsampleBeforeCropping; avifDiagnostics diag; - EXPECT_TRUE(avifCropRectFromCleanApertureBox( - &crop_rect, &upsampleBeforeCropping, ¶m.clap, param.width, - param.height, param.yuv_format, &diag)) + ASSERT_TRUE(avifCropRectFromCleanApertureBox( + &crop_rect, ¶m.clap, param.width, param.height, &diag)) << diag.error; + const avifBool upsample_before_cropping = + avifCropRectRequiresUpsampling(&crop_rect, param.yuv_format); EXPECT_EQ(crop_rect.x, param.expected_crop_rect.x); EXPECT_EQ(crop_rect.y, param.expected_crop_rect.y); EXPECT_EQ(crop_rect.width, param.expected_crop_rect.width); EXPECT_EQ(crop_rect.height, param.expected_crop_rect.height); - EXPECT_EQ(upsampleBeforeCropping, param.expected_upsample_before_cropping); + EXPECT_EQ(upsample_before_cropping, param.expected_upsample_before_cropping); // Deprecated function coverage. EXPECT_EQ(avifCropRectConvertCleanApertureBox(&crop_rect, ¶m.clap, param.width, param.height, param.yuv_format, &diag), - !upsampleBeforeCropping); + !upsample_before_cropping); } } // namespace From 607096e9adaa322dfb6268039b89dc3f16712025 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Fri, 22 Nov 2024 16:49:49 +0100 Subject: [PATCH 03/12] Remove unused variable --- apps/shared/avifutil.c | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index a80c5849f7..d600ee8b9d 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -99,7 +99,6 @@ static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uin printf("\n"); avifCropRect cropRect; - avifBool upsampleBeforeCropping; avifDiagnostics diag; avifDiagnosticsClearError(&diag); avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag); From 0191fd5e922c157ca7393a5b5cbfa69cf20af28c Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Fri, 22 Nov 2024 16:53:14 +0100 Subject: [PATCH 04/12] Inline avifCodecConfigurationBoxGetFormat() Unused unless AVIF_ENABLE_EXPERIMENTAL_MINI is defined. --- src/read.c | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/read.c b/src/read.c index 23daa8469e..a0455ca0e4 100644 --- a/src/read.c +++ b/src/read.c @@ -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) { @@ -1239,9 +1226,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 { @@ -1250,7 +1247,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; } } From ce97e0898105a4e1031d0e3c6a3c5f20c11119dc Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Wed, 8 Jan 2025 14:00:39 +0100 Subject: [PATCH 05/12] Apply comments --- apps/avifdec.c | 16 ++--- apps/avifenc.c | 10 ++-- include/avif/avif.h | 16 +++-- src/avif.c | 113 +++++++++++++++++++++--------------- tests/gtest/avifclaptest.cc | 54 ++++++++++++++--- 5 files changed, 131 insertions(+), 78 deletions(-) diff --git a/apps/avifdec.c b/apps/avifdec.c index f4b0aed69c..9c0e33cf68 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c @@ -349,17 +349,11 @@ int main(int argc, char * argv[]) 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 { + if (!avifCropRectFromCleanApertureBox(&cropRect, + &decoder->image->clap, + decoder->image->width, + decoder->image->height, + &decoder->diag)) { // Should happen only if AVIF_STRICT_CLAP_VALID is disabled. fprintf(stderr, "Warning: Invalid Clean Aperture values\n"); } diff --git a/apps/avifenc.c b/apps/avifenc.c index 699cc84612..f52ee00ade 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -388,7 +388,7 @@ static avifBool strpre(const char * str, const char * prefix) return strncmp(str, prefix, strlen(prefix)) == 0; } -static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, avifPixelFormat yuvFormat, uint32_t clapValues[8]) +static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, uint32_t clapValues[8]) { avifCleanApertureBox clap; avifCropRect cropRect; @@ -399,13 +399,12 @@ static avifBool convertCropToClap(uint32_t srcW, uint32_t srcH, avifPixelFormat avifDiagnostics diag; avifDiagnosticsClearError(&diag); - avifBool convertResult = avifCleanApertureBoxConvertCropRect(&clap, &cropRect, srcW, srcH, yuvFormat, &diag); + avifBool convertResult = avifCleanApertureBoxFromCropRect(&clap, &cropRect, srcW, srcH, &diag); if (!convertResult) { fprintf(stderr, - "ERROR: Impossible crop rect: imageSize:[%ux%u], pixelFormat:%s, cropRect:[%u,%u, %ux%u] - %s\n", + "ERROR: Impossible crop rect: imageSize:[%ux%u], cropRect:[%u,%u, %ux%u] - %s\n", srcW, srcH, - avifPixelFormatToString(yuvFormat), cropRect.x, cropRect.y, cropRect.width, @@ -2298,7 +2297,7 @@ int main(int argc, char * argv[]) image->pasp.vSpacing = settings.paspValues[1]; } if (cropConversionRequired) { - if (!convertCropToClap(image->width, image->height, image->yuvFormat, settings.clapValues)) { + if (!convertCropToClap(image->width, image->height, settings.clapValues)) { goto cleanup; } settings.clapValid = AVIF_TRUE; @@ -2332,7 +2331,6 @@ 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; diff --git a/include/avif/avif.h b/include/avif/avif.h index b319917ec7..b25fc57341 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -486,6 +486,8 @@ 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. + // However, AV1 supports odd dimensions with chroma subsampling in those directions, so mostly watch + // for offsets. // a fractional number which defines the width of the clean aperture image uint32_t widthN; @@ -549,12 +551,11 @@ AVIF_NODISCARD AVIF_API avifBool avifCropRectFromCleanApertureBox(avifCropRect * 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); +AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxFromCropRect(avifCleanApertureBox * clap, + const avifCropRect * cropRect, + uint32_t imageW, + uint32_t imageH, + 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); @@ -562,6 +563,9 @@ AVIF_NODISCARD AVIF_API avifBool avifCropRectRequiresUpsampling(const avifCropRe // Deprecated. Use avifCropRectFromCleanApertureBox() instead. AVIF_NODISCARD AVIF_API avifBool avifCropRectConvertCleanApertureBox(avifCropRect *, const avifCleanApertureBox *, uint32_t, uint32_t, avifPixelFormat, avifDiagnostics *); +// Deprecated. Use avifCleanApertureBoxFromCropRect() instead. +AVIF_NODISCARD AVIF_API avifBool +avifCleanApertureBoxConvertCropRect(avifCleanApertureBox *, const avifCropRect *, uint32_t, uint32_t, avifPixelFormat, avifDiagnostics *); // --------------------------------------------------------------------------- // avifContentLightLevelInformationBox diff --git a/src/avif.c b/src/avif.c index f543abb01d..c53b363bfd 100644 --- a/src/avif.c +++ b/src/avif.c @@ -873,53 +873,12 @@ avifBool avifCropRectFromCleanApertureBox(avifCropRect * cropRect, 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, - const avifCropRect * cropRect, - uint32_t imageW, - uint32_t imageH, - avifPixelFormat yuvFormat, - avifDiagnostics * diag) +avifBool avifCleanApertureBoxFromCropRect(avifCleanApertureBox * clap, + const avifCropRect * cropRect, + uint32_t imageW, + uint32_t imageH, + avifDiagnostics * diag) { - (void)yuvFormat; avifDiagnosticsClearError(diag); if (!avifCropRectIsValid(cropRect, imageW, imageH, diag)) { @@ -977,6 +936,68 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, return AVIF_TRUE; } +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)) { + return AVIF_FALSE; + } + // 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; +} + +avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, + const avifCropRect * cropRect, + uint32_t imageW, + uint32_t imageH, + avifPixelFormat yuvFormat, + avifDiagnostics * 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 avifCleanApertureBoxFromCropRect(clap, cropRect, imageW, imageH, diag); +} + // --------------------------------------------------------------------------- avifBool avifIsAlpha(avifItemCategory itemCategory) diff --git a/tests/gtest/avifclaptest.cc b/tests/gtest/avifclaptest.cc index a1d01599b1..d1fc636f52 100644 --- a/tests/gtest/avifclaptest.cc +++ b/tests/gtest/avifclaptest.cc @@ -133,10 +133,10 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = { {99, 1, 99, 1, static_cast(-1), 2, static_cast(-1), 2}, {0, 0, 99, 99}, false}, - // pcX = -1/2 + (100 - 1)/2 = 49 - // pcY = -1/2 + (100 - 1)/2 = 49 - // leftmost = 49 - (99 - 1)/2 = 0 - // topmost = 49 - (99 - 1)/2 = 0 + // pcX = 1/2 + (100 - 1)/2 = 50 + // pcY = 1/2 + (100 - 1)/2 = 50 + // leftmost = 50 - (99 - 1)/2 = 1 + // topmost = 50 - (99 - 1)/2 = 1 {100, 100, AVIF_PIXEL_FORMAT_YUV420, @@ -150,7 +150,8 @@ using ValidClapPropertyTest = ::testing::TestWithParam; INSTANTIATE_TEST_SUITE_P(Parameterized, ValidClapPropertyTest, ::testing::ValuesIn(kValidClapPropertyTestParams)); -// Positive tests for the avifCropRectFromCleanApertureBox() function. +// Positive tests for the avifCropRectFromCleanApertureBox() and +// avifCleanApertureBoxFromCropRect() functions. TEST_P(ValidClapPropertyTest, ValidateClapProperty) { const ValidClapPropertyParam& param = GetParam(); avifCropRect crop_rect; @@ -167,10 +168,45 @@ TEST_P(ValidClapPropertyTest, ValidateClapProperty) { EXPECT_EQ(upsample_before_cropping, param.expected_upsample_before_cropping); // Deprecated function coverage. - EXPECT_EQ(avifCropRectConvertCleanApertureBox(&crop_rect, ¶m.clap, - param.width, param.height, - param.yuv_format, &diag), - !upsample_before_cropping); + avifBool success = avifCropRectConvertCleanApertureBox( + &crop_rect, ¶m.clap, param.width, param.height, param.yuv_format, + &diag); + EXPECT_EQ(success, !upsample_before_cropping); + if (success) { + EXPECT_EQ(crop_rect.x, param.expected_crop_rect.x); + EXPECT_EQ(crop_rect.y, param.expected_crop_rect.y); + EXPECT_EQ(crop_rect.width, param.expected_crop_rect.width); + EXPECT_EQ(crop_rect.height, param.expected_crop_rect.height); + } + + avifCleanApertureBox clap; + ASSERT_TRUE(avifCleanApertureBoxFromCropRect( + &clap, ¶m.expected_crop_rect, param.width, param.height, &diag)) + << diag.error; + EXPECT_EQ(clap.widthN, param.clap.widthN); + EXPECT_EQ(clap.widthD, param.clap.widthD); + EXPECT_EQ(clap.heightN, param.clap.heightN); + EXPECT_EQ(clap.heightD, param.clap.heightD); + EXPECT_EQ(clap.horizOffN, param.clap.horizOffN); + EXPECT_EQ(clap.horizOffD, param.clap.horizOffD); + EXPECT_EQ(clap.vertOffN, param.clap.vertOffN); + EXPECT_EQ(clap.vertOffD, param.clap.vertOffD); + + // Deprecated function coverage. + success = avifCleanApertureBoxConvertCropRect( + &clap, ¶m.expected_crop_rect, param.width, param.height, + param.yuv_format, &diag); + EXPECT_EQ(success, !upsample_before_cropping); + if (success) { + EXPECT_EQ(clap.widthN, param.clap.widthN); + EXPECT_EQ(clap.widthD, param.clap.widthD); + EXPECT_EQ(clap.heightN, param.clap.heightN); + EXPECT_EQ(clap.heightD, param.clap.heightD); + EXPECT_EQ(clap.horizOffN, param.clap.horizOffN); + EXPECT_EQ(clap.horizOffD, param.clap.horizOffD); + EXPECT_EQ(clap.vertOffN, param.clap.vertOffN); + EXPECT_EQ(clap.vertOffD, param.clap.vertOffD); + } } } // namespace From 8010388c7ce4b412e85298c9e4c477b7baff124d Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Wed, 8 Jan 2025 14:20:51 +0100 Subject: [PATCH 06/12] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f08bf3cbb0..b60dfedc0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,8 +51,9 @@ The changes are relative to the previous release, unless the baseline is specifi removed. Dependency options can now only be set to OFF/LOCAL/SYSTEM. * Change the default quality for alpha to be the same as the quality for color. * Allow decoding subsampled images with odd Clean Aperture dimensions or offsets. -* Deprecate avifCropRectConvertCleanApertureBox(). Replace it with - avifCropRectFromCleanApertureBox(). +* Deprecate avifCropRectConvertCleanApertureBox() and + avifCleanApertureBoxConvertCropRect(). Replace them with + avifCropRectFromCleanApertureBox() and avifCleanApertureBoxFromCropRect(). ## [1.1.1] - 2024-07-30 From dfecdc4702536bde2b8ba225d7dcb87930824e62 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Wed, 8 Jan 2025 14:22:38 +0100 Subject: [PATCH 07/12] Remove trailing space --- tests/gtest/avifclaptest.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/gtest/avifclaptest.cc b/tests/gtest/avifclaptest.cc index d1fc636f52..e052290088 100644 --- a/tests/gtest/avifclaptest.cc +++ b/tests/gtest/avifclaptest.cc @@ -150,7 +150,7 @@ using ValidClapPropertyTest = ::testing::TestWithParam; INSTANTIATE_TEST_SUITE_P(Parameterized, ValidClapPropertyTest, ::testing::ValuesIn(kValidClapPropertyTestParams)); -// Positive tests for the avifCropRectFromCleanApertureBox() and +// Positive tests for the avifCropRectFromCleanApertureBox() and // avifCleanApertureBoxFromCropRect() functions. TEST_P(ValidClapPropertyTest, ValidateClapProperty) { const ValidClapPropertyParam& param = GetParam(); From baf25254509f32ea01b23041b745b31f8c2ac1c1 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Thu, 9 Jan 2025 11:54:47 +0100 Subject: [PATCH 08/12] Reword comment [skip ci] --- include/avif/avif.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/avif/avif.h b/include/avif/avif.h index b25fc57341..6f72426d0b 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -486,8 +486,8 @@ 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. - // However, AV1 supports odd dimensions with chroma subsampling in those directions, so mostly watch - // for offsets. + // However, AV1 supports odd dimensions with chroma subsampling in those directions, so only apply the + // requirements to offsets. // a fractional number which defines the width of the clean aperture image uint32_t widthN; From dead45b26a5c1865bc595ae7a01bb9172fda4e08 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Fri, 10 Jan 2025 13:36:24 +0100 Subject: [PATCH 09/12] Add comments about applying transforms --- include/avif/avif.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/avif/avif.h b/include/avif/avif.h index 6f72426d0b..3d2ad8aafd 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -557,7 +557,8 @@ AVIF_NODISCARD AVIF_API avifBool avifCleanApertureBoxFromCropRect(avifCleanApert uint32_t imageH, 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. +// Clean Aperture values are applied. This can be done by converting the avifImage to RGB using +// avifImageYUVToRGB() and only using the cropRect region of the avifRGBImage. AVIF_NODISCARD AVIF_API avifBool avifCropRectRequiresUpsampling(const avifCropRect * cropRect, avifPixelFormat yuvFormat); // Deprecated. Use avifCropRectFromCleanApertureBox() instead. @@ -1309,6 +1310,10 @@ typedef struct avifDecoder // legal to call avifImageYUVToRGB() on this in between calls to avifDecoderNextImage(), but use // avifImageCopy() if you want to make a complete, permanent copy of this image's YUV content or // metadata. + // + // For each field among clap, irot and imir, if the corresponding avifTransformFlag is set, the + // transform must be applied before rendering or converting the image, or forwarded along as + // attached metadata. avifImage * image; // Counts and timing for the current image in an image sequence. Uninteresting for single image files. From 2be929f1b483ab716c3894fd787414c8166ac626 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Fri, 10 Jan 2025 13:39:44 +0100 Subject: [PATCH 10/12] Replace avifCropRectConvertCleanApertureBox() by avifCropRectFromCleanApertureBox() in y4m.c. --- apps/shared/y4m.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/shared/y4m.c b/apps/shared/y4m.c index f569122f9b..86094252af 100644 --- a/apps/shared/y4m.c +++ b/apps/shared/y4m.c @@ -496,7 +496,7 @@ avifBool y4mWrite(const char * outputFilename, const avifImage * avif) if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { avifCropRect cropRect; avifDiagnostics diag; - if (avifCropRectConvertCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, avif->yuvFormat, &diag) && + if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) && (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) { // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement. fprintf(stderr, From 0323aba612b06b3f0b18e3ba90c18ccde86ecc44 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Mon, 13 Jan 2025 15:41:39 +0100 Subject: [PATCH 11/12] Apply to avifpng, avifjpeg and libavif_jni --- .../avifandroidjni/src/main/jni/libavif_jni.cc | 14 ++++++++------ apps/shared/avifjpeg.c | 2 +- apps/shared/avifpng.c | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 9c75071a62..83eb3e3d93 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -85,12 +85,15 @@ 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. + // TODO(vigneshv): Handle the case of avifCropRectRequiresUpsampling() + // returning true. if (!(decoder->decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) || - !avifCropRectConvertCleanApertureBox( + !avifCropRectFromCleanApertureBox( &decoder->crop, &decoder->decoder->image->clap, decoder->decoder->image->width, decoder->decoder->image->height, - decoder->decoder->image->yuvFormat, &diag)) { + &diag) || + avifCropRectRequiresUpsampling(&decoder->crop, + decoder->decoder->image->yuvFormat)) { decoder->crop.width = decoder->decoder->image->width; decoder->crop.height = decoder->decoder->image->height; decoder->crop.x = 0; @@ -415,9 +418,8 @@ FUNC(jstring, versionString) { libyuv_version[0] = '\0'; } char version_string[512]; - snprintf(version_string, sizeof(version_string), - "libavif: %s. Codecs: %s.%s", avifVersion(), - codec_versions, libyuv_version); + snprintf(version_string, sizeof(version_string), "libavif: %s. Codecs: %s.%s", + avifVersion(), codec_versions, libyuv_version); return env->NewStringUTF(version_string); } diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c index 5e10d11e88..98cfbc3b6e 100644 --- a/apps/shared/avifjpeg.c +++ b/apps/shared/avifjpeg.c @@ -1300,7 +1300,7 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { avifCropRect cropRect; avifDiagnostics diag; - if (avifCropRectConvertCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, avif->yuvFormat, &diag) && + if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) && (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) { // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement. fprintf(stderr, diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c index cb630bf176..08b33efec5 100644 --- a/apps/shared/avifpng.c +++ b/apps/shared/avifpng.c @@ -731,7 +731,7 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3 if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { avifCropRect cropRect; avifDiagnostics diag; - if (avifCropRectConvertCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, avif->yuvFormat, &diag) && + if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) && (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) { // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement. fprintf(stderr, From ee24cbbe5d7474e9ee93509bbc6405eb33ddbcea Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Mon, 13 Jan 2025 16:18:52 +0100 Subject: [PATCH 12/12] Add avifCropRectRequiresUpsampling to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b60dfedc0e..f5eb5567d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ The changes are relative to the previous release, unless the baseline is specifi * Deprecate avifCropRectConvertCleanApertureBox() and avifCleanApertureBoxConvertCropRect(). Replace them with avifCropRectFromCleanApertureBox() and avifCleanApertureBoxFromCropRect(). +* Add avifCropRectRequiresUpsampling(). ## [1.1.1] - 2024-07-30