From e8fc9055815df5177a0ac35a59c43f5fb4f62fb1 Mon Sep 17 00:00:00 2001 From: Yannis Guyon Date: Tue, 10 Sep 2024 17:08:02 +0200 Subject: [PATCH] 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 9101280b4e..297e9f8b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ The changes are relative to the previous release, unless the baseline is specifi * Write an empty HandlerBox name field instead of "libavif" (saves 7 bytes). * Update aom.cmd/LocalAom.cmake: v3.10.0 * Update svt.cmd/svt.sh/LocalSvt.cmake: v2.2.1 +* 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 59b6fd48a9..f2b4e7f7e6 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -84,6 +84,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 da5b659528..84b8939849 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -2290,9 +2290,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 72edbcab88..cc73e9162a 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -104,16 +104,18 @@ static void avifImageDumpInternal(const avifImage * avif, 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 57e2e9bcfe..28c8d0d729 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -460,6 +460,8 @@ typedef struct avifPixelAspectRatioBox typedef struct avifCleanApertureBox { // 'clap' from ISO/IEC 14496-12:2015 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 exact clean aperture width, in counted pixels, of the video image uint32_t widthN; @@ -516,12 +518,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, @@ -529,6 +534,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 @@ -1113,7 +1122,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 7f8d4edf3a..a79b6f66f2 100644 --- a/src/avif.c +++ b/src/avif.c @@ -670,19 +670,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; @@ -692,28 +681,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); @@ -722,6 +699,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; @@ -810,7 +797,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, @@ -820,9 +827,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 6a122cc80b..9ea90198bd 100644 --- a/src/read.c +++ b/src/read.c @@ -1253,10 +1253,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