diff --git a/.github/workflows/ci-fuzztest.yml b/.github/workflows/ci-fuzztest.yml index 03c57a3dbd..bad1b3fa82 100644 --- a/.github/workflows/ci-fuzztest.yml +++ b/.github/workflows/ci-fuzztest.yml @@ -48,6 +48,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_FUZZTEST=LOCAL -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DAVIF_ENABLE_WERROR=ON diff --git a/.github/workflows/ci-linux-coverage.yml b/.github/workflows/ci-linux-coverage.yml index 582b612cc1..6cfcf6d7c0 100644 --- a/.github/workflows/ci-linux-coverage.yml +++ b/.github/workflows/ci-linux-coverage.yml @@ -43,6 +43,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON -DAVIF_ENABLE_COVERAGE=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ - name: Build libavif (ninja) diff --git a/.github/workflows/ci-unix-shared-local.yml b/.github/workflows/ci-unix-shared-local.yml index f8edd3476c..dd2c3aeaa7 100644 --- a/.github/workflows/ci-unix-shared-local.yml +++ b/.github/workflows/ci-unix-shared-local.yml @@ -57,6 +57,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON - name: Build libavif (ninja) working-directory: ./build diff --git a/.github/workflows/ci-unix-static.yml b/.github/workflows/ci-unix-static.yml index 38d043accd..00e7c5b60f 100644 --- a/.github/workflows/ci-unix-static.yml +++ b/.github/workflows/ci-unix-static.yml @@ -62,6 +62,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON - name: Build libavif (ninja) working-directory: ./build diff --git a/.github/workflows/ci-windows-installed.yml b/.github/workflows/ci-windows-installed.yml index 03de5067e1..1c57b8cef5 100644 --- a/.github/workflows/ci-windows-installed.yml +++ b/.github/workflows/ci-windows-installed.yml @@ -76,6 +76,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON $env:AVIF_CMAKE_C_COMPILER $env:AVIF_CMAKE_CXX_COMPILER - name: Build libavif (ninja) working-directory: ./build diff --git a/.github/workflows/ci-windows-shared-local.yml b/.github/workflows/ci-windows-shared-local.yml index d6880a42f2..b3d99c08da 100644 --- a/.github/workflows/ci-windows-shared-local.yml +++ b/.github/workflows/ci-windows-shared-local.yml @@ -57,6 +57,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON $env:AVIF_CMAKE_C_COMPILER $env:AVIF_CMAKE_CXX_COMPILER - name: Build libavif (ninja) working-directory: ./build diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index def6741b17..c97c1dd851 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -70,6 +70,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_MINI=ON -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI=ON -DAVIF_ENABLE_WERROR=ON - name: Build libavif (ninja) working-directory: ./build diff --git a/CHANGELOG.md b/CHANGELOG.md index 10dfd7ae45..dd41170dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -268,6 +268,8 @@ List of incompatible ABI changes in this release: * Add avifenc --no-overwrite flag to avoid overwriting output file. * Add avifenc --clli flag to set clli. * Add support for all transfer functions when using libsharpyuv. +* Add experimental support for PixelInformationProperty syntax from HEIF 3rd Ed. + Amd2 behind the compilation flag AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI. ### Changed * Enable the libaom AV1E_SET_SKIP_POSTPROC_FILTERING codec control by default. diff --git a/CMakeLists.txt b/CMakeLists.txt index be45725957..46ca8f4dbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ option(AVIF_ENABLE_WERROR "Treat all compiler warnings as errors" OFF) option(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R "Enable experimental YCgCo-R matrix code" OFF) option(AVIF_ENABLE_EXPERIMENTAL_MINI "Enable experimental reduced header" OFF) option(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM "Enable experimental sample transform code" OFF) +option(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI "Enable experimental PixelInformationProperty syntax from HEIF 3rd Ed. Amd2" OFF) set(AVIF_PKG_CONFIG_EXTRA_LIBS_PRIVATE "") set(AVIF_PKG_CONFIG_EXTRA_REQUIRES_PRIVATE "") @@ -351,6 +352,10 @@ if(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) endif() +if(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) +endif() + set(AVIF_SRCS src/alpha.c src/avif.c diff --git a/include/avif/avif.h b/include/avif/avif.h index 1402d95b6d..4417e1c7e7 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -220,6 +220,11 @@ typedef enum avifHeaderFormat // WARNING: Experimental feature. Produces files that are incompatible with older decoders. AVIF_HEADER_REDUCED, #endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + // Use the full syntax of the PixelInformationProperty from HEIF 3rd edition Amendment 2. + // WARNING: Experimental feature. Produces files that may be incompatible with older decoders. + AVIF_HEADER_FULL_WITH_EXTENDED_PIXI, +#endif } avifHeaderFormat; // --------------------------------------------------------------------------- diff --git a/include/avif/internal.h b/include/avif/internal.h index 426d8e49aa..848c5863f5 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -704,7 +704,7 @@ AVIF_NODISCARD avifBool avifROStreamReadString(avifROStream * stream, char * out AVIF_NODISCARD avifBool avifROStreamReadBoxHeader(avifROStream * stream, avifBoxHeader * header); // This fails if the size reported by the header cannot fit in the stream AVIF_NODISCARD avifBool avifROStreamReadBoxHeaderPartial(avifROStream * stream, avifBoxHeader * header, avifBool topLevel); // This doesn't require that the full box can fit in the stream AVIF_NODISCARD avifBool avifROStreamReadVersionAndFlags(avifROStream * stream, uint8_t * version, uint32_t * flags); // version and flags ptrs are both optional -AVIF_NODISCARD avifBool avifROStreamReadAndEnforceVersion(avifROStream * stream, uint8_t enforcedVersion); // currently discards flags +AVIF_NODISCARD avifBool avifROStreamReadAndEnforceVersion(avifROStream * stream, uint8_t enforcedVersion, uint32_t * flags); // flags ptr is optional // The following functions can read non-aligned bits. AVIF_NODISCARD avifBool avifROStreamSkipBits(avifROStream * stream, size_t bitCount); AVIF_NODISCARD avifBool avifROStreamReadBitsU8(avifROStream * stream, uint8_t * v, size_t bitCount); @@ -788,6 +788,23 @@ typedef struct avifSequenceHeader AVIF_NODISCARD avifBool avifSequenceHeaderParse(avifSequenceHeader * header, const avifROData * sample, avifCodecType codecType); +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) +// Subsampling type as defined in ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3. +typedef enum avifPixiSubsamplingType +{ + AVIF_PIXI_444 = 0, + AVIF_PIXI_422 = 1, + AVIF_PIXI_420 = 2, + AVIF_PIXI_411 = 3, + AVIF_PIXI_440 = 4, + AVIF_PIXI_SUBSAMPLING_RESERVED = 5, +} avifPixiSubsamplingType; + +// Mapping from subsampling_x, subsampling_y as defined in AV1 specification Section 6.4.2 +// to PixelInformationBox subsampling_type as defined in ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3. +uint8_t avifCodecConfigurationBoxGetSubsamplingType(const avifCodecConfigurationBox * av1C, uint8_t channelIndex); +#endif + // --------------------------------------------------------------------------- // gain maps diff --git a/src/read.c b/src/read.c index 1f75461927..73427c3fa7 100644 --- a/src/read.c +++ b/src/read.c @@ -122,6 +122,12 @@ typedef struct avifPixelInformationProperty { uint8_t planeDepths[MAX_PIXI_PLANE_DEPTHS]; uint8_t planeCount; +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + avifBool hasExtendedFields; // The fields below were signaled if this is true. + uint8_t subsamplingFlag[MAX_PIXI_PLANE_DEPTHS]; // The fields below were signaled if this is true for a given channel. + uint8_t subsamplingType[MAX_PIXI_PLANE_DEPTHS]; + uint8_t subsamplingLocation[MAX_PIXI_PLANE_DEPTHS]; +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI } avifPixelInformationProperty; typedef struct avifOperatingPointSelectorProperty @@ -367,6 +373,61 @@ static uint32_t avifCodecConfigurationBoxGetDepth(const avifCodecConfigurationBo return 8; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) +uint8_t avifCodecConfigurationBoxGetSubsamplingType(const avifCodecConfigurationBox * av1C, uint8_t channelIndex) +{ + if (channelIndex == 0) { + return AVIF_PIXI_444; + } + if (av1C->chromaSubsamplingX == 0) { + if (av1C->chromaSubsamplingY == 0) { + return AVIF_PIXI_444; + } + return AVIF_PIXI_440; + } + if (av1C->chromaSubsamplingY == 0) { + return AVIF_PIXI_422; + } + return AVIF_PIXI_420; +} + +// Mapping from PixelInformationBox subsampling_type and subsampling_location as defined in ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3 +// to chroma_sample_position as defined in AV1 specification Section 6.4.2. +static uint8_t avifSubsamplingLocationToChromaSamplePosition(uint8_t subsamplingType, uint8_t subsamplingLocation) +{ + if (subsamplingType == AVIF_PIXI_444) { + return AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + } + if (subsamplingType == AVIF_PIXI_422) { + if (subsamplingLocation == 0 || subsamplingLocation == 2 || subsamplingLocation == 4) { + return AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + } + } + if (subsamplingType == AVIF_PIXI_420) { + if (subsamplingLocation == 0) { + return AVIF_CHROMA_SAMPLE_POSITION_VERTICAL; + } + if (subsamplingLocation == 2) { + return AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + } + } + if (subsamplingType == AVIF_PIXI_411) { + if (subsamplingLocation == 0 || subsamplingLocation == 2 || subsamplingLocation == 4) { + return AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + } + } + if (subsamplingType == AVIF_PIXI_440) { + if (subsamplingLocation == 0 || subsamplingLocation == 1) { + return AVIF_CHROMA_SAMPLE_POSITION_VERTICAL; + } + if (subsamplingLocation == 2 || subsamplingLocation == 3) { + return AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + } + } + return AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI + static const avifPropertyArray * avifSampleTableGetProperties(const avifSampleTable * sampleTable, avifCodecType codecType) { for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) { @@ -1221,6 +1282,38 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item configDepth); return AVIF_RESULT_BMFF_PARSE_FAILED; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + if (pixiProp->u.pixi.subsamplingFlag[i]) { + if (pixiProp->u.pixi.subsamplingType[i] != avifCodecConfigurationBoxGetSubsamplingType(&configProp->u.av1C, i)) { + avifDiagnosticsPrintf(diag, + "Item ID %u subsampling type specified by pixi property [%u] for channel %u does not match %s property [%u,%u]", + item->id, + pixiProp->u.pixi.subsamplingType[i], + i, + configPropName, + configProp->u.av1C.chromaSubsamplingX, + configProp->u.av1C.chromaSubsamplingY); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + if (configProp->u.av1C.chromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN) { + const avifChromaSamplePosition expectedChromaSamplePosition = + i == AVIF_CHAN_Y ? AVIF_CHROMA_SAMPLE_POSITION_COLOCATED : configProp->u.av1C.chromaSamplePosition; + if (avifSubsamplingLocationToChromaSamplePosition(pixiProp->u.pixi.subsamplingType[i], + pixiProp->u.pixi.subsamplingLocation[i]) != + expectedChromaSamplePosition) { + avifDiagnosticsPrintf(diag, + "Item ID %u subsampling type and location specified by pixi property [%u,%u] for channel %u does not match %s property chroma sample position [%u]", + item->id, + pixiProp->u.pixi.subsamplingType[i], + pixiProp->u.pixi.subsamplingLocation[i], + i, + configPropName, + configProp->u.av1C.chromaSamplePosition); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + } + } +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI } } @@ -1819,7 +1912,7 @@ static avifBool avifParseHandlerBox(const uint8_t * raw, size_t rawLen, avifDiag { BEGIN_STREAM(s, raw, rawLen, diag, "Box[hdlr]"); - AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, 0)); + AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL)); uint32_t predefined; AVIF_CHECK(avifROStreamReadU32(&s, &predefined)); // unsigned int(32) pre_defined = 0; @@ -2268,7 +2361,7 @@ static avifResult avifDecoderItemReadAndParse(const avifDecoder * decoder, static avifBool avifParseImageSpatialExtentsProperty(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) { BEGIN_STREAM(s, raw, rawLen, diag, "Box[ispe]"); - AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, 0)); + AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL)); avifImageSpatialExtents * ispe = &prop->u.ispe; AVIF_CHECK(avifROStreamReadU32(&s, &ispe->width)); @@ -2279,7 +2372,7 @@ static avifBool avifParseImageSpatialExtentsProperty(avifProperty * prop, const static avifBool avifParseAuxiliaryTypeProperty(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) { BEGIN_STREAM(s, raw, rawLen, diag, "Box[auxC]"); - AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, 0)); + AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL)); AVIF_CHECK(avifROStreamReadString(&s, prop->u.auxC.auxType, AUXTYPE_SIZE)); return AVIF_TRUE; @@ -2556,28 +2649,96 @@ static avifBool avifParseImageMirrorProperty(avifProperty * prop, const uint8_t return AVIF_TRUE; } -static avifBool avifParsePixelInformationProperty(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) +static avifResult avifParsePixelInformationProperty(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) { BEGIN_STREAM(s, raw, rawLen, diag, "Box[pixi]"); - AVIF_CHECK(avifROStreamReadAndEnforceVersion(&s, 0)); + uint32_t flags = 0; // px_flags + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, &flags), AVIF_RESULT_BMFF_PARSE_FAILED); avifPixelInformationProperty * pixi = &prop->u.pixi; - AVIF_CHECK(avifROStreamRead(&s, &pixi->planeCount, 1)); // unsigned int (8) num_channels; + AVIF_CHECKERR(avifROStreamRead(&s, &pixi->planeCount, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int (8) num_channels; if (pixi->planeCount < 1 || pixi->planeCount > MAX_PIXI_PLANE_DEPTHS) { avifDiagnosticsPrintf(diag, "Box[pixi] contains unsupported plane count [%u]", pixi->planeCount); - return AVIF_FALSE; + return AVIF_RESULT_NOT_IMPLEMENTED; } for (uint8_t i = 0; i < pixi->planeCount; ++i) { - AVIF_CHECK(avifROStreamRead(&s, &pixi->planeDepths[i], 1)); // unsigned int (8) bits_per_channel; + AVIF_CHECKERR(avifROStreamRead(&s, &pixi->planeDepths[i], 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int (8) bits_per_channel; +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + if (pixi->planeDepths[i] == 0) { + avifDiagnosticsPrintf(diag, "Box[pixi] plane depth shall not be 0 for channel %u", i); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI if (pixi->planeDepths[i] != pixi->planeDepths[0]) { avifDiagnosticsPrintf(diag, "Box[pixi] contains unsupported mismatched plane depths [%u != %u]", pixi->planeDepths[i], pixi->planeDepths[0]); - return AVIF_FALSE; + return AVIF_RESULT_NOT_IMPLEMENTED; } } - return AVIF_TRUE; +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + if (flags & 1) { + for (uint8_t i = 0; i < pixi->planeCount; ++i) { + uint8_t channelIdc, reserved, componentFormat, channelLabelFlag; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &channelIdc, /*bitCount=*/3), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(3) channel_idc; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &reserved, /*bitCount=*/1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) reserved = 0; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &componentFormat, /*bitCount=*/2), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(2) component_format; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &pixi->subsamplingFlag[i], /*bitCount=*/1), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) subsampling_flag; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &channelLabelFlag, /*bitCount=*/1), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) channel_label_flag; + if (pixi->subsamplingFlag[i]) { + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &pixi->subsamplingType[i], /*bitCount=*/4), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(4) subsampling_type; + AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &pixi->subsamplingLocation[i], /*bitCount=*/4), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(4) subsampling_location; + } + + // ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3: + // This field indicates the contents of the channel. A value of 0 indicates colour/grayscale. A value of + // 1 indicates alpha. A value of 2 indicates depth. Values 3-7 are reserved for future use. At most one + // channel shall have a channel_idc of 1. + if (channelIdc != 0) { + avifDiagnosticsPrintf(diag, "Box[pixi] contains unsupported channel_idc %u for channel %u", channelIdc, i); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + if (reserved != 0) { + avifDiagnosticsPrintf(diag, "Box[pixi] contains non-zero reserved field %u for channel %u", reserved, i); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + // ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3: + // component_format: This field indicates the data type of the channel as defined by the component_format + // values in ISO/IEC 23001-17 where component_bit_depth is considered to be equal to bits_per_channel. + // ISO/IEC 23001-17 section 5.2.1.2: + // component_format: When equal to 0, component value is an unsigned integer coded on component_bit_depth bits. + if (componentFormat != 0) { + avifDiagnosticsPrintf(diag, "Box[pixi] contains unsupported component_format %u for channel %u", componentFormat, i); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + if (pixi->subsamplingFlag[i]) { + if (pixi->subsamplingType[i] >= AVIF_PIXI_SUBSAMPLING_RESERVED) { + avifDiagnosticsPrintf(diag, + "Box[pixi] contains reserved subsampling_type %u for channel %u", + pixi->subsamplingType[i], + i); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + if (pixi->subsamplingLocation[i] > 4) { + avifDiagnosticsPrintf(diag, + "Box[pixi] contains reserved subsampling_location %u for channel %u", + pixi->subsamplingLocation[i], + i); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + } + if (channelLabelFlag) { + AVIF_CHECKERR(avifROStreamReadString(&s, NULL, 0), AVIF_RESULT_BMFF_PARSE_FAILED); // utf8string channel_label; (skipped) + } + } + } +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI + return AVIF_RESULT_OK; } static avifBool avifParseOperatingPointSelectorProperty(avifProperty * prop, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) @@ -2676,8 +2837,7 @@ static avifResult avifParseItemPropertyContainerBox(avifPropertyArray * properti } else if (!memcmp(header.type, "imir", 4)) { AVIF_CHECKERR(avifParseImageMirrorProperty(prop, avifROStreamCurrent(&s), header.size, diag), AVIF_RESULT_BMFF_PARSE_FAILED); } else if (!memcmp(header.type, "pixi", 4)) { - AVIF_CHECKERR(avifParsePixelInformationProperty(prop, avifROStreamCurrent(&s), header.size, diag), - AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKRES(avifParsePixelInformationProperty(prop, avifROStreamCurrent(&s), header.size, diag)); } else if (!memcmp(header.type, "a1op", 4)) { AVIF_CHECKERR(avifParseOperatingPointSelectorProperty(prop, avifROStreamCurrent(&s), header.size, diag), AVIF_RESULT_BMFF_PARSE_FAILED); @@ -3147,14 +3307,8 @@ static avifResult avifParseMetaBox(avifMeta * meta, uint64_t rawOffset, const ui { BEGIN_STREAM(s, raw, rawLen, diag, "Box[meta]"); - uint8_t version; uint32_t flags; - AVIF_CHECKERR(avifROStreamReadVersionAndFlags(&s, &version, &flags), AVIF_RESULT_BMFF_PARSE_FAILED); - - if (version != 0) { - avifDiagnosticsPrintf(diag, "Box[meta]: Expecting box version 0, got version %u", version); - return AVIF_RESULT_BMFF_PARSE_FAILED; - } + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0, &flags), AVIF_RESULT_BMFF_PARSE_FAILED); ++meta->idatID; // for tracking idat @@ -3306,7 +3460,7 @@ static avifResult avifParseChunkOffsetBox(avifSampleTable * sampleTable, avifBoo { BEGIN_STREAM(s, raw, rawLen, diag, largeOffsets ? "Box[co64]" : "Box[stco]"); - AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL), AVIF_RESULT_BMFF_PARSE_FAILED); uint32_t entryCount; AVIF_CHECKERR(avifROStreamReadU32(&s, &entryCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) entry_count; @@ -3331,7 +3485,7 @@ static avifResult avifParseSampleToChunkBox(avifSampleTable * sampleTable, const { BEGIN_STREAM(s, raw, rawLen, diag, "Box[stsc]"); - AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL), AVIF_RESULT_BMFF_PARSE_FAILED); uint32_t entryCount; AVIF_CHECKERR(avifROStreamReadU32(&s, &entryCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) entry_count; @@ -3364,7 +3518,7 @@ static avifResult avifParseSampleSizeBox(avifSampleTable * sampleTable, const ui { BEGIN_STREAM(s, raw, rawLen, diag, "Box[stsz]"); - AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL), AVIF_RESULT_BMFF_PARSE_FAILED); uint32_t allSamplesSize, sampleCount; AVIF_CHECKERR(avifROStreamReadU32(&s, &allSamplesSize), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) sample_size; @@ -3386,7 +3540,7 @@ static avifResult avifParseSyncSampleBox(avifSampleTable * sampleTable, const ui { BEGIN_STREAM(s, raw, rawLen, diag, "Box[stss]"); - AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL), AVIF_RESULT_BMFF_PARSE_FAILED); uint32_t entryCount; AVIF_CHECKERR(avifROStreamReadU32(&s, &entryCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) entry_count; @@ -3405,7 +3559,7 @@ static avifResult avifParseTimeToSampleBox(avifSampleTable * sampleTable, const { BEGIN_STREAM(s, raw, rawLen, diag, "Box[stts]"); - AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, 0), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifROStreamReadAndEnforceVersion(&s, /*enforcedVersion=*/0, /*flags=*/NULL), AVIF_RESULT_BMFF_PARSE_FAILED); uint32_t entryCount; AVIF_CHECKERR(avifROStreamReadU32(&s, &entryCount), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) entry_count; diff --git a/src/stream.c b/src/stream.c index 99c2901b6f..770c8ba042 100644 --- a/src/stream.c +++ b/src/stream.c @@ -317,10 +317,10 @@ avifBool avifROStreamReadVersionAndFlags(avifROStream * stream, uint8_t * versio return AVIF_TRUE; } -avifBool avifROStreamReadAndEnforceVersion(avifROStream * stream, uint8_t enforcedVersion) +avifBool avifROStreamReadAndEnforceVersion(avifROStream * stream, uint8_t enforcedVersion, uint32_t * flags) { uint8_t version; - AVIF_CHECK(avifROStreamReadVersionAndFlags(stream, &version, NULL)); + AVIF_CHECK(avifROStreamReadVersionAndFlags(stream, &version, flags)); if (version != enforcedVersion) { avifDiagnosticsPrintf(stream->diag, "%s: Expecting box version %u, got version %u", stream->diagContext, enforcedVersion, version); return AVIF_FALSE; diff --git a/src/write.c b/src/write.c index 6ab66e105d..892d5aa976 100644 --- a/src/write.c +++ b/src/write.c @@ -2953,12 +2953,44 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu if (hasPixi) { avifItemPropertyDedupStart(dedup); uint8_t channelCount = (isAlpha || (itemMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) ? 1 : 3; + // See ISO/IEC 23008-12:2024/CDAM 2:2025 section 6.5.6.3. avifBoxMarker pixi; - AVIF_CHECKRES(avifRWStreamWriteFullBox(&dedup->s, "pixi", AVIF_BOX_SIZE_TBD, 0, 0, &pixi)); + uint32_t flags = 0; +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + if (encoder->headerFormat == AVIF_HEADER_FULL_WITH_EXTENDED_PIXI) { + flags |= 1; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI + AVIF_CHECKRES(avifRWStreamWriteFullBox(&dedup->s, "pixi", AVIF_BOX_SIZE_TBD, 0, flags, &pixi)); AVIF_CHECKRES(avifRWStreamWriteU8(&dedup->s, channelCount)); // unsigned int (8) num_channels; for (uint8_t chan = 0; chan < channelCount; ++chan) { AVIF_CHECKRES(avifRWStreamWriteU8(&dedup->s, depth)); // unsigned int (8) bits_per_channel; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + if (flags & 1) { + AVIF_ASSERT_OR_RETURN(item->av1C.chromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_RESERVED); + // Do not signal any subsampling information if the sample position is unknown because the 'pixi' box + // does not have an enum entry for "unknown subsampling location". + const uint8_t subsampling_flag = item->av1C.chromaSamplePosition == AVIF_CHROMA_SAMPLE_POSITION_VERTICAL || + item->av1C.chromaSamplePosition == AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; + for (uint8_t chan = 0; chan < channelCount; ++chan) { + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, 0, /*bitCount=*/3)); // unsigned int(3) channel_idc; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, 0, /*bitCount=*/1)); // unsigned int(1) reserved; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, 0, /*bitCount=*/2)); // unsigned int(2) component_format; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, subsampling_flag, /*bitCount=*/1)); // unsigned int(1) subsampling_flag; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, 0, /*bitCount=*/1)); // unsigned int(1) channel_label_flag; + if (subsampling_flag) { + const uint8_t subsamplingType = avifCodecConfigurationBoxGetSubsamplingType(&item->av1C, chan); + const uint8_t subsamplingLocation = subsamplingType == AVIF_PIXI_444 ? 0 + : item->av1C.chromaSamplePosition == AVIF_CHROMA_SAMPLE_POSITION_VERTICAL + ? 0 + : 2; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, subsamplingType, /*bitCount=*/4)); // unsigned int(4) subsampling_type; + AVIF_CHECKRES(avifRWStreamWriteBits(&dedup->s, subsamplingLocation, /*bitCount=*/4)); // unsigned int(4) subsampling_location; + } + } + } +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI avifRWStreamFinishBox(&dedup->s, pixi); AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, s, &item->associations, /*essential=*/AVIF_FALSE)); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3ecff24577..1341c9c5e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -133,6 +133,12 @@ if(AVIF_GTEST) endif() add_avif_gtest(avifopaquetest) + + add_avif_gtest_with_data(avifpixitest) + if(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + target_compile_definitions(avifpixitest PRIVATE AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) + endif() + add_avif_gtest_with_data(avifpng16bittest) add_avif_gtest_with_data(avifprogressivetest) add_avif_gtest_with_data(avifpropertytest) @@ -155,7 +161,8 @@ if(AVIF_GTEST) # these tests are disabled because other codecs may not implement all the necessary features. # For example, SVT-AV1 requires 4:2:0 images with even dimensions of at least 64x64 px. set_tests_properties( - avifallocationtest avifgridapitest avifincrtest aviflosslesstest avifmetadatatest PROPERTIES DISABLED True + avifallocationtest avifgridapitest avifincrtest aviflosslesstest avifmetadatatest avifpixitest PROPERTIES DISABLED + True ) message(STATUS "Some tests are disabled because aom is unavailable for encoding or decoding.") @@ -360,6 +367,7 @@ if(AVIF_CODEC_AVM_ENABLED) avifincrtest avifiostatstest avifmetadatatest + avifpixitest avifprogressivetest avifpropertytest avifrangetest diff --git a/tests/data/README.md b/tests/data/README.md index 280959fadd..4fc92bb199 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -72,6 +72,14 @@ Source: Generated with ImageMagick's `convert` command: It is of color type 3 (PNG_COLOR_TYPE_PALETTE) and has a tRNS chunk. +### File [extended_pixi.avif](extended_pixi.avif) + +![](extended_pixi.avif) + +License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) + +Source: Generated with `avifpixitest` (4:2:0, vertical chroma sample position). + ### File [white_1x1.avif](white_1x1.avif) ![](white_1x1.avif) diff --git a/tests/data/extended_pixi.avif b/tests/data/extended_pixi.avif new file mode 100644 index 0000000000..7b760f6264 Binary files /dev/null and b/tests/data/extended_pixi.avif differ diff --git a/tests/gtest/avifpixitest.cc b/tests/gtest/avifpixitest.cc new file mode 100644 index 0000000000..ca2688eeab --- /dev/null +++ b/tests/gtest/avifpixitest.cc @@ -0,0 +1,106 @@ +// Copyright 2025 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +using ::testing::Combine; +using ::testing::Values; + +namespace avif { +namespace { + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + +//------------------------------------------------------------------------------ + +#if defined(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) +TEST(AvifPixiTest, SameOutput) { + ImagePtr image = + testutil::CreateImage(4, 4, 8, AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_YUV); + ASSERT_NE(image, nullptr); + image->yuvChromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_VERTICAL; + testutil::FillImageGradient(image.get()); // The pixels do not matter. + + // Encode. + + testutil::AvifRwData encoded_regular_pixi; + EncoderPtr encoder_regular_pixi(avifEncoderCreate()); + ASSERT_NE(encoder_regular_pixi, nullptr); + encoder_regular_pixi->speed = AVIF_SPEED_FASTEST; + encoder_regular_pixi->headerFormat = AVIF_HEADER_FULL; + ASSERT_EQ(avifEncoderWrite(encoder_regular_pixi.get(), image.get(), + &encoded_regular_pixi), + AVIF_RESULT_OK); + + testutil::AvifRwData encoded_extended_pixi; + EncoderPtr encoder_extended_pixi(avifEncoderCreate()); + ASSERT_NE(encoder_extended_pixi, nullptr); + encoder_extended_pixi->speed = AVIF_SPEED_FASTEST; + encoder_extended_pixi->headerFormat = AVIF_HEADER_FULL_WITH_EXTENDED_PIXI; + ASSERT_EQ(avifEncoderWrite(encoder_extended_pixi.get(), image.get(), + &encoded_extended_pixi), + AVIF_RESULT_OK); + EXPECT_LT(encoded_regular_pixi.size, encoded_extended_pixi.size); + + // Decode. + + ImagePtr decoded_regular_pixi(avifImageCreateEmpty()); + ASSERT_NE(decoded_regular_pixi, nullptr); + DecoderPtr decoder_regular_pixi(avifDecoderCreate()); + ASSERT_NE(decoder_regular_pixi, nullptr); + decoder_regular_pixi->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP; + ASSERT_EQ(avifDecoderReadMemory( + decoder_regular_pixi.get(), decoded_regular_pixi.get(), + encoded_regular_pixi.data, encoded_regular_pixi.size), + AVIF_RESULT_OK); + + ImagePtr decoded_extended_pixi(avifImageCreateEmpty()); + ASSERT_NE(decoded_extended_pixi, nullptr); + DecoderPtr decoder_extended_pixi(avifDecoderCreate()); + ASSERT_NE(decoder_extended_pixi, nullptr); + decoder_extended_pixi->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP; + ASSERT_EQ(avifDecoderReadMemory( + decoder_extended_pixi.get(), decoded_extended_pixi.get(), + encoded_extended_pixi.data, encoded_extended_pixi.size), + AVIF_RESULT_OK); + + EXPECT_TRUE( + testutil::AreImagesEqual(*decoded_regular_pixi, *decoded_extended_pixi)); +} +#endif // AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI + +TEST(AvifPixiTest, ExtendedPixiWorksEvenWithoutCMakeFlagOn) { + const testutil::AvifRwData avif = + testutil::ReadFile(std::string(data_path) + "extended_pixi.avif"); + ASSERT_NE(avif.size, 0u); + ImagePtr image(avifImageCreateEmpty()); + ASSERT_NE(image, nullptr); + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + ASSERT_EQ( + avifDecoderReadMemory(decoder.get(), image.get(), avif.data, avif.size), + AVIF_RESULT_OK); + EXPECT_EQ(image->yuvFormat, AVIF_PIXEL_FORMAT_YUV420); + EXPECT_EQ(image->yuvChromaSamplePosition, + AVIF_CHROMA_SAMPLE_POSITION_VERTICAL); +} + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace avif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc != 2) { + std::cerr << "There must be exactly one argument containing the path to " + "the test data folder" + << std::endl; + return 1; + } + avif::data_path = argv[1]; + return RUN_ALL_TESTS(); +}