Add AVIF_STRICT_CLAP_VALID strict flag, which ensures the clap box does not violate any standards
diff --git a/src/avif.c b/src/avif.c
index 9c54e1e..d8d0d09 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -391,11 +391,11 @@
typedef struct clapFraction
{
- uint32_t n;
- uint32_t d;
+ int32_t n;
+ int32_t d;
} clapFraction;
-static clapFraction calcCenter(uint32_t dim)
+static clapFraction calcCenter(int32_t dim)
{
clapFraction f;
f.n = dim >> 1;
@@ -428,7 +428,7 @@
static void clapFractionSimplify(clapFraction * f)
{
- int32_t gcd = calcGCD((int32_t)f->n, (int32_t)f->d);
+ int32_t gcd = calcGCD(f->n, f->d);
if (gcd > 1) {
f->n /= gcd;
f->d /= gcd;
@@ -439,11 +439,11 @@
static void clapFractionCD(clapFraction * a, clapFraction * b)
{
if ((a->d != b->d)) {
- const uint32_t ad = a->d;
- const uint32_t bd = b->d;
- a->n = (uint32_t)((int32_t)a->n * (int32_t)bd);
+ const int32_t ad = a->d;
+ const int32_t bd = b->d;
+ a->n = a->n * bd;
a->d *= bd;
- b->n = (uint32_t)((int32_t)b->n * (int32_t)ad);
+ b->n = b->n * ad;
b->d *= ad;
}
}
@@ -453,7 +453,7 @@
clapFractionCD(&a, &b);
clapFraction result;
- result.n = (uint32_t)((int32_t)a.n + (int32_t)b.n);
+ result.n = a.n + b.n;
result.d = a.d;
clapFractionSimplify(&result);
@@ -465,7 +465,7 @@
clapFractionCD(&a, &b);
clapFraction result;
- result.n = (uint32_t)((int32_t)a.n - (int32_t)b.n);
+ result.n = a.n - b.n;
result.d = a.d;
clapFractionSimplify(&result);
@@ -475,8 +475,8 @@
static clapFraction clapFractionMul(clapFraction a, clapFraction b)
{
clapFraction result;
- result.n = (uint32_t)((int32_t)a.n * (int32_t)b.n);
- result.d = (uint32_t)((int32_t)a.d * (int32_t)b.d);
+ result.n = a.n * b.n;
+ result.d = a.d * b.d;
clapFractionSimplify(&result);
return result;
}
@@ -488,7 +488,16 @@
avifDiagnostics * diag)
{
- (void)yuvFormat;
+ // ISO/IEC 23000-22:2019/DAM 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:
+ // - when the image is 4:0:0 (monochrome) or 4:4:4, the horizontal and
+ // vertical cropped offsets and widths shall be integers;
+ // - when the image is 4:2:2 the horizontal cropped offset and width
+ // shall be even numbers and the vertical values shall be integers;
+ // - when the image is 4:2:0 both the horizontal and vertical cropped
+ // offsets and widths shall be even numbers.
if ((cropRect->width == 0) || (cropRect->height == 0)) {
avifDiagnosticsPrintf(diag, "crop rect width and height must be nonzero");
@@ -498,6 +507,19 @@
avifDiagnosticsPrintf(diag, "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) || ((cropRect->width % 2) != 0)) {
+ avifDiagnosticsPrintf(diag, "crop rect X offset and width must both be even due to this image's YUV subsampling");
+ return AVIF_FALSE;
+ }
+ }
+ if (yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {
+ if (((cropRect->y % 2) != 0) || ((cropRect->height % 2) != 0)) {
+ avifDiagnosticsPrintf(diag, "crop rect Y offset and height must both be even due to this image's YUV subsampling");
+ return AVIF_FALSE;
+ }
+ }
return AVIF_TRUE;
}
@@ -508,49 +530,73 @@
const avifPixelFormat yuvFormat,
avifDiagnostics * diag)
{
- if (((int32_t)clap->widthN % (int32_t)clap->widthD) != 0) {
+ // ISO/IEC 14496-12:2020, Section 12.1.4.1:
+ // For horizOff and vertOff, D shall be strictly positive and N may be
+ // positive or negative. For cleanApertureWidth and cleanApertureHeight,
+ // N shall be positive and D shall be strictly positive.
+
+ const int32_t widthN = (int32_t)clap->widthN;
+ const int32_t widthD = (int32_t)clap->widthD;
+ const int32_t heightN = (int32_t)clap->heightN;
+ const int32_t heightD = (int32_t)clap->heightD;
+ const int32_t horizOffN = (int32_t)clap->horizOffN;
+ const int32_t horizOffD = (int32_t)clap->horizOffD;
+ const int32_t vertOffN = (int32_t)clap->vertOffN;
+ const int32_t vertOffD = (int32_t)clap->vertOffD;
+ if ((widthD <= 0) || (heightD <= 0) || (horizOffD <= 0) || (vertOffD <= 0)) {
+ avifDiagnosticsPrintf(diag, "clap contains a denominator that is not strictly positive");
+ return AVIF_FALSE;
+ }
+
+ if ((widthN % widthD) != 0) {
avifDiagnosticsPrintf(diag, "clap width is not an integer");
return AVIF_FALSE;
}
- if ((clap->heightN % clap->heightD) != 0) {
+ if ((heightN % heightD) != 0) {
avifDiagnosticsPrintf(diag, "clap height is not an integer");
return AVIF_FALSE;
}
- clapFraction horizOff;
- horizOff.n = clap->horizOffN;
- horizOff.d = clap->horizOffD;
- clapFraction vertOff;
- vertOff.n = clap->vertOffN;
- vertOff.d = clap->vertOffD;
+ clapFraction uncroppedCenterX = calcCenter((int32_t)imageW);
+ clapFraction uncroppedCenterY = calcCenter((int32_t)imageH);
- clapFraction uncroppedCenterX = calcCenter(imageW);
- clapFraction uncroppedCenterY = calcCenter(imageH);
+ clapFraction horizOff;
+ horizOff.n = horizOffN;
+ horizOff.d = horizOffD;
clapFraction croppedCenterX = clapFractionAdd(uncroppedCenterX, horizOff);
+
+ clapFraction vertOff;
+ vertOff.n = vertOffN;
+ vertOff.d = vertOffD;
clapFraction croppedCenterY = clapFractionAdd(uncroppedCenterY, vertOff);
clapFraction halfW;
- halfW.n = clap->widthN;
- halfW.d = clap->widthD * 2;
+ halfW.n = widthN;
+ halfW.d = widthD * 2;
clapFraction cropX = clapFractionSub(croppedCenterX, halfW);
- if (((int32_t)cropX.n % (int32_t)cropX.d) != 0) {
- avifDiagnosticsPrintf(diag, "crop X offset is not an integer");
+ if ((cropX.n % cropX.d) != 0) {
+ avifDiagnosticsPrintf(diag, "calculated crop X offset is not an integer");
return AVIF_FALSE;
}
clapFraction halfH;
- halfH.n = clap->heightN;
- halfH.d = clap->heightD * 2;
+ halfH.n = heightN;
+ halfH.d = heightD * 2;
clapFraction cropY = clapFractionSub(croppedCenterY, halfH);
if (((int32_t)cropY.n % (int32_t)cropY.d) != 0) {
- avifDiagnosticsPrintf(diag, "crop Y offset is not an integer");
+ avifDiagnosticsPrintf(diag, "calculated crop Y offset is not an integer");
return AVIF_FALSE;
}
- cropRect->x = cropX.n / cropX.d;
- cropRect->y = cropY.n / cropY.d;
- cropRect->width = clap->widthN / clap->widthD;
- cropRect->height = clap->heightN / clap->heightD;
+ if ((cropX.n < 0) || (cropY.n < 0)) {
+ avifDiagnosticsPrintf(diag, "at least one crop offset is not positive");
+ return AVIF_FALSE;
+ }
+
+ cropRect->x = (uint32_t)(cropX.n / cropX.d);
+ cropRect->y = (uint32_t)(cropY.n / cropY.d);
+ cropRect->width = (uint32_t)(clap->widthN / clap->widthD);
+ cropRect->height = (uint32_t)(clap->heightN / clap->heightD);
return avifCropRectIsValid(cropRect, imageW, imageH, yuvFormat, diag);
}
diff --git a/src/read.c b/src/read.c
index 5426fc2..0d0b756 100644
--- a/src/read.c
+++ b/src/read.c
@@ -285,6 +285,19 @@
return 8;
}
+// This is used as a hint to validating the clap box in avifDecoderItemValidateAV1.
+static avifPixelFormat avifCodecConfigurationBoxGetFormat(const avifCodecConfigurationBox * av1C)
+{
+ if (av1C->monochrome) {
+ return AVIF_PIXEL_FORMAT_YUV400;
+ } else if ((av1C->chromaSubsamplingX == 1) && (av1C->chromaSubsamplingY == 1)) {
+ return AVIF_PIXEL_FORMAT_YUV420;
+ } else if ((av1C->chromaSubsamplingX == 1) && (av1C->chromaSubsamplingY == 0)) {
+ return AVIF_PIXEL_FORMAT_YUV422;
+ }
+ return AVIF_PIXEL_FORMAT_YUV444;
+}
+
static const avifPropertyArray * avifSampleTableGetProperties(const avifSampleTable * sampleTable)
{
for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) {
@@ -733,7 +746,6 @@
avifDiagnosticsPrintf(diag, "Item ID %u is missing mandatory av1C property", item->id);
return AVIF_RESULT_BMFF_PARSE_FAILED;
}
- const uint32_t av1CDepth = avifCodecConfigurationBoxGetDepth(&av1CProp->u.av1C);
const avifProperty * pixiProp = avifPropertyArrayFind(&item->properties, "pixi");
if (!pixiProp && (strictFlags & AVIF_STRICT_PIXI_REQUIRED)) {
@@ -743,6 +755,7 @@
}
if (pixiProp) {
+ const uint32_t av1CDepth = avifCodecConfigurationBoxGetDepth(&av1CProp->u.av1C);
for (uint8_t i = 0; i < pixiProp->u.pixi.planeCount; ++i) {
if (pixiProp->u.pixi.planeDepths[i] != av1CDepth) {
// pixi depth must match av1C depth
@@ -755,6 +768,26 @@
}
}
}
+
+ if (strictFlags & AVIF_STRICT_CLAP_VALID) {
+ const avifProperty * clapProp = avifPropertyArrayFind(&item->properties, "clap");
+ if (clapProp) {
+ const avifProperty * ispeProp = avifPropertyArrayFind(&item->properties, "ispe");
+ if (!ispeProp) {
+ avifDiagnosticsPrintf(diag, "Item ID %u is missing an ispe property, so its clap property cannot be validated", item->id);
+ return AVIF_RESULT_BMFF_PARSE_FAILED;
+ }
+
+ avifCropRect cropRect;
+ const uint32_t imageW = ispeProp->u.ispe.width;
+ const uint32_t imageH = ispeProp->u.ispe.height;
+ const avifPixelFormat av1CFormat = avifCodecConfigurationBoxGetFormat(&av1CProp->u.av1C);
+ avifBool validClap = avifCropRectConvertCleanApertureBox(&cropRect, &clapProp->u.clap, imageW, imageH, av1CFormat, diag);
+ if (!validClap) {
+ return AVIF_RESULT_BMFF_PARSE_FAILED;
+ }
+ }
+ }
return AVIF_RESULT_OK;
}