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 1 commit 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 @@ -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

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 @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/avifenc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions apps/shared/avifutil.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
23 changes: 16 additions & 7 deletions include/avif/avif.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -516,19 +518,26 @@ 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,
uint32_t imageH,
avifPixelFormat yuvFormat,
avifDiagnostics * diag);

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

// ---------------------------------------------------------------------------
// avifContentLightLevelInformationBox

Expand Down Expand Up @@ -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),

Expand Down
74 changes: 41 additions & 33 deletions src/avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is best to wait until ISO/IEC 23000-22:2024 is formally published. Its status is still Final Draft Internal Standard (FDIS) right now: https://www.iso.org/standard/87576.html

// 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

For AVIF, the first and third conditions are too strict. They are equivalent to disallowing odd widths for 4:2:2 and 4:2:0 and disallowing odd heights for 4:2:0, but AV1 supports those two cases. In libavif we can consider ignoring the first and third conditions.


const int32_t widthN = (int32_t)clap->widthN;
const int32_t widthD = (int32_t)clap->widthD;
const int32_t heightN = (int32_t)clap->heightN;
Expand Down Expand Up @@ -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));
Copy link
Collaborator

Choose a reason for hiding this comment

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

An alternative design is to remove the yuvFormat input parameter and the upsampleBeforeCropping output parameter from this function (avifCropRectFromCleanApertureBox), and then add another function that takes const avifCropRect * cropRect and yuvFormat as input parameters, and returns upsampleBeforeCropping as the return value.

Note that yuvFormat and upsampleBeforeCropping are only used in this statement (lines 804-806), so this statement can be cleanly moved to a separate function.

Another alternative is for libavif to upsample to 4:4:4 and crop automatically when necessary so that libavif users don't need to worry about this. But I am not sure if this is a good idea.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

add another function that takes const avifCropRect * cropRect and yuvFormat as input parameters, and returns upsampleBeforeCropping as the return value

The issue with this new function and with the current avifImage::clap are that users can easily ignore them. The point of the "mandatory" upsampleBeforeCropping argument was to force users to at least consider upsampling (because there is no other function available to get a rect). But they can just ignore clap anyway so it has little effect.

Another alternative is for libavif to upsample to 4:4:4 and crop automatically when necessary so that libavif users don't need to worry about this. But I am not sure if this is a good idea.

I would lean towards that as a default behavior, and why not add an "ignoreClap" setting in avifDecoder to keep the old behavior back.
However existing files with clap will no longer display the same, in places where clap is ignored by users of libavif.

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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This does not keep the old behavior exactly. The difference is the first and third conditions at lines 706-710. So to be precise, the comment should say "most of the pre-deprecation behavior".

If we ignore the first and third conditions as I suggested in my comment at line 710, then we keep the old behavior exactly.

avifBool upsampleBeforeCropping;
return avifCropRectFromCleanApertureBox(cropRect, &upsampleBeforeCropping, clap, imageW, imageH, yuvFormat, diag) &&
!upsampleBeforeCropping;
}

avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap,
Expand All @@ -820,9 +827,10 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap,
avifPixelFormat yuvFormat,
avifDiagnostics * diag)
{
(void)yuvFormat;
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we want to preserve the old behavior, we cannot ignore yuvFormat.

It would be useful to move lines 804-806 to a new function that takes const avifCropRect * cropRect and avifPixelFormat yuvFormat as input and returns the upsampleBeforeCropping boolean. Then we can call that new function here (to preserve the old behavior) and at line 804.

avifDiagnosticsClearError(diag);

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

Expand Down
4 changes: 3 additions & 1 deletion src/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
45 changes: 34 additions & 11 deletions tests/gtest/avifclaptest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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, &param.clap,
param.width, param.height,
param.yuv_format, &diag));
EXPECT_FALSE(avifCropRectFromCleanApertureBox(
&crop_rect, &upsampleBeforeCropping, &param.clap, param.width,
param.height, param.yuv_format, &diag));
}

struct ValidClapPropertyParam {
Expand All @@ -99,6 +100,7 @@ struct ValidClapPropertyParam {
avifCleanApertureBox clap;

avifCropRect expected_crop_rect;
bool expected_upsample_before_cropping;
};

constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = {
Expand All @@ -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
Expand All @@ -120,7 +123,8 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = {
AVIF_PIXEL_FORMAT_YUV420,
{60, 1, 80, 1, static_cast<uint32_t>(-30), 1, static_cast<uint32_t>(-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
Expand All @@ -129,27 +133,46 @@ constexpr ValidClapPropertyParam kValidClapPropertyTestParams[] = {
100,
AVIF_PIXEL_FORMAT_YUV420,
{99, 1, 99, 1, static_cast<uint32_t>(-1), 2, static_cast<uint32_t>(-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<ValidClapPropertyParam>;

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, &param.clap,
param.width, param.height,
param.yuv_format, &diag))
EXPECT_TRUE(avifCropRectFromCleanApertureBox(
&crop_rect, &upsampleBeforeCropping, &param.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, &param.clap,
param.width, param.height,
param.yuv_format, &diag),
!upsampleBeforeCropping);
}

} // namespace
Loading