diff --git a/.github/workflows/ci-unix-static.yml b/.github/workflows/ci-unix-static.yml index 1ab32f0a1a..529e6b3055 100644 --- a/.github/workflows/ci-unix-static.yml +++ b/.github/workflows/ci-unix-static.yml @@ -109,6 +109,7 @@ jobs: -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_GAIN_MAP=ON -DAVIF_ENABLE_EXPERIMENTAL_AVIR=ON + -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON -DAVIF_ENABLE_WERROR=ON - name: Build libavif (ninja) working-directory: ./build diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index a220aba6d6..eed77eb1e3 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -125,7 +125,9 @@ jobs: -DAVIF_BUILD_TESTS=ON -DAVIF_ENABLE_GTEST=ON -DAVIF_LOCAL_GTEST=ON -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON -DAVIF_ENABLE_EXPERIMENTAL_GAIN_MAP=ON - -DAVIF_ENABLE_EXPERIMENTAL_AVIR=ON -DAVIF_ENABLE_WERROR=ON + -DAVIF_ENABLE_EXPERIMENTAL_AVIR=ON + -DAVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM=ON + -DAVIF_ENABLE_WERROR=ON - name: Build libavif (ninja) working-directory: ./build run: ninja diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c0c4c9bc..9a53a46908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ The changes are relative to the previous release, unless the baseline is specifi * Require libyuv by default (but it can still be disabled with -DAVIF_LIBYUV=OFF). * Add avifdec --icc flag to override the output color profile. +* Add experimental API for reading and writing 16-bit AVIF files behind the + compilation flag AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM. ### Changed since 1.0.0 * Update aom.cmd: v3.8.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 30644e8a6b..67fe674cc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ option(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP "Enable experimental gain map code (for HDR images that look good both on HDR and SDR displays)" OFF ) option(AVIF_ENABLE_EXPERIMENTAL_AVIR "Enable experimental reduced header" OFF) +option(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM "Enable experimental sample transform code" OFF) set(AVIF_PKG_CONFIG_EXTRA_LIBS_PRIVATE "") set(AVIF_PKG_CONFIG_EXTRA_REQUIRES_PRIVATE "") @@ -328,6 +329,10 @@ if(AVIF_ENABLE_EXPERIMENTAL_AVIR) add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_AVIR) endif() +if(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +endif() + set(AVIF_SRCS src/alpha.c src/avif.c @@ -351,6 +356,9 @@ set(AVIF_SRCS if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) list(APPEND AVIF_SRCS src/gainmap.c) endif() +if(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + list(APPEND AVIF_SRCS src/sampletransform.c) +endif() if(AVIF_ENABLE_COMPLIANCE_WARDEN) if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/ComplianceWarden") diff --git a/include/avif/avif.h b/include/avif/avif.h index 63d7077474..60da1f5e0d 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -192,6 +192,10 @@ typedef enum AVIF_NODISCARD avifResult AVIF_RESULT_DECODE_GAIN_MAP_FAILED = 31, AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE = 32, #endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + AVIF_RESULT_ENCODE_SAMPLE_TRANSFORM_FAILED = 33, + AVIF_RESULT_DECODE_SAMPLE_TRANSFORM_FAILED = 34, +#endif // Kept for backward compatibility; please use the symbols above instead. AVIF_RESULT_NO_AV1_ITEMS_FOUND = AVIF_RESULT_MISSING_IMAGE_ITEM @@ -706,6 +710,39 @@ AVIF_NODISCARD AVIF_API avifBool avifGainMapMetadataFractionsToDouble(avifGainMa #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +// --------------------------------------------------------------------------- + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +// Sample Transforms are a HIGHLY EXPERIMENTAL FEATURE. The format might still +// change and images containing a sample transform item encoded with the current +// version of libavif might not decode with a future version of libavif. +// Use are your own risk. +// This is based on a proposal from the Alliance for Open Media. + +typedef enum avifSampleTransformRecipe +{ + AVIF_SAMPLE_TRANSFORM_NONE, + // Encode the 8 most significant bits of each input image sample losslessly + // into one base image. The remaining least 8 significant bits are encoded + // in a separate hidden image item. The two are combined at decoding into + // one image with the same bit depth as the original image. + // It is backward compatible in the sense that only the base image may be + // decoded (ignoring the hidden image item), leading to a valid image but + // with precision loss (16-bit samples truncated to the 8 most significant + // bits). + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B, + // Encode the 12 most significant bits of each input image sample losslessly + // into one base image. The remaining least 4 significant bits are encoded + // in a separate hidden image item. The two are combined at decoding into + // one image with the same bit depth as the original image. + // It is backward compatible in the sense that only the base image may be + // decoded (ignoring the hidden image item), leading to a valid image but + // with precision loss (16-bit samples truncated to the 12 most significant + // bits). + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B +} avifSampleTransformRecipe; +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + // --------------------------------------------------------------------------- // avifImage @@ -1458,6 +1495,11 @@ typedef struct avifEncoder #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) int qualityGainMap; // changeable encoder setting #endif + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // Perform extra steps at encoding and decoding to extend AV1 features as bundled additional image items. + avifSampleTransformRecipe sampleTransformRecipe; +#endif } avifEncoder; // avifEncoderCreate() returns NULL if a memory allocation failed. diff --git a/include/avif/internal.h b/include/avif/internal.h index 3cf3aa016b..9093b1b42f 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -156,6 +156,79 @@ void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage); // Ignores the gainMap field (which exists only if AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP is defined). void avifImageCopySamples(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes); +// --------------------------------------------------------------------------- + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +// Mapping used in the coding of Sample Transform metadata. +typedef enum avifSampleTransformBitDepth +{ + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_8 = 0, // Signed 8-bit. + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_16 = 1, // Signed 16-bit. + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32 = 2, // Signed 32-bit. + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_64 = 3 // Signed 64-bit. +} avifSampleTransformBitDepth; + +// Meaning of an operand or operator in Sample Transform metadata. +typedef enum avifSampleTransformTokenType +{ + // Operands. + AVIF_SAMPLE_TRANSFORM_CONSTANT = 0, + AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX = 1, + + // Operators. L is the left operand and R is the right operand. + AVIF_SAMPLE_TRANSFORM_SUM = 2, // S = L + R + AVIF_SAMPLE_TRANSFORM_DIFFERENCE = 3, // S = L - R + AVIF_SAMPLE_TRANSFORM_PRODUCT = 4, // S = L * R + AVIF_SAMPLE_TRANSFORM_DIVIDE = 5, // S = R==0 ? L : floor(L / R) + AVIF_SAMPLE_TRANSFORM_AND = 6, // S = L & R + AVIF_SAMPLE_TRANSFORM_OR = 7, // S = L | R + AVIF_SAMPLE_TRANSFORM_XOR = 8, // S = L ^ R + AVIF_SAMPLE_TRANSFORM_NOR = 9, // S = ~(L | R) + AVIF_SAMPLE_TRANSFORM_MSB = 10, // S = L<=0 ? R : floor(log2(L)) + AVIF_SAMPLE_TRANSFORM_POW = 11, // S = pow(L, abs(R)) + AVIF_SAMPLE_TRANSFORM_MIN = 12, // S = L<=R ? L : R + AVIF_SAMPLE_TRANSFORM_MAX = 13, // S = L<=R ? R : L + AVIF_SAMPLE_TRANSFORM_RESERVED +} avifSampleTransformTokenType; + +typedef struct avifSampleTransformToken +{ + uint8_t value; // avifSampleTransformTokenType + int32_t constant; // If value is AVIF_SAMPLE_TRANSFORM_CONSTANT. + uint8_t inputImageItemIndex; // If value is AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX. 1-based. +} avifSampleTransformToken; + +AVIF_ARRAY_DECLARE(avifSampleTransformExpression, avifSampleTransformToken, tokens); +avifBool avifSampleTransformExpressionIsValid(const avifSampleTransformExpression * expression, uint32_t numInputImageItems); +avifBool avifSampleTransformExpressionIsEquivalentTo(const avifSampleTransformExpression * a, const avifSampleTransformExpression * b); + +avifResult avifSampleTransformRecipeToExpression(avifSampleTransformRecipe recipe, avifSampleTransformExpression * expression); +avifResult avifSampleTransformExpressionToRecipe(const avifSampleTransformExpression * expression, avifSampleTransformRecipe * recipe); + +// Applies the expression to the samples of the inputImageItems in the selected planes and stores +// the results in dstImage. dstImage can be part of the inputImageItems. +// dstImage and inputImageItems must be allocated and have the same planes and dimensions. +avifResult avifImageApplyExpression(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + const avifSampleTransformExpression * expression, + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes); + +// Same as avifImageApplyExpression(). Convenience function. +avifResult avifImageApplyOperations(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + uint32_t numTokens, + const avifSampleTransformToken tokens[], + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes); + +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + +// --------------------------------------------------------------------------- +// Alpha + typedef struct avifAlphaParams { uint32_t width; @@ -317,10 +390,31 @@ typedef enum avifItemCategory AVIF_ITEM_ALPHA, #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) AVIF_ITEM_GAIN_MAP, +#endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + AVIF_ITEM_SAMPLE_TRANSFORM, // Sample Transform derived image item 'sato'. + // Extra input image items for AVIF_ITEM_SAMPLE_TRANSFORM. "Extra" because AVIF_ITEM_COLOR could be one too. + AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR, + AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_COLOR, + AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA, + AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_ALPHA, #endif AVIF_ITEM_CATEGORY_COUNT } avifItemCategory; +avifBool avifIsAlpha(avifItemCategory itemCategory); + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +#define AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS \ + (AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA - AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR) +#define AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS \ + (1 /* for AVIF_ITEM_COLOR */ + AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS) + +#define AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR +#define AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY \ + (AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA + AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS - 1) +#endif + // --------------------------------------------------------------------------- #if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) diff --git a/src/avif.c b/src/avif.c index 264d004572..ecc4eec2f4 100644 --- a/src/avif.c +++ b/src/avif.c @@ -108,6 +108,10 @@ const char * avifResultToString(avifResult result) case AVIF_RESULT_ENCODE_GAIN_MAP_FAILED: return "Encoding of gain map planes failed"; case AVIF_RESULT_DECODE_GAIN_MAP_FAILED: return "Decoding of gain map planes failed"; case AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE: return "Invalid tone mapped image item"; +#endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + case AVIF_RESULT_ENCODE_SAMPLE_TRANSFORM_FAILED: return "Encoding of sample transformed image failed"; + case AVIF_RESULT_DECODE_SAMPLE_TRANSFORM_FAILED: return "Decoding of sample transformed image failed"; #endif case AVIF_RESULT_UNKNOWN_ERROR: default: @@ -868,6 +872,22 @@ avifBool avifCleanApertureBoxConvertCropRect(avifCleanApertureBox * clap, // --------------------------------------------------------------------------- +avifBool avifIsAlpha(avifItemCategory itemCategory) +{ + if (itemCategory == AVIF_ITEM_ALPHA) { + return AVIF_TRUE; + } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (itemCategory >= AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA && + itemCategory < AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA + AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS) { + return AVIF_TRUE; + } +#endif + return AVIF_FALSE; +} + +// --------------------------------------------------------------------------- + avifBool avifAreGridDimensionsValid(avifPixelFormat yuvFormat, uint32_t imageW, uint32_t imageH, uint32_t tileW, uint32_t tileH, avifDiagnostics * diag) { // ISO/IEC 23000-22:2019, Section 7.3.11.4.2: diff --git a/src/read.c b/src/read.c index e1a652bc9d..9560e05ad1 100644 --- a/src/read.c +++ b/src/read.c @@ -740,6 +740,13 @@ typedef struct avifMeta // constraints on its optional extendedMeta field, such as forbidden item IDs, properties etc. avifBool fromMini; #endif + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // Parsed from Sample Transform metadata if present, otherwise empty. + avifSampleTransformExpression sampleTransformExpression; + // Bit depth extracted from the pixi property of the Sample Transform derived image item, if any. + uint32_t sampleTransformDepth; +#endif } avifMeta; static void avifMetaDestroy(avifMeta * meta); @@ -772,6 +779,9 @@ static void avifMetaDestroy(avifMeta * meta) avifArrayDestroy(&meta->items); avifArrayDestroy(&meta->properties); avifRWDataFree(&meta->idat); +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + avifArrayDestroy(&meta->sampleTransformExpression); +#endif avifFree(meta); } @@ -863,6 +873,13 @@ typedef struct avifDecoderData // The colour information property takes precedence over any colour information // in the image bitstream, i.e. if the property is present, colour information in // the bitstream shall be ignored. + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // Remember the dimg association order to the Sample Transform derived image item. + // Colour items only. The alpha items are implicit. + uint8_t sampleTransformNumInputImageItems; // At most AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS. + avifItemCategory sampleTransformInputImageItems[AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS]; +#endif } avifDecoderData; static void avifDecoderDataDestroy(avifDecoderData * data); @@ -1500,41 +1517,58 @@ static avifResult avifDecoderGenerateImageGridTiles(avifDecoder * decoder, avifI return AVIF_RESULT_OK; } -// Allocates the dstImage based on the grid image requirements. Also verifies some spec compliance rules for grids. -static avifResult avifDecoderDataAllocateGridImagePlanes(avifDecoderData * data, const avifTileInfo * info, avifImage * dstImage) +// Allocates the dstImage. Also verifies some spec compliance rules for grids, if relevant. +static avifResult avifDecoderDataAllocateImagePlanes(avifDecoderData * data, const avifTileInfo * info, avifImage * dstImage) { - const avifImageGrid * grid = &info->grid; const avifTile * tile = &data->tiles.tile[info->firstTileIndex]; - - // Validate grid image size and tile size. - // - // HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1: - // The tiled input images shall completely "cover" the reconstructed image grid canvas, ... - if (((tile->image->width * grid->columns) < grid->outputWidth) || ((tile->image->height * grid->rows) < grid->outputHeight)) { - avifDiagnosticsPrintf(data->diag, - "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1)"); - return AVIF_RESULT_INVALID_IMAGE_GRID; - } - // Tiles in the rightmost column and bottommost row must overlap the reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section 7.3.11.4.2, Figure 2. - if (((tile->image->width * (grid->columns - 1)) >= grid->outputWidth) || - ((tile->image->height * (grid->rows - 1)) >= grid->outputHeight)) { - avifDiagnosticsPrintf(data->diag, - "Grid image tiles in the rightmost column and bottommost row do not overlap the reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section 7.3.11.4.2, Figure 2"); - return AVIF_RESULT_INVALID_IMAGE_GRID; + uint32_t dstWidth; + uint32_t dstHeight; + + if (info->grid.columns > 0 && info->grid.rows > 0) { + const avifImageGrid * grid = &info->grid; + // Validate grid image size and tile size. + // + // HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1: + // The tiled input images shall completely "cover" the reconstructed image grid canvas, ... + if (((tile->image->width * grid->columns) < grid->outputWidth) || ((tile->image->height * grid->rows) < grid->outputHeight)) { + avifDiagnosticsPrintf(data->diag, + "Grid image tiles do not completely cover the image (HEIF (ISO/IEC 23008-12:2017), Section 6.6.2.3.1)"); + return AVIF_RESULT_INVALID_IMAGE_GRID; + } + // Tiles in the rightmost column and bottommost row must overlap the reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section 7.3.11.4.2, Figure 2. + if (((tile->image->width * (grid->columns - 1)) >= grid->outputWidth) || + ((tile->image->height * (grid->rows - 1)) >= grid->outputHeight)) { + avifDiagnosticsPrintf(data->diag, + "Grid image tiles in the rightmost column and bottommost row do not overlap the reconstructed image grid canvas. See MIAF (ISO/IEC 23000-22:2019), Section 7.3.11.4.2, Figure 2"); + return AVIF_RESULT_INVALID_IMAGE_GRID; + } + if (!avifAreGridDimensionsValid(tile->image->yuvFormat, + grid->outputWidth, + grid->outputHeight, + tile->image->width, + tile->image->height, + data->diag)) { + return AVIF_RESULT_INVALID_IMAGE_GRID; + } + dstWidth = grid->outputWidth; + dstHeight = grid->outputHeight; + } else { + // Only one tile. Width and height are inherited from the 'ispe' property of the corresponding avifDecoderItem. + dstWidth = tile->width; + dstHeight = tile->height; } - avifBool alpha = (tile->input->itemCategory == AVIF_ITEM_ALPHA); + const avifBool alpha = avifIsAlpha(tile->input->itemCategory); if (alpha) { // An alpha tile does not contain any YUV pixels. AVIF_ASSERT_OR_RETURN(tile->image->yuvFormat == AVIF_PIXEL_FORMAT_NONE); } - if (!avifAreGridDimensionsValid(tile->image->yuvFormat, grid->outputWidth, grid->outputHeight, tile->image->width, tile->image->height, data->diag)) { - return AVIF_RESULT_INVALID_IMAGE_GRID; - } + + const uint32_t dstDepth = tile->image->depth; // Lazily populate dstImage with the new frame's properties. - const avifBool dimsOrDepthIsDifferent = (dstImage->width != grid->outputWidth) || (dstImage->height != grid->outputHeight) || - (dstImage->depth != tile->image->depth); + const avifBool dimsOrDepthIsDifferent = (dstImage->width != dstWidth) || (dstImage->height != dstHeight) || + (dstImage->depth != dstDepth); const avifBool yuvFormatIsDifferent = !alpha && (dstImage->yuvFormat != tile->image->yuvFormat); if (dimsOrDepthIsDifferent || yuvFormatIsDifferent) { if (alpha) { @@ -1545,9 +1579,9 @@ static avifResult avifDecoderDataAllocateGridImagePlanes(avifDecoderData * data, if (dimsOrDepthIsDifferent) { avifImageFreePlanes(dstImage, AVIF_PLANES_ALL); - dstImage->width = grid->outputWidth; - dstImage->height = grid->outputHeight; - dstImage->depth = tile->image->depth; + dstImage->width = dstWidth; + dstImage->height = dstHeight; + dstImage->depth = dstDepth; } if (yuvFormatIsDifferent) { avifImageFreePlanes(dstImage, AVIF_PLANES_YUV); @@ -1571,15 +1605,14 @@ static avifResult avifDecoderDataAllocateGridImagePlanes(avifDecoderData * data, return AVIF_RESULT_OK; } -// After verifying that the relevant properties of the tile match those of the first tile, copies over the pixels from the tile -// into dstImage. +// Copies over the pixels from the tile into dstImage. +// Verifies that the relevant properties of the tile match those of the first tile in case of a grid. static avifResult avifDecoderDataCopyTileToImage(avifDecoderData * data, const avifTileInfo * info, avifImage * dstImage, const avifTile * tile, unsigned int tileIndex) { - const avifImageGrid * grid = &info->grid; const avifTile * firstTile = &data->tiles.tile[info->firstTileIndex]; if (tile != firstTile) { // Check for tile consistency. All tiles in a grid image should match the first tile in the properties checked below. @@ -1593,33 +1626,27 @@ static avifResult avifDecoderDataCopyTileToImage(avifDecoderData * data, } } - unsigned int rowIndex = tileIndex / info->grid.columns; - unsigned int colIndex = tileIndex % info->grid.columns; avifImage srcView; avifImageSetDefaults(&srcView); avifImage dstView; avifImageSetDefaults(&dstView); - avifCropRect dstViewRect = { - firstTile->image->width * colIndex, firstTile->image->height * rowIndex, firstTile->image->width, firstTile->image->height - }; - if (dstViewRect.x + dstViewRect.width > grid->outputWidth) { - dstViewRect.width = grid->outputWidth - dstViewRect.x; - } - if (dstViewRect.y + dstViewRect.height > grid->outputHeight) { - dstViewRect.height = grid->outputHeight - dstViewRect.y; + avifCropRect dstViewRect = { 0, 0, firstTile->image->width, firstTile->image->height }; + if (info->grid.columns > 0 && info->grid.rows > 0) { + unsigned int rowIndex = tileIndex / info->grid.columns; + unsigned int colIndex = tileIndex % info->grid.columns; + dstViewRect.x = firstTile->image->width * colIndex; + dstViewRect.y = firstTile->image->height * rowIndex; + if (dstViewRect.x + dstViewRect.width > info->grid.outputWidth) { + dstViewRect.width = info->grid.outputWidth - dstViewRect.x; + } + if (dstViewRect.y + dstViewRect.height > info->grid.outputHeight) { + dstViewRect.height = info->grid.outputHeight - dstViewRect.y; + } } const avifCropRect srcViewRect = { 0, 0, dstViewRect.width, dstViewRect.height }; - avifImage * dst = dstImage; -#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) - if (tile->input->itemCategory == AVIF_ITEM_GAIN_MAP) { - AVIF_ASSERT_OR_RETURN(dst->gainMap && dst->gainMap->image); - dst = dst->gainMap->image; - } -#endif - AVIF_ASSERT_OR_RETURN(avifImageSetViewRect(&dstView, dst, &dstViewRect) == AVIF_RESULT_OK && + AVIF_ASSERT_OR_RETURN(avifImageSetViewRect(&dstView, dstImage, &dstViewRect) == AVIF_RESULT_OK && avifImageSetViewRect(&srcView, tile->image, &srcViewRect) == AVIF_RESULT_OK); - avifImageCopySamples(&dstView, &srcView, (tile->input->itemCategory == AVIF_ITEM_ALPHA) ? AVIF_PLANES_A : AVIF_PLANES_YUV); - + avifImageCopySamples(&dstView, &srcView, avifIsAlpha(tile->input->itemCategory) ? AVIF_PLANES_A : AVIF_PLANES_YUV); return AVIF_RESULT_OK; } @@ -1988,6 +2015,107 @@ static avifBool avifParseToneMappedImageBox(avifGainMapMetadata * metadata, cons } #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +static avifResult avifParseSampleTransformTokens(avifROStream * s, avifSampleTransformExpression * expression) +{ + uint8_t tokenCount; + AVIF_CHECK(avifROStreamRead(s, &tokenCount, /*size=*/1)); // unsigned int(8) token_count; + AVIF_CHECKERR(avifArrayCreate(expression, sizeof(expression->tokens[0]), tokenCount), AVIF_RESULT_OUT_OF_MEMORY); + + for (uint32_t t = 0; t < tokenCount; ++t) { + avifSampleTransformToken * token = (avifSampleTransformToken *)avifArrayPush(expression); + AVIF_ASSERT_OR_RETURN(token != NULL); + + AVIF_CHECK(avifROStreamRead(s, &token->value, /*size=*/1)); // unsigned int(8) token; + if (token->value == AVIF_SAMPLE_TRANSFORM_CONSTANT) { + // TODO(yguyon): Verify two's complement representation is guaranteed here. + uint32_t constant; + AVIF_CHECK(avifROStreamReadU32(s, &constant)); // signed int(1<<(bit_depth+3)) constant; + token->constant = *(int32_t *)&constant; // maybe =(int32_t)constant; is enough + } else if (token->value == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX) { + AVIF_CHECK(avifROStreamRead(s, &token->inputImageItemIndex, 1)); // unsigned int(8) input_image_item_index; + } + } + AVIF_CHECKERR(avifROStreamRemainingBytes(s) == 0, AVIF_RESULT_BMFF_PARSE_FAILED); + return AVIF_RESULT_OK; +} + +// Parses the raw bitstream of the 'sato' Sample Transform derived image item and extracts the expression. +static avifResult avifParseSampleTransformImageBox(const uint8_t * raw, + size_t rawLen, + uint32_t numInputImageItems, + avifSampleTransformExpression * expression, + avifDiagnostics * diag) +{ + BEGIN_STREAM(s, raw, rawLen, diag, "Box[sato]"); + + uint8_t version, bitDepth; + AVIF_CHECK(avifROStreamReadBits8(&s, &version, /*bitCount=*/6)); // unsigned int(6) version = 0; + AVIF_CHECK(avifROStreamReadBits8(&s, &bitDepth, /*bitCount=*/2)); // unsigned int(2) bit_depth; + AVIF_CHECKERR(version == 0, AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_CHECKERR(bitDepth == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, AVIF_RESULT_NOT_IMPLEMENTED); + + const avifResult result = avifParseSampleTransformTokens(&s, expression); + if (result != AVIF_RESULT_OK) { + avifArrayDestroy(expression); + return result; + } + if (!avifSampleTransformExpressionIsValid(expression, numInputImageItems)) { + avifArrayDestroy(expression); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + return AVIF_RESULT_OK; +} + +static avifResult avifDecoderSampleTransformItemValidateProperties(const avifDecoderItem * item, avifDiagnostics * diag) +{ + const avifProperty * pixiProp = avifPropertyArrayFind(&item->properties, "pixi"); + if (!pixiProp) { + avifDiagnosticsPrintf(diag, "Item ID %u of type '%.4s' is missing mandatory pixi property", item->id, (const char *)item->type); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + for (uint8_t i = 0; i < pixiProp->u.pixi.planeCount; ++i) { + if (pixiProp->u.pixi.planeDepths[i] != pixiProp->u.pixi.planeDepths[0]) { + avifDiagnosticsPrintf(diag, + "Item ID %u of type '%.4s' depth specified by pixi property [%u] is not supported", + item->id, + (const char *)item->type, + pixiProp->u.pixi.planeDepths[i]); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + } + + const avifProperty * ispeProp = avifPropertyArrayFind(&item->properties, "ispe"); + if (!ispeProp) { + avifDiagnosticsPrintf(diag, "Item ID %u of type '%.4s' is missing mandatory ispe property", item->id, (const char *)item->type); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + + for (uint32_t i = 0; i < item->meta->items.count; ++i) { + avifDecoderItem * inputImageItem = item->meta->items.item[i]; + if (inputImageItem->dimgForID != item->id) { + continue; + } + // Even if inputImageItem is a grid, the ispe property from its first tile should have been copied to the grid item. + const avifProperty * inputImageItemIspeProp = avifPropertyArrayFind(&inputImageItem->properties, "ispe"); + AVIF_ASSERT_OR_RETURN(inputImageItemIspeProp != NULL); + if (inputImageItemIspeProp->u.ispe.width != ispeProp->u.ispe.width || + inputImageItemIspeProp->u.ispe.height != ispeProp->u.ispe.height) { + avifDiagnosticsPrintf(diag, + "The fields of the ispe property of item ID %u of type '%.4s' differs from item ID %u", + inputImageItem->id, + (const char *)inputImageItem->type, + item->id); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } + // TODO(yguyon): Check that all input image items share the same codec config (except for the bit depth value). + } + + AVIF_CHECKERR(avifPropertyArrayFind(&item->properties, "clap") == NULL, AVIF_RESULT_NOT_IMPLEMENTED); + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + // Extracts the codecType from the item type or from its children. // Also parses and outputs grid information if the item is a grid. // isItemInInput must be false if the item is a made-up structure @@ -2030,6 +2158,11 @@ static avifResult avifDecoderItemReadAndParse(const avifDecoder * decoder, *codecType = avifGetCodecType(item->type); AVIF_ASSERT_OR_RETURN(*codecType != AVIF_CODEC_TYPE_UNKNOWN); } + // Note: If AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM is defined, backward-incompatible files + // with a primary 'sato' Sample Transform derived image item could be handled here + // (compared to backward-compatible files with a 'sato' item in the same 'altr' group + // as the primary regular color item which are handled in + // avifDecoderDataFindSampleTransformImageItem() below). return AVIF_RESULT_OK; } @@ -2222,12 +2355,19 @@ static avifBool avifParsePixelInformationProperty(avifProperty * prop, const uin avifPixelInformationProperty * pixi = &prop->u.pixi; AVIF_CHECK(avifROStreamRead(&s, &pixi->planeCount, 1)); // unsigned int (8) num_channels; - if (pixi->planeCount > MAX_PIXI_PLANE_DEPTHS) { + if (pixi->planeCount < 1 || pixi->planeCount > MAX_PIXI_PLANE_DEPTHS) { avifDiagnosticsPrintf(diag, "Box[pixi] contains unsupported plane count [%u]", pixi->planeCount); return AVIF_FALSE; } for (uint8_t i = 0; i < pixi->planeCount; ++i) { AVIF_CHECK(avifROStreamRead(&s, &pixi->planeDepths[i], 1)); // unsigned int (8) bits_per_channel; + 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_TRUE; } @@ -4240,6 +4380,13 @@ static avifBool avifTilesCanBeDecodedWithSameCodecInstance(avifDecoderData * dat if (data->tileInfos[c].tileCount > 0) { ++numImageBuffers; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // The sample operations require multiple buffers for compositing so no plane is stolen + // when there is a 'sato' Sample Transform derived image item. + if (c >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && c <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY && data->tileInfos[c].tileCount > 0) { + continue; + } +#endif if (data->tileInfos[c].tileCount == 1) { ++numStolenImageBuffers; } @@ -4609,12 +4756,7 @@ static avifResult avifDecoderFindGainMapItem(const avifDecoder * decoder, const avifProperty * pixiProp = avifPropertyArrayFind(&toneMappedImageItemTmp->properties, "pixi"); if (pixiProp) { - if (pixiProp->u.pixi.planeCount == 0) { - avifDiagnosticsPrintf(data->diag, "Box[pixi] of tmap item contains unsupported plane count [%u]", pixiProp->u.pixi.planeCount); - return AVIF_RESULT_BMFF_PARSE_FAILED; - } gainMap->altPlaneCount = pixiProp->u.pixi.planeCount; - // Assume all planes have the same depth. gainMap->altDepth = pixiProp->u.pixi.planeDepths[0]; } } @@ -4644,6 +4786,31 @@ static avifResult avifDecoderFindGainMapItem(const avifDecoder * decoder, } #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +// Finds a 'sato' Sample Transform derived image item box. +// If found, fills 'sampleTransformItem'. Otherwise, sets 'sampleTransformItem' to NULL. +// Returns AVIF_RESULT_OK on success (whether or not a 'sato' box was found). +// Assumes that there is a single 'sato' item. +// Assumes that the 'sato' item is not the primary item and that both the primary item and 'sato' +// are in the same 'altr' group. +// TODO(yguyon): Check instead of assuming. +static avifResult avifDecoderDataFindSampleTransformImageItem(avifDecoderData * data, avifDecoderItem ** sampleTransformItem) +{ + for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) { + avifDecoderItem * item = data->meta->items.item[itemIndex]; + if (!item->size || item->hasUnsupportedEssentialProperty || item->thumbnailForID != 0) { + continue; + } + if (!memcmp(item->type, "sato", 4)) { + *sampleTransformItem = item; + return AVIF_RESULT_OK; + } + } + *sampleTransformItem = NULL; + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + static avifResult avifDecoderGenerateImageTiles(avifDecoder * decoder, avifTileInfo * info, avifDecoderItem * item, avifItemCategory itemCategory) { const uint32_t previousTileCount = decoder->data->tiles.count; @@ -4943,6 +5110,92 @@ avifResult avifDecoderReset(avifDecoder * decoder) } #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // AVIF_ITEM_SAMPLE_TRANSFORM (not used through mainItems because not a coded item (well grids neither but it's different)). + avifDecoderItem * sampleTransformItem = NULL; + AVIF_CHECKRES(avifDecoderDataFindSampleTransformImageItem(data, &sampleTransformItem)); + if (sampleTransformItem != NULL) { + AVIF_ASSERT_OR_RETURN(data->sampleTransformNumInputImageItems == 0); + uint32_t numExtraInputImageItems = 0; + for (uint32_t i = 0; i < data->meta->items.count; ++i) { + avifDecoderItem * inputImageItem = data->meta->items.item[i]; + if (inputImageItem->dimgForID != sampleTransformItem->id) { + continue; + } + if (avifDecoderItemShouldBeSkipped(inputImageItem)) { + avifDiagnosticsPrintf(data->diag, "Box[sato] input item %d is not a supported image type", inputImageItem->id); + return AVIF_RESULT_DECODE_SAMPLE_TRANSFORM_FAILED; + } + // Input image item order is important because input image items are indexed according to this order. + AVIF_CHECKERR(inputImageItem->dimgIdx == data->sampleTransformNumInputImageItems, AVIF_RESULT_NOT_IMPLEMENTED); + + avifItemCategory * category = &data->sampleTransformInputImageItems[data->sampleTransformNumInputImageItems]; + avifBool foundItem = AVIF_FALSE; + uint32_t alphaCategory = AVIF_ITEM_CATEGORY_COUNT; + for (int c = AVIF_ITEM_COLOR; c < AVIF_ITEM_CATEGORY_COUNT; ++c) { + if (mainItems[c] && inputImageItem->id == mainItems[c]->id) { + *category = c; + AVIF_CHECKERR(*category == AVIF_ITEM_COLOR, AVIF_RESULT_NOT_IMPLEMENTED); + alphaCategory = AVIF_ITEM_ALPHA; + foundItem = AVIF_TRUE; + break; + } + } + if (!foundItem) { + AVIF_CHECKERR(numExtraInputImageItems < AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS, + AVIF_RESULT_NOT_IMPLEMENTED); + *category = AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR + numExtraInputImageItems; + alphaCategory = AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA + numExtraInputImageItems; + mainItems[*category] = inputImageItem; + ++numExtraInputImageItems; + + AVIF_CHECKRES(avifDecoderItemReadAndParse(decoder, + inputImageItem, + /*isItemInInput=*/AVIF_TRUE, + &data->tileInfos[*category].grid, + &codecType[*category])); + + // Optional alpha auxiliary item + avifBool isAlphaInputImageItemInInput = AVIF_FALSE; + AVIF_CHECKRES(avifMetaFindAlphaItem(data->meta, + mainItems[*category], + &data->tileInfos[*category], + &mainItems[alphaCategory], + &data->tileInfos[alphaCategory], + &isAlphaInputImageItemInInput)); + + AVIF_CHECKERR(!mainItems[alphaCategory] == !mainItems[AVIF_ITEM_ALPHA], AVIF_RESULT_NOT_IMPLEMENTED); + if (mainItems[alphaCategory] != NULL) { + AVIF_CHECKERR(isAlphaInputImageItemInInput == isAlphaItemInInput, AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_CHECKERR((mainItems[*category]->premByID == mainItems[alphaCategory]->id) == + (mainItems[AVIF_ITEM_COLOR]->premByID == mainItems[AVIF_ITEM_ALPHA]->id), + AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_CHECKRES(avifDecoderItemReadAndParse(decoder, + mainItems[alphaCategory], + isAlphaInputImageItemInInput, + &data->tileInfos[alphaCategory].grid, + &codecType[alphaCategory])); + } + } + + ++data->sampleTransformNumInputImageItems; + } + + AVIF_ASSERT_OR_RETURN(data->meta->sampleTransformExpression.tokens == NULL); + avifROData satoData; + AVIF_CHECKRES(avifDecoderItemRead(sampleTransformItem, decoder->io, &satoData, 0, 0, data->diag)); + AVIF_CHECKRES(avifParseSampleTransformImageBox(satoData.data, + satoData.size, + data->sampleTransformNumInputImageItems, + &data->meta->sampleTransformExpression, + data->diag)); + AVIF_CHECKRES(avifDecoderSampleTransformItemValidateProperties(sampleTransformItem, data->diag)); + const avifProperty * pixiProp = avifPropertyArrayFind(&sampleTransformItem->properties, "pixi"); + AVIF_ASSERT_OR_RETURN(pixiProp != NULL); + data->meta->sampleTransformDepth = pixiProp->u.pixi.planeDepths[0]; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP + // Find Exif and/or XMP metadata, if any AVIF_CHECKRES(avifDecoderFindMetadata(decoder, data->meta, decoder->image, mainItems[AVIF_ITEM_COLOR]->id)); @@ -4968,7 +5221,7 @@ avifResult avifDecoderReset(avifDecoder * decoder) continue; } #endif - if (c == AVIF_ITEM_ALPHA && !mainItems[c]->width && !mainItems[c]->height) { + if (avifIsAlpha(c) && !mainItems[c]->width && !mainItems[c]->height) { // NON-STANDARD: Alpha subimage does not have an ispe property; adopt width/height from color item AVIF_ASSERT_OR_RETURN(!(decoder->strictFlags & AVIF_STRICT_ALPHA_ISPE_REQUIRED)); mainItems[c]->width = mainItems[AVIF_ITEM_COLOR]->width; @@ -4978,7 +5231,7 @@ avifResult avifDecoderReset(avifDecoder * decoder) AVIF_CHECKRES(avifDecoderGenerateImageTiles(decoder, &data->tileInfos[c], mainItems[c], c)); avifStrictFlags strictFlags = decoder->strictFlags; - if (c == AVIF_ITEM_ALPHA && !isAlphaItemInInput) { + if (avifIsAlpha(c) && !isAlphaItemInInput) { // In this case, the made up grid item will not have an associated pixi property. So validate everything else // but the pixi property. strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; @@ -4989,7 +5242,7 @@ avifResult avifDecoderReset(avifDecoder * decoder) if (mainItems[AVIF_ITEM_COLOR]->progressive) { decoder->progressiveState = AVIF_PROGRESSIVE_STATE_AVAILABLE; - // data->color.firstTileIndex is not yet defined but will be set to 0 a few lines below. + // data->tileInfos[AVIF_ITEM_COLOR].firstTileIndex is not yet defined but will be set to 0 a few lines below. const avifTile * colorTile = &data->tiles.tile[0]; if (colorTile->input->samples.count > 1) { decoder->progressiveState = AVIF_PROGRESSIVE_STATE_ACTIVE; @@ -5183,6 +5436,11 @@ static avifResult avifGetErrorForItemCategory(avifItemCategory itemCategory) if (itemCategory == AVIF_ITEM_GAIN_MAP) { return AVIF_RESULT_DECODE_GAIN_MAP_FAILED; } +#endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY) { + return AVIF_RESULT_DECODE_SAMPLE_TRANSFORM_FAILED; + } #endif return (itemCategory == AVIF_ITEM_ALPHA) ? AVIF_RESULT_DECODE_ALPHA_FAILED : AVIF_RESULT_DECODE_COLOR_FAILED; } @@ -5201,7 +5459,7 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma } avifBool isLimitedRangeAlpha = AVIF_FALSE; - if (!tile->codec->getNextImage(tile->codec, decoder, sample, tile->input->itemCategory == AVIF_ITEM_ALPHA, &isLimitedRangeAlpha, tile->image)) { + if (!tile->codec->getNextImage(tile->codec, decoder, sample, avifIsAlpha(tile->input->itemCategory), &isLimitedRangeAlpha, tile->image)) { avifDiagnosticsPrintf(&decoder->diag, "tile->codec->getNextImage() failed"); return avifGetErrorForItemCategory(tile->input->itemCategory); } @@ -5229,7 +5487,7 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma // of the specification. However, it was allowed in version 1.0.0 of the // specification. To allow such files, simply convert the alpha plane to // full range. - if ((tile->input->itemCategory == AVIF_ITEM_ALPHA) && isLimitedRangeAlpha) { + if (avifIsAlpha(tile->input->itemCategory) && isLimitedRangeAlpha) { avifResult result = avifImageLimitedToFullAlpha(tile->image); if (result != AVIF_RESULT_OK) { avifDiagnosticsPrintf(&decoder->diag, "avifImageLimitedToFullAlpha failed"); @@ -5251,18 +5509,33 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma ++info->decodedTileCount; - if ((info->grid.rows > 0) && (info->grid.columns > 0)) { - if (tileIndex == 0) { - avifImage * dstImage = decoder->image; + const avifBool isGrid = (info->grid.rows > 0) && (info->grid.columns > 0); + avifBool stealPlanes = !isGrid; +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (decoder->data->meta->sampleTransformExpression.count > 0) { + // Keep everything as a copy for now. + stealPlanes = AVIF_FALSE; + } + if (tile->input->itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && + tile->input->itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY) { + // Keep Sample Transform input image item samples in tiles. + // The expression will be applied in avifDecoderNextImage() below instead, once all the tiles are available. + continue; + } +#endif + + if (!stealPlanes) { + avifImage * dstImage = decoder->image; #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) - if (tile->input->itemCategory == AVIF_ITEM_GAIN_MAP) { - AVIF_ASSERT_OR_RETURN(dstImage->gainMap && dstImage->gainMap->image); - dstImage = dstImage->gainMap->image; - } + if (tile->input->itemCategory == AVIF_ITEM_GAIN_MAP) { + AVIF_ASSERT_OR_RETURN(dstImage->gainMap && dstImage->gainMap->image); + dstImage = dstImage->gainMap->image; + } #endif - AVIF_CHECKRES(avifDecoderDataAllocateGridImagePlanes(decoder->data, info, dstImage)); + if (tileIndex == 0) { + AVIF_CHECKRES(avifDecoderDataAllocateImagePlanes(decoder->data, info, dstImage)); } - AVIF_CHECKRES(avifDecoderDataCopyTileToImage(decoder->data, info, decoder->image, tile, tileIndex)); + AVIF_CHECKRES(avifDecoderDataCopyTileToImage(decoder->data, info, dstImage, tile, tileIndex)); } else { // Non-grid path. Just steal the planes from the only "tile". AVIF_ASSERT_OR_RETURN(info->tileCount == 1); @@ -5281,7 +5554,7 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma default: if ((decoder->image->width != src->width) || (decoder->image->height != src->height) || (decoder->image->depth != src->depth)) { - if (tile->input->itemCategory == AVIF_ITEM_ALPHA) { + if (avifIsAlpha(tile->input->itemCategory)) { avifDiagnosticsPrintf(&decoder->diag, "The color image item does not match the alpha image item in width, height, or bit depth"); return AVIF_RESULT_DECODE_ALPHA_FAILED; @@ -5295,7 +5568,7 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma break; } - if (tile->input->itemCategory == AVIF_ITEM_ALPHA) { + if (avifIsAlpha(tile->input->itemCategory)) { avifImageStealPlanes(decoder->image, src, AVIF_PLANES_A); #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) } else if (tile->input->itemCategory == AVIF_ITEM_GAIN_MAP) { @@ -5321,6 +5594,62 @@ static avifBool avifDecoderDataFrameFullyDecoded(const avifDecoderData * data) return AVIF_TRUE; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +avifResult avifDecoderApplySampleTransform(const avifDecoder * decoder, avifImage * dstImage) +{ + if (dstImage->depth != decoder->data->meta->sampleTransformDepth) { + AVIF_ASSERT_OR_RETURN(dstImage->yuvPlanes[0] != NULL); + AVIF_ASSERT_OR_RETURN(dstImage->imageOwnsYUVPlanes); + + // Use a temporary buffer because dstImage may point to decoder->image, which could be an input image. + avifImage * dstImageWithCorrectDepth = + avifImageCreate(dstImage->width, dstImage->height, decoder->data->meta->sampleTransformDepth, dstImage->yuvFormat); + AVIF_CHECKERR(dstImageWithCorrectDepth != NULL, AVIF_RESULT_OUT_OF_MEMORY); + avifResult result = + avifImageAllocatePlanes(dstImageWithCorrectDepth, dstImage->alphaPlane != NULL ? AVIF_PLANES_ALL : AVIF_PLANES_YUV); + if (result == AVIF_RESULT_OK) { + result = avifDecoderApplySampleTransform(decoder, dstImageWithCorrectDepth); + if (result == AVIF_RESULT_OK) { + // Keep the same dstImage object rather than swapping decoder->image, in case the user already accessed it. + avifImageFreePlanes(dstImage, AVIF_PLANES_ALL); + dstImage->depth = dstImageWithCorrectDepth->depth; + avifImageStealPlanes(dstImage, dstImageWithCorrectDepth, AVIF_PLANES_ALL); + } + } + avifImageDestroy(dstImageWithCorrectDepth); + return result; + } + + for (avifBool alpha = AVIF_FALSE; alpha <= decoder->alphaPresent ? AVIF_TRUE : AVIF_FALSE; ++alpha) { + const avifImage * inputImages[AVIF_SAMPLE_TRANSFORM_MAX_NUM_INPUT_IMAGE_ITEMS]; + for (uint32_t i = 0; i < decoder->data->sampleTransformNumInputImageItems; ++i) { + avifItemCategory category = decoder->data->sampleTransformInputImageItems[i]; + if (category == AVIF_ITEM_COLOR) { + inputImages[i] = decoder->image; + } else { + AVIF_ASSERT_OR_RETURN(category >= AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR && + category < AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR + + AVIF_SAMPLE_TRANSFORM_MAX_NUM_EXTRA_INPUT_IMAGE_ITEMS); + if (alpha) { + category += AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA - AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR; + } + const avifTileInfo * tileInfo = &decoder->data->tileInfos[category]; + AVIF_CHECKERR(tileInfo->tileCount == 1, AVIF_RESULT_NOT_IMPLEMENTED); // TODO(yguyon): Implement Sample Transform grids + inputImages[i] = decoder->data->tiles.tile[tileInfo->firstTileIndex].image; + AVIF_ASSERT_OR_RETURN(inputImages[i] != NULL); + } + } + AVIF_CHECKRES(avifImageApplyExpression(dstImage, + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, + &decoder->data->meta->sampleTransformExpression, + decoder->data->sampleTransformNumInputImageItems, + inputImages, + alpha ? AVIF_PLANES_A : AVIF_PLANES_YUV)); + } + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + avifResult avifDecoderNextImage(avifDecoder * decoder) { avifDiagnosticsClearError(&decoder->diag); @@ -5389,6 +5718,13 @@ avifResult avifDecoderNextImage(avifDecoder * decoder) AVIF_ASSERT_OR_RETURN(prepareTileResult[c] == AVIF_RESULT_OK); } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (decoder->data->meta->sampleTransformExpression.count > 0) { + // TODO(yguyon): Add a field in avifDecoder and only perform sample transformations upon request. + AVIF_CHECKRES(avifDecoderApplySampleTransform(decoder, decoder->image)); + } +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + // Only advance decoder->imageIndex once the image is completely decoded, so that // avifDecoderNthImage(decoder, decoder->imageIndex + 1) is equivalent to avifDecoderNextImage(decoder) // if the previous call to avifDecoderNextImage() returned AVIF_RESULT_WAITING_ON_IO. @@ -5540,6 +5876,13 @@ static uint32_t avifGetDecodedRowCount(const avifDecoder * decoder, const avifTi return 0; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (decoder->data->meta->sampleTransformExpression.count > 0) { + // TODO(yguyon): Support incremental Sample Transforms + return 0; + } +#endif + if ((info->grid.rows > 0) && (info->grid.columns > 0)) { // Grid of AVIF tiles (not to be confused with AV1 tiles). const uint32_t tileHeight = decoder->data->tiles.tile[info->firstTileIndex].height; @@ -5589,6 +5932,8 @@ avifResult avifDecoderRead(avifDecoder * decoder, avifImage * image) if (result != AVIF_RESULT_OK) { return result; } + // If decoder->image owns its planes, their ownership could be transferred here instead of copied, + // unless the user reuses the decoder instance, which is unknown yet. return avifImageCopy(image, decoder->image, AVIF_PLANES_ALL); } diff --git a/src/sampletransform.c b/src/sampletransform.c new file mode 100644 index 0000000000..fff047fd7e --- /dev/null +++ b/src/sampletransform.c @@ -0,0 +1,353 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/internal.h" + +#include +#include +#include + +//------------------------------------------------------------------------------ +// Convenience functions + +avifBool avifSampleTransformExpressionIsValid(const avifSampleTransformExpression * tokens, uint32_t numInputImageItems) +{ + uint32_t stackSize = 0; + for (uint32_t t = 0; t < tokens->count; ++t) { + const avifSampleTransformToken * token = &tokens->tokens[t]; + if (token->value >= AVIF_SAMPLE_TRANSFORM_RESERVED) { + return AVIF_FALSE; + } + if (token->value == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX && + (token->inputImageItemIndex == 0 || token->inputImageItemIndex > numInputImageItems)) { + // inputImageItemIndex is 1-based. + return AVIF_FALSE; + } + if (token->value == AVIF_SAMPLE_TRANSFORM_CONSTANT || token->value == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX) { + ++stackSize; + } else { + if (stackSize < 2) { + return AVIF_FALSE; // All operators use two input operands. + } + --stackSize; // Pop two and push one. + } + } + return stackSize == 1; +} + +avifBool avifSampleTransformExpressionIsEquivalentTo(const avifSampleTransformExpression * a, const avifSampleTransformExpression * b) +{ + if (a->count != b->count) { + return AVIF_FALSE; + } + + for (uint32_t t = 0; t < a->count; ++t) { + const avifSampleTransformToken * aToken = &a->tokens[t]; + const avifSampleTransformToken * bToken = &b->tokens[t]; + if (aToken->value != bToken->value || (aToken->value == AVIF_SAMPLE_TRANSFORM_CONSTANT && aToken->constant != bToken->constant)) { + return AVIF_FALSE; + } + // For AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, no need to compare inputImageItemIndex + // because these are variables in the expression. + } + return AVIF_TRUE; +} + +//------------------------------------------------------------------------------ +// Recipe to expression + +static avifBool avifPushConstant(avifSampleTransformExpression * expression, int32_t constant) +{ + avifSampleTransformToken * token = (avifSampleTransformToken *)avifArrayPush(expression); + if (token == NULL) { + return AVIF_FALSE; + } + token->value = AVIF_SAMPLE_TRANSFORM_CONSTANT; + token->constant = constant; + return AVIF_TRUE; +} +static avifBool avifPushInputImageItem(avifSampleTransformExpression * expression, uint8_t inputImageItemIndex) +{ + avifSampleTransformToken * token = (avifSampleTransformToken *)avifArrayPush(expression); + if (token == NULL) { + return AVIF_FALSE; + } + token->value = AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX; + token->inputImageItemIndex = inputImageItemIndex; + return AVIF_TRUE; +} +static avifBool avifPushOperator(avifSampleTransformExpression * expression, avifSampleTransformTokenType operator) +{ + avifSampleTransformToken * token = (avifSampleTransformToken *)avifArrayPush(expression); + if (token == NULL) { + return AVIF_FALSE; + } + token->value = (uint8_t) operator; + return AVIF_TRUE; +} + +avifResult avifSampleTransformRecipeToExpression(avifSampleTransformRecipe recipe, avifSampleTransformExpression * expression) +{ + // Postfix (or Reverse Polish) notation. Brackets to highlight sub-expressions. + + if (recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B) { + // reference_count is two: two 8-bit input images. + // (base_sample << 8) | hidden_sample + // Note: base_sample is encoded losslessly. hidden_sample is encoded lossily or losslessly. + AVIF_CHECKERR(avifArrayCreate(expression, sizeof(avifSampleTransformToken), 5), AVIF_RESULT_OUT_OF_MEMORY); + + { + // The base image represents the 8 most significant bits of the reconstructed, bit-depth-extended output image. + // Left shift the base image (which is also the primary item, or the auxiliary alpha item of the primary item) + // by 8 bits. This is equivalent to multiplying by 2^8. + AVIF_ASSERT_OR_RETURN(avifPushConstant(expression, 256)); + AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 1)); + AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_PRODUCT)); + } + { + // The second image represents the 8 least significant bits of the reconstructed, bit-depth-extended output image. + AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 2)); + } + AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_OR)); + return AVIF_RESULT_OK; + } + + if (recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) { + // reference_count is two: one 12-bit input image and one 8-bit input image (because AV1 does not support 4-bit samples). + // (base_sample << 4) | (hidden_sample >> 4) + // Note: base_sample is encoded losslessly. hidden_sample is encoded lossily or losslessly. + AVIF_CHECKERR(avifArrayCreate(expression, sizeof(avifSampleTransformToken), 5), AVIF_RESULT_OUT_OF_MEMORY); + + { + // The base image represents the 12 most significant bits of the reconstructed, bit-depth-extended output image. + // Left shift the base image (which is also the primary item, or the auxiliary alpha item of the primary item) + // by 4 bits. This is equivalent to multiplying by 2^4. + AVIF_ASSERT_OR_RETURN(avifPushConstant(expression, 16)); + AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 1)); + AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_PRODUCT)); + } + { + // The second image represents the 4 least significant bits of the reconstructed, bit-depth-extended output image. + AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 2)); + AVIF_ASSERT_OR_RETURN(avifPushConstant(expression, 16)); + AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_DIVIDE)); + } + AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_SUM)); + return AVIF_RESULT_OK; + } + + return AVIF_RESULT_INVALID_ARGUMENT; +} + +avifResult avifSampleTransformExpressionToRecipe(const avifSampleTransformExpression * expression, avifSampleTransformRecipe * recipe) +{ + *recipe = AVIF_SAMPLE_TRANSFORM_NONE; + const avifSampleTransformRecipe kAllRecipes[] = { AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B, + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B }; + for (size_t i = 0; i < sizeof(kAllRecipes) / sizeof(kAllRecipes[0]); ++i) { + avifSampleTransformRecipe candidateRecipe = kAllRecipes[i]; + avifSampleTransformExpression candidateExpression = { 0 }; + AVIF_CHECKRES(avifSampleTransformRecipeToExpression(candidateRecipe, &candidateExpression)); + const avifBool equivalence = avifSampleTransformExpressionIsEquivalentTo(expression, &candidateExpression); + avifArrayDestroy(&candidateExpression); + if (equivalence) { + *recipe = candidateRecipe; + return AVIF_RESULT_OK; + } + } + return AVIF_RESULT_OK; +} + +//------------------------------------------------------------------------------ +// Operators + +static int32_t avifSampleTransformClamp32b(int64_t value) +{ + return value <= INT32_MIN ? INT32_MIN : value >= INT32_MAX ? INT32_MAX : (int32_t)value; +} + +static int32_t avifSampleTransformOperation32b(int32_t leftOperand, int32_t rightOperand, uint8_t operator) +{ + switch (operator) { + case AVIF_SAMPLE_TRANSFORM_SUM: + return avifSampleTransformClamp32b(leftOperand + rightOperand); + case AVIF_SAMPLE_TRANSFORM_DIFFERENCE: + return avifSampleTransformClamp32b(leftOperand - rightOperand); + case AVIF_SAMPLE_TRANSFORM_PRODUCT: + return avifSampleTransformClamp32b(leftOperand * rightOperand); + case AVIF_SAMPLE_TRANSFORM_DIVIDE: + return rightOperand == 0 ? leftOperand : leftOperand / rightOperand; + case AVIF_SAMPLE_TRANSFORM_AND: + return leftOperand & rightOperand; + case AVIF_SAMPLE_TRANSFORM_OR: + return leftOperand | rightOperand; + case AVIF_SAMPLE_TRANSFORM_XOR: + return leftOperand ^ rightOperand; + case AVIF_SAMPLE_TRANSFORM_NOR: + return ~(leftOperand | rightOperand); + case AVIF_SAMPLE_TRANSFORM_MSB: { + if (leftOperand <= 0) { + return rightOperand; + } + int32_t log2 = 0; + leftOperand >>= 1; + for (; leftOperand != 0; ++log2) { + leftOperand >>= 1; + } + return log2; + } + case AVIF_SAMPLE_TRANSFORM_POW: { + if (leftOperand == 0 || leftOperand == 1) { + return leftOperand; + } + const uint32_t exponent = rightOperand > 0 ? (uint32_t)rightOperand : (uint32_t) - (int64_t)rightOperand; + if (exponent == 0) { + return 1; + } + if (exponent == 1) { + return leftOperand; + } + if (leftOperand == -1) { + return (exponent % 2 == 0) ? 1 : -1; + } + + int64_t result = leftOperand; + for (uint32_t i = 1; i < exponent; ++i) { + result *= leftOperand; + if (result <= INT32_MIN) { + return INT32_MIN; + } else if (result >= INT32_MAX) { + return INT32_MAX; + } + } + return (int32_t)result; + } + case AVIF_SAMPLE_TRANSFORM_MIN: + return leftOperand <= rightOperand ? leftOperand : rightOperand; + case AVIF_SAMPLE_TRANSFORM_MAX: + return leftOperand <= rightOperand ? rightOperand : leftOperand; + default: + assert(AVIF_FALSE); + } + return 0; +} + +//------------------------------------------------------------------------------ +// Expression + +AVIF_ARRAY_DECLARE(avifSampleTransformStack32b, int32_t, elements); + +static avifResult avifImageApplyExpression32b(avifImage * dstImage, + const avifSampleTransformExpression * expression, + const avifImage * inputImageItems[], + avifPlanesFlags planes, + int32_t * stack, + uint32_t stackCapacity) +{ + // This slow path could be avoided by recognizing the recipe thanks to avifSampleTransformExpressionToRecipe() + // and having a dedicated optimized implementation for each recipe. + + const int32_t minValue = 0; + const int32_t maxValue = (1 << dstImage->depth) - 1; + + const avifBool skipColor = !(planes & AVIF_PLANES_YUV); + const avifBool skipAlpha = !(planes & AVIF_PLANES_A); + for (int c = AVIF_CHAN_Y; c <= AVIF_CHAN_A; ++c) { + const avifBool alpha = c == AVIF_CHAN_A; + if ((skipColor && !alpha) || (skipAlpha && alpha)) { + continue; + } + + const uint32_t planeWidth = avifImagePlaneWidth(dstImage, c); + const uint32_t planeHeight = avifImagePlaneHeight(dstImage, c); + for (uint32_t y = 0; y < planeHeight; ++y) { + for (uint32_t x = 0; x < planeWidth; ++x) { + uint32_t stackSize = 0; + for (uint32_t t = 0; t < expression->count; ++t) { + const avifSampleTransformToken * token = &expression->tokens[t]; + if (token->value == AVIF_SAMPLE_TRANSFORM_CONSTANT) { + AVIF_ASSERT_OR_RETURN(stackSize < stackCapacity); + stack[stackSize++] = token->constant; + } else if (token->value == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX) { + const avifImage * image = inputImageItems[token->inputImageItemIndex - 1]; // 1-based + const uint8_t * row = avifImagePlane(image, c) + avifImagePlaneRowBytes(image, c) * y; + AVIF_ASSERT_OR_RETURN(stackSize < stackCapacity); + stack[stackSize++] = avifImageUsesU16(image) ? ((const uint16_t *)row)[x] : row[x]; + } else { + AVIF_ASSERT_OR_RETURN(stackSize >= 2); + stack[stackSize - 2] = avifSampleTransformOperation32b(stack[stackSize - 2], stack[stackSize - 1], token->value); + stackSize--; // Pop two and push one. + } + } + AVIF_ASSERT_OR_RETURN(stackSize == 1); + // Fit to 'pixi'-defined range. TODO(yguyon): Take avifRange into account. + stack[0] = stack[0] <= minValue ? minValue : stack[0] >= maxValue ? maxValue : stack[0]; + + uint8_t * row = avifImagePlane(dstImage, c) + avifImagePlaneRowBytes(dstImage, c) * y; + if (avifImageUsesU16(dstImage)) { + ((uint16_t *)row)[x] = (uint16_t)stack[0]; + } else { + row[x] = (uint8_t)stack[0]; + } + } + } + } + return AVIF_RESULT_OK; +} + +avifResult avifImageApplyExpression(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + const avifSampleTransformExpression * expression, + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes) +{ + // Check that the expression is valid. + AVIF_ASSERT_OR_RETURN(avifSampleTransformExpressionIsValid(expression, numInputImageItems)); + const avifBool skipColor = !(planes & AVIF_PLANES_YUV); + const avifBool skipAlpha = !(planes & AVIF_PLANES_A); + for (int c = AVIF_CHAN_Y; c <= AVIF_CHAN_A; ++c) { + const avifBool alpha = c == AVIF_CHAN_A; + if ((skipColor && !alpha) || (skipAlpha && alpha)) { + continue; + } + + const uint32_t planeWidth = avifImagePlaneWidth(dstImage, c); + const uint32_t planeHeight = avifImagePlaneHeight(dstImage, c); + for (uint32_t i = 0; i < numInputImageItems; ++i) { + AVIF_CHECKERR(avifImagePlaneWidth(inputImageItems[i], c) == planeWidth, AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(avifImagePlaneHeight(inputImageItems[i], c) == planeHeight, AVIF_RESULT_BMFF_PARSE_FAILED); + } + } + + // Then apply it. This part should not fail except for memory shortage reasons. + if (bitDepth == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32) { + uint32_t stackCapacity = expression->count / 2 + 1; + int32_t * stack = avifAlloc(stackCapacity * sizeof(int32_t)); + AVIF_CHECKERR(stack != NULL, AVIF_RESULT_OUT_OF_MEMORY); + const avifResult result = avifImageApplyExpression32b(dstImage, expression, inputImageItems, planes, stack, stackCapacity); + avifFree(stack); + return result; + } + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifImageApplyOperations(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + uint32_t numTokens, + const avifSampleTransformToken tokens[], + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes) +{ + avifSampleTransformExpression expression = { 0 }; + AVIF_CHECKERR(avifArrayCreate(&expression, sizeof(avifSampleTransformToken), numTokens), AVIF_RESULT_OUT_OF_MEMORY); + for (uint32_t t = 0; t < numTokens; ++t) { + avifSampleTransformToken * token = (avifSampleTransformToken *)avifArrayPush(&expression); + AVIF_ASSERT_OR_RETURN(token != NULL); + *token = tokens[t]; + } + const avifResult result = avifImageApplyExpression(dstImage, bitDepth, &expression, numInputImageItems, inputImageItems, planes); + avifArrayDestroy(&expression); + return result; +} diff --git a/src/write.c b/src/write.c index 9cbe1f8d0b..fb2e49b8d8 100644 --- a/src/write.c +++ b/src/write.c @@ -485,6 +485,9 @@ avifEncoder * avifEncoderCreate(void) return NULL; } encoder->headerFormat = AVIF_HEADER_FULL; +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + encoder->sampleTransformRecipe = AVIF_SAMPLE_TRANSFORM_NONE; +#endif return encoder; } @@ -527,6 +530,9 @@ static void avifEncoderBackupSettings(avifEncoder * encoder) encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2; encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2; lastEncoder->scalingMode = encoder->scalingMode; +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + lastEncoder->sampleTransformRecipe = encoder->sampleTransformRecipe; +#endif } // This function detects changes made on avifEncoder. It returns true on success (i.e., if every @@ -580,6 +586,12 @@ static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncode *encoderChanges |= AVIF_ENCODER_CHANGE_CODEC_SPECIFIC; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (lastEncoder->sampleTransformRecipe != encoder->sampleTransformRecipe) { + return AVIF_FALSE; + } +#endif + return AVIF_TRUE; } @@ -983,6 +995,50 @@ static avifResult avifImageCopyAltImageMetadata(avifImage * altImageMetadata, co } #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +static avifResult avifEncoderWriteSampleTransformTokens(avifRWStream * s, const avifSampleTransformExpression * expression) +{ + AVIF_ASSERT_OR_RETURN(expression->count <= 256); + AVIF_CHECKRES(avifRWStreamWriteU8(s, (uint8_t)expression->count)); // unsigned int(8) token_count; + + for (uint32_t t = 0; t < expression->count; ++t) { + const avifSampleTransformToken * token = &expression->tokens[t]; + AVIF_CHECKRES(avifRWStreamWriteU8(s, token->value)); // unsigned int(8) token; + + if (token->value == AVIF_SAMPLE_TRANSFORM_CONSTANT) { + // TODO(yguyon): Verify two's complement representation is guaranteed here. + const uint32_t constant = *(uint32_t *)&token->constant; + AVIF_CHECKRES(avifRWStreamWriteU32(s, constant)); // signed int(1<<(bit_depth+3)) constant; + } else if (token->value == AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX) { + AVIF_CHECKRES(avifRWStreamWriteU8(s, token->inputImageItemIndex)); // unsigned int(8) input_image_item_index; + } + } + return AVIF_RESULT_OK; +} + +static avifResult avifEncoderWriteSampleTransformPayload(avifEncoder * encoder, avifRWData * data) +{ + avifRWStream s; + avifRWStreamStart(&s, data); + AVIF_CHECKRES(avifRWStreamWriteBits(&s, 0, /*bitCount=*/6)); // unsigned int(6) version = 0; + // AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32 is necessary because the two input images + // once combined use 16-bit unsigned values, but intermediate results are stored in signed integers. + AVIF_CHECKRES(avifRWStreamWriteBits(&s, AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, /*bitCount=*/2)); // unsigned int(2) bit_depth; + + avifSampleTransformExpression expression = { 0 }; + AVIF_CHECKRES(avifSampleTransformRecipeToExpression(encoder->sampleTransformRecipe, &expression)); + const avifResult result = avifEncoderWriteSampleTransformTokens(&s, &expression); + avifArrayDestroy(&expression); + if (result != AVIF_RESULT_OK) { + avifDiagnosticsPrintf(&encoder->diag, "Failed to write sample transform metadata for recipe %d", encoder->sampleTransformRecipe); + return result; + } + + avifRWStreamFinishWrite(&s); + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif) { size_t exifTiffHeaderOffset; @@ -1101,6 +1157,9 @@ static const char infeNameAlpha[] = "Alpha"; #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) static const char infeNameGainMap[] = "GMap"; #endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +static const char infeNameSampleTransform[] = "SampleTransform"; +#endif // Adds the items for a single cell or a grid of cells. Outputs the topLevelItemID which is // the only item if there is exactly one cell, or the grid item for multiple cells. @@ -1119,7 +1178,11 @@ static avifResult avifEncoderAddImageItems(avifEncoder * encoder, #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) : (itemCategory == AVIF_ITEM_GAIN_MAP) ? infeNameGainMap #endif - : infeNameColor; +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + : (itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY) + ? infeNameSampleTransform +#endif + : infeNameColor; const size_t infeNameSize = strlen(infeName) + 1; if (cellCount > 1) { @@ -1153,6 +1216,185 @@ static avifResult avifEncoderAddImageItems(avifEncoder * encoder, return AVIF_RESULT_OK; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) +static avifResult avifEncoderCreateBitDepthExtensionItems(avifEncoder * encoder, + uint32_t gridCols, + uint32_t gridRows, + uint32_t gridWidth, + uint32_t gridHeight, + uint16_t colorItemID) +{ + AVIF_ASSERT_OR_RETURN(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || + encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B); + + // There are multiple possible ISOBMFF box hierarchies for translucent images, + // using 'sato' (Sample Transform) derived image items: + // - a primary 'sato' item uses a main color coded item and a hidden color coded item; each color coded + // item has an auxiliary alpha coded item; the main color coded item and the 'sato' item are in + // an 'altr' group (backward-compatible, implemented) + // - a primary 'sato' item uses a main color coded item and a hidden color coded item; the primary + // 'sato' item has an auxiliary alpha 'sato' item using two alpha coded items (backward-incompatible) + // Likewise, there are multiple possible ISOBMFF box hierarchies for bit-depth-extended grids, + // using 'sato' (Sample Transform) derived image items: + // - a primary color 'grid', an auxiliary alpha 'grid', a hidden color 'grid', a hidden auxiliary alpha 'grid' + // and a 'sato' using the two color 'grid's as input items in this order; the primary color item + // and the 'sato' item being in an 'altr' group (backward-compatible, implemented) + // - a primary 'grid' of 'sato' cells and an auxiliary alpha 'grid' of 'sato' cells (backward-incompatible) + avifEncoderItem * sampleTransformItem = avifEncoderDataCreateItem(encoder->data, + "sato", + infeNameSampleTransform, + /*infeNameSize=*/strlen(infeNameSampleTransform) + 1, + /*cellIndex=*/0); + AVIF_CHECKRES(avifEncoderWriteSampleTransformPayload(encoder, &sampleTransformItem->metadataPayload)); + sampleTransformItem->itemCategory = AVIF_ITEM_SAMPLE_TRANSFORM; + uint16_t sampleTransformItemID = sampleTransformItem->id; + // 'altr' group + uint16_t * alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); + AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); + *alternativeItemID = sampleTransformItem->id; + alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); + AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); + *alternativeItemID = colorItemID; + + uint16_t bitDepthExtensionColorItemId; + AVIF_CHECKRES( + avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR, &bitDepthExtensionColorItemId)); + avifEncoderItem * bitDepthExtensionColorItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionColorItemId); + assert(bitDepthExtensionColorItem); + bitDepthExtensionColorItem->hiddenImage = AVIF_TRUE; + + // Set the color and bit depth extension items' dimgFromID value to point to the sample transform item. + // The color item shall be first, and the bit depth extension item second. avifEncoderFinish() writes the + // dimg item references in item id order, so as long as colorItemID < bitDepthExtensionColorItemId, the order + // will be correct. + AVIF_ASSERT_OR_RETURN(colorItemID < bitDepthExtensionColorItemId); + avifEncoderItem * colorItem = avifEncoderDataFindItemByID(encoder->data, colorItemID); + AVIF_ASSERT_OR_RETURN(colorItem != NULL); + AVIF_ASSERT_OR_RETURN(colorItem->dimgFromID == 0); // Our internal API only allows one dimg value per item. + colorItem->dimgFromID = sampleTransformItemID; + bitDepthExtensionColorItem->dimgFromID = sampleTransformItemID; + + if (encoder->data->alphaPresent) { + uint16_t bitDepthExtensionAlphaItemId; + AVIF_CHECKRES( + avifEncoderAddImageItems(encoder, gridCols, gridRows, gridWidth, gridHeight, AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA, &bitDepthExtensionAlphaItemId)); + avifEncoderItem * bitDepthExtensionAlphaItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionAlphaItemId); + assert(bitDepthExtensionAlphaItem); + bitDepthExtensionAlphaItem->irefType = "auxl"; + bitDepthExtensionAlphaItem->irefToID = bitDepthExtensionColorItemId; + if (encoder->data->imageMetadata->alphaPremultiplied) { + // The reference may have changed; fetch it again. + bitDepthExtensionColorItem = avifEncoderDataFindItemByID(encoder->data, bitDepthExtensionColorItemId); + assert(bitDepthExtensionColorItem); + bitDepthExtensionColorItem->irefType = "prem"; + bitDepthExtensionColorItem->irefToID = bitDepthExtensionAlphaItemId; + } + } + return AVIF_RESULT_OK; +} + +// Same as avifImageApplyExpression() but for the expression (inputImageItem [op] constant). +// Convenience function. +static avifResult avifImageApplyOperation(avifImage * result, + const avifImage * inputImageItem, + avifSampleTransformTokenType op, + int32_t constant, + avifPlanesFlags planes) +{ + // Postfix notation. + const avifSampleTransformToken tokens[] = { { AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0, /*inputImageItemIndex=*/1 }, + { AVIF_SAMPLE_TRANSFORM_CONSTANT, constant, 0 }, + { op, 0, 0 } }; + return avifImageApplyOperations(result, AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, 3, tokens, 1, &inputImageItem, planes); +} + +static avifResult avifEncoderCreateBitDepthExtensionImage(const avifEncoder * encoder, + const avifEncoderItem * item, + avifBool itemWillBeEncodedLosslessly, + const avifImage * image, + avifImage ** sampleTransformedImage) +{ + // The bit depth of the first image item used as input to the 'sato' Sample Transform derived image item. + uint32_t numMostSignificantBits; + // The bit depth of the current image item used as input to the 'sato' Sample Transform derived image item. + uint32_t sampleTransformInputImageDepth; + if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B) { + numMostSignificantBits = 8; + sampleTransformInputImageDepth = 8; + } else { + AVIF_ASSERT_OR_RETURN(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B); + numMostSignificantBits = 12; + if (item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA) { + sampleTransformInputImageDepth = numMostSignificantBits; + } else { + AVIF_ASSERT_OR_RETURN(item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR || + item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA); + sampleTransformInputImageDepth = 8; // Will be shifted to 4-bit samples at decoding. + } + } + AVIF_ASSERT_OR_RETURN(image->depth == 16); // Other bit depths could be supported but for now it is 16-bit only. + const uint32_t numLeastSignificantBits = image->depth - numMostSignificantBits; + + *sampleTransformedImage = avifImageCreate(image->width, image->height, sampleTransformInputImageDepth, image->yuvFormat); + AVIF_CHECKERR(*sampleTransformedImage != NULL, AVIF_RESULT_OUT_OF_MEMORY); + const avifPlanesFlag planes = avifIsAlpha(item->itemCategory) ? AVIF_PLANES_A : AVIF_PLANES_YUV; + const avifResult allocationResult = avifImageAllocatePlanes(*sampleTransformedImage, planes); + if (allocationResult != AVIF_RESULT_OK) { + avifImageDestroy(*sampleTransformedImage); + return allocationResult; + } + + avifResult result; + if (item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA) { + // 16-bit to sampleTransformInputImageDepth-bit so shift right by numLeastSignificantBits bits. + // Equivalent to dividing by 1<itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && + item->itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY); + // Keep the numLeastSignificantBits from the 16-bit image. Use a bit mask. + result = + avifImageApplyOperation(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_AND, (1 << numLeastSignificantBits) - 1, planes); + + if (result == AVIF_RESULT_OK && (*sampleTransformedImage)->depth != numLeastSignificantBits) { + AVIF_ASSERT_OR_RETURN((*sampleTransformedImage)->depth > numLeastSignificantBits); + // AVIF only supports 8, 10 or 12-bit image items. Scale the samples to fit the range. + // Note: The samples could be encoded as is without being shifted left before encoding, + // but they would not be shifted right after decoding either. Right shifting after + // decoding provides a guarantee on the range of values and on the lack of integer + // overflow, so it is safer to do these extra steps. + // It also makes more sense from a compression point-of-view to use the full range. + // Transform in-place. + const uint32_t numShiftedBits = (*sampleTransformedImage)->depth - numLeastSignificantBits; + result = avifImageApplyOperation(*sampleTransformedImage, + *sampleTransformedImage, + AVIF_SAMPLE_TRANSFORM_PRODUCT, + 1 << numShiftedBits, + planes); + + if (result == AVIF_RESULT_OK && !itemWillBeEncodedLosslessly) { + // Small loss at encoding could be amplified by the truncation caused by the right + // shift after decoding. Offset sample values now, before encoding, to round rather + // than floor the samples shifted after decoding. + // Note: Samples were just left shifted by numShiftedBits, so adding less than + // (1<> 1, + planes); + } + } + } + if (result != AVIF_RESULT_OK) { + avifImageDestroy(*sampleTransformedImage); + return result; + } + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + static avifCodecType avifEncoderGetCodecType(const avifEncoder * encoder) { // TODO(yguyon): Rework when AVIF_CODEC_CHOICE_AUTO can be AVM @@ -1192,20 +1434,18 @@ static avifBool avifEncoderDataShouldForceKeyframeForAlpha(const avifEncoderData static avifResult avifGetErrorForItemCategory(avifItemCategory itemCategory) { - return (itemCategory == AVIF_ITEM_ALPHA) ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED; -} - -static avifResult avifValidateImageBasicProperties(const avifImage * avifImage) -{ - if ((avifImage->depth != 8) && (avifImage->depth != 10) && (avifImage->depth != 12)) { - return AVIF_RESULT_UNSUPPORTED_DEPTH; +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + if (itemCategory == AVIF_ITEM_GAIN_MAP) { + return AVIF_RESULT_ENCODE_GAIN_MAP_FAILED; } - - if (avifImage->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { - return AVIF_RESULT_NO_YUV_FORMAT_SELECTED; +#endif +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM || + (itemCategory >= AVIF_SAMPLE_TRANSFORM_MIN_CATEGORY && itemCategory <= AVIF_SAMPLE_TRANSFORM_MAX_CATEGORY)) { + return AVIF_RESULT_ENCODE_SAMPLE_TRANSFORM_FAILED; } - - return AVIF_RESULT_OK; +#endif + return avifIsAlpha(itemCategory) ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED; } static uint32_t avifGridWidth(uint32_t gridCols, const avifImage * firstCell, const avifImage * bottomRightCell) @@ -1326,7 +1566,14 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, const avifImage * firstCell = cellImages[0]; const avifImage * bottomRightCell = cellImages[cellCount - 1]; - AVIF_CHECKRES(avifValidateImageBasicProperties(firstCell)); +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + AVIF_CHECKERR(firstCell->depth == 8 || firstCell->depth == 10 || firstCell->depth == 12 || + (firstCell->depth == 16 && encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE), + AVIF_RESULT_UNSUPPORTED_DEPTH); +#else + AVIF_CHECKERR(firstCell->depth == 8 || firstCell->depth == 10 || firstCell->depth == 12, AVIF_RESULT_UNSUPPORTED_DEPTH); +#endif + AVIF_CHECKERR(firstCell->yuvFormat != AVIF_PIXEL_FORMAT_NONE, AVIF_RESULT_NO_YUV_FORMAT_SELECTED); if (!firstCell->width || !firstCell->height || !bottomRightCell->width || !bottomRightCell->height) { return AVIF_RESULT_NO_CONTENT; } @@ -1388,7 +1635,12 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, } if (hasGainMap) { - AVIF_CHECKRES(avifValidateImageBasicProperties(firstCell->gainMap->image)); + // AVIF supports 16-bit images through sample transforms used as bit depth extensions, + // but this is not implemented for gain maps for now. Stick to at most 12 bits. + AVIF_CHECKERR(firstCell->gainMap->image->depth == 8 || firstCell->gainMap->image->depth == 10 || + firstCell->gainMap->image->depth == 12, + AVIF_RESULT_UNSUPPORTED_DEPTH); + AVIF_CHECKERR(firstCell->gainMap->image->yuvFormat != AVIF_PIXEL_FORMAT_NONE, AVIF_RESULT_NO_YUV_FORMAT_SELECTED); AVIF_CHECKRES(avifValidateGrid(gridCols, gridRows, cellImages, /*validateGainMap=*/AVIF_TRUE, &encoder->diag)); if (firstCell->gainMap->image->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED || firstCell->gainMap->image->transferCharacteristics != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) { @@ -1543,6 +1795,7 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, toneMappedItem->itemCategory = AVIF_ITEM_COLOR; uint16_t toneMappedItemID = toneMappedItem->id; + AVIF_ASSERT_OR_RETURN(encoder->data->alternativeItemIDs.count == 0); uint16_t * alternativeItemID = (uint16_t *)avifArrayPush(&encoder->data->alternativeItemIDs); AVIF_CHECKERR(alternativeItemID != NULL, AVIF_RESULT_OUT_OF_MEMORY); *alternativeItemID = toneMappedItemID; @@ -1576,6 +1829,20 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, } #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || + encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) { + // For now, only 16-bit depth is supported. + AVIF_ASSERT_OR_RETURN(firstCell->depth == 16); +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + AVIF_CHECKERR(!firstCell->gainMap, AVIF_RESULT_NOT_IMPLEMENTED); // TODO(yguyon): Implement 16-bit HDR +#endif + AVIF_CHECKRES(avifEncoderCreateBitDepthExtensionItems(encoder, gridCols, gridRows, gridWidth, gridHeight, colorItemID)); + } else { + AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_NONE, AVIF_RESULT_NOT_IMPLEMENTED); + } +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + // ----------------------------------------------------------------------- // Create metadata items (Exif, XMP) @@ -1642,7 +1909,9 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, avifEncoderItem * item = &encoder->data->items.item[itemIndex]; if (item->codec) { const avifImage * cellImage = cellImages[item->cellIndex]; + avifImage * cellImagePlaceholder = NULL; // May be used as a temporary, modified cellImage. Left as NULL otherwise. const avifImage * firstCellImage = firstCell; + #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) if (item->itemCategory == AVIF_ITEM_GAIN_MAP) { AVIF_ASSERT_OR_RETURN(cellImage->gainMap && cellImage->gainMap->image); @@ -1651,29 +1920,69 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, firstCellImage = firstCell->gainMap->image; } #endif - avifImage * paddedCellImage = NULL; + if ((cellImage->width != firstCellImage->width) || (cellImage->height != firstCellImage->height)) { - paddedCellImage = avifImageCreateEmpty(); - AVIF_CHECKERR(paddedCellImage, AVIF_RESULT_OUT_OF_MEMORY); - const avifResult result = avifImageCopyAndPad(paddedCellImage, cellImage, firstCellImage->width, firstCellImage->height); + cellImagePlaceholder = avifImageCreateEmpty(); + AVIF_CHECKERR(cellImagePlaceholder, AVIF_RESULT_OUT_OF_MEMORY); + const avifResult result = + avifImageCopyAndPad(cellImagePlaceholder, cellImage, firstCellImage->width, firstCellImage->height); if (result != AVIF_RESULT_OK) { - avifImageDestroy(paddedCellImage); + avifImageDestroy(cellImagePlaceholder); return result; } - cellImage = paddedCellImage; + cellImage = cellImagePlaceholder; } - const int quantizer = (item->itemCategory == AVIF_ITEM_ALPHA) ? encoder->data->quantizerAlpha + + const avifBool isAlpha = avifIsAlpha(item->itemCategory); + int quantizer = isAlpha ? encoder->data->quantizerAlpha #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) - : (item->itemCategory == AVIF_ITEM_GAIN_MAP) ? encoder->data->quantizerGainMap + : (item->itemCategory == AVIF_ITEM_GAIN_MAP) ? encoder->data->quantizerGainMap #endif - : encoder->data->quantizer; + : encoder->data->quantizer; + +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // Remember original quantizer values in case they change, to reset them afterwards. + int * encoderMinQuantizer = isAlpha ? &encoder->minQuantizerAlpha : &encoder->minQuantizer; + int * encoderMaxQuantizer = isAlpha ? &encoder->maxQuantizerAlpha : &encoder->maxQuantizer; + const int originalMinQuantizer = *encoderMinQuantizer; + const int originalMaxQuantizer = *encoderMaxQuantizer; + + if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || + encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) { + if (item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA) { + // Encoding the least significant bits of a sample does not make any sense if the + // other bits are lossily compressed. Encode the most significant bits losslessly. + quantizer = AVIF_QUANTIZER_LOSSLESS; + *encoderMinQuantizer = AVIF_QUANTIZER_LOSSLESS; + *encoderMaxQuantizer = AVIF_QUANTIZER_LOSSLESS; + if (!avifEncoderDetectChanges(encoder, &encoderChanges)) { + assert(AVIF_FALSE); + } + } + + // Replace cellImage by the first or second input to the AVIF_ITEM_SAMPLE_TRANSFORM derived image item. + const avifBool itemWillBeEncodedLosslessly = (quantizer == AVIF_QUANTIZER_LOSSLESS); + avifImage * sampleTransformedImage = NULL; + if (cellImagePlaceholder) { + avifImageDestroy(cellImagePlaceholder); // Replaced by sampleTransformedImage. + cellImagePlaceholder = NULL; + } + AVIF_CHECKRES( + avifEncoderCreateBitDepthExtensionImage(encoder, item, itemWillBeEncodedLosslessly, cellImage, &sampleTransformedImage)); + cellImagePlaceholder = sampleTransformedImage; // Transfer ownership. + cellImage = cellImagePlaceholder; + } else { + AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_NONE, AVIF_RESULT_NOT_IMPLEMENTED); + } +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + // If alpha channel is present, set disableLaggedOutput to AVIF_TRUE. If the encoder supports it, this enables // avifEncoderDataShouldForceKeyframeForAlpha to force a keyframe in the alpha channel whenever a keyframe has been // encoded in the color channel for animated images. avifResult encodeResult = item->codec->encodeImage(item->codec, encoder, cellImage, - item->itemCategory == AVIF_ITEM_ALPHA, + isAlpha, encoder->data->tileRowsLog2, encoder->data->tileColsLog2, quantizer, @@ -1681,15 +1990,21 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, /*disableLaggedOutput=*/encoder->data->alphaPresent, addImageFlags, item->encodeOutput); - if (paddedCellImage) { - avifImageDestroy(paddedCellImage); +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + // Revert quality settings if they changed. + if (*encoderMinQuantizer != originalMinQuantizer || *encoderMaxQuantizer != originalMaxQuantizer) { + avifEncoderBackupSettings(encoder); // Remember last encoding settings for next avifEncoderDetectChanges(). + *encoderMinQuantizer = originalMinQuantizer; + *encoderMaxQuantizer = originalMaxQuantizer; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM + if (cellImagePlaceholder) { + avifImageDestroy(cellImagePlaceholder); } if (encodeResult == AVIF_RESULT_UNKNOWN_ERROR) { encodeResult = avifGetErrorForItemCategory(item->itemCategory); } - if (encodeResult != AVIF_RESULT_OK) { - return encodeResult; - } + AVIF_CHECKRES(encodeResult); if (itemIndex == 0 && avifEncoderDataShouldForceKeyframeForAlpha(encoder->data, item, addImageFlags)) { addImageFlags |= AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME; } @@ -1780,10 +2095,12 @@ static avifResult avifEncoderWriteMediaDataBox(avifEncoder * encoder, // only process metadata (XMP/Exif) payloads when metadataPass is true continue; } - avifBool isAlphaOrGainMap = item->itemCategory == AVIF_ITEM_ALPHA; + const avifBool isAlpha = avifIsAlpha(item->itemCategory); + const avifBool isAlphaOrGainMap = isAlpha #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) - isAlphaOrGainMap = isAlphaOrGainMap || item->itemCategory == AVIF_ITEM_GAIN_MAP; + || item->itemCategory == AVIF_ITEM_GAIN_MAP #endif + ; if (alphaAndGainMapPass != isAlphaOrGainMap) { // only process alpha payloads when alphaPass is true continue; @@ -1794,8 +2111,7 @@ static avifResult avifEncoderWriteMediaDataBox(avifEncoder * encoder, // We always interleave all AV1 items for layered images. AVIF_ASSERT_OR_RETURN(item->encodeOutput->samples.count == item->mdatFixups.count); - avifEncoderItemReference * ref = (item->itemCategory == AVIF_ITEM_ALPHA) ? avifArrayPush(layeredAlphaItems) - : avifArrayPush(layeredColorItems); + avifEncoderItemReference * ref = isAlpha ? avifArrayPush(layeredAlphaItems) : avifArrayPush(layeredColorItems); AVIF_CHECKERR(ref != NULL, AVIF_RESULT_OUT_OF_MEMORY); *ref = item; continue; @@ -1820,7 +2136,7 @@ static avifResult avifEncoderWriteMediaDataBox(avifEncoder * encoder, avifEncodeSample * sample = &item->encodeOutput->samples.sample[sampleIndex]; AVIF_CHECKRES(avifRWStreamWrite(s, sample->data.data, sample->data.size)); - if (item->itemCategory == AVIF_ITEM_ALPHA) { + if (isAlpha) { encoder->ioStats.alphaOBUSize += sample->data.size; } else if (item->itemCategory == AVIF_ITEM_COLOR) { encoder->ioStats.colorOBUSize += sample->data.size; @@ -1982,6 +2298,12 @@ static avifBool avifEncoderIsCondensedImageBoxCompatible(const avifEncoder * enc return AVIF_FALSE; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (encoder->sampleTransformRecipe != AVIF_SAMPLE_TRANSFORM_NONE) { + return AVIF_FALSE; + } +#endif + // 4:4:4, 4:2:2, 4:2:0 and 4:0:0 are supported by a CondensedImageBox. if (encoder->data->imageMetadata->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { return AVIF_FALSE; @@ -2206,8 +2528,9 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu avifEncoderItem * item = &encoder->data->items.item[itemIndex]; const avifBool isGrid = (item->gridCols > 0); const avifBool isToneMappedImage = !memcmp(item->type, "tmap", 4); + const avifBool isSampleTransformImage = !memcmp(item->type, "sato", 4); memset(&item->ipma, 0, sizeof(item->ipma)); - if (!item->codec && !isGrid && !isToneMappedImage) { + if (!item->codec && !isGrid && !isToneMappedImage && !isSampleTransformImage) { // No ipma to write for this item continue; } @@ -2254,7 +2577,7 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu imageHeight = item->gridHeight; } - // Properties all image items need + // Properties all image items need (coded and derived) // ispe = image spatial extent (width, height) avifItemPropertyDedupStart(dedup); avifBoxMarker ispe; @@ -2272,15 +2595,37 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu hasPixi = AVIF_FALSE; } #endif + const avifBool isAlpha = avifIsAlpha(item->itemCategory); + uint8_t depth = (uint8_t)itemMetadata->depth; +#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B || + encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) { + if (item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM) { + AVIF_ASSERT_OR_RETURN(depth == 16); + } else if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B) { + depth = 8; + } else { + if (item->itemCategory == AVIF_ITEM_COLOR || item->itemCategory == AVIF_ITEM_ALPHA) { + depth = 12; + } else { + AVIF_ASSERT_OR_RETURN(item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR || + item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA); + depth = 8; // Will be shifted to 4-bit samples at decoding. + } + } + } else { + AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_NONE, AVIF_RESULT_NOT_IMPLEMENTED); + } + assert(isSampleTransformImage == (item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM)); +#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM if (hasPixi) { avifItemPropertyDedupStart(dedup); - uint8_t channelCount = - (item->itemCategory == AVIF_ITEM_ALPHA || (itemMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) ? 1 : 3; + uint8_t channelCount = (isAlpha || (itemMetadata->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) ? 1 : 3; avifBoxMarker pixi; AVIF_CHECKRES(avifRWStreamWriteFullBox(&dedup->s, "pixi", AVIF_BOX_SIZE_TBD, 0, 0, &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, (uint8_t)itemMetadata->depth)); // unsigned int (8) bits_per_channel; + AVIF_CHECKRES(avifRWStreamWriteU8(&dedup->s, depth)); // unsigned int (8) bits_per_channel; } avifRWStreamFinishBox(&dedup->s, pixi); AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, s, &item->ipma, AVIF_FALSE)); @@ -2293,7 +2638,7 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu AVIF_CHECKRES(avifItemPropertyDedupFinish(dedup, s, &item->ipma, AVIF_TRUE)); } - if (item->itemCategory == AVIF_ITEM_ALPHA) { + if (isAlpha) { // Alpha specific properties avifItemPropertyDedupStart(dedup); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4ec1593530..6786c12d86 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -100,6 +100,11 @@ if(AVIF_ENABLE_GTEST) endif() if(AVIF_ENABLE_GTEST) + if(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + add_avif_gtest_with_data(avif16bittest) + add_avif_gtest(avifsampletransformtest) + endif() + add_avif_gtest(avifallocationtest) add_avif_gtest_with_data(avifalphanoispetest) add_avif_gtest(avifalphapremtest) @@ -363,6 +368,9 @@ if(AVIF_CODEC_AVM_ENABLED) set_tests_properties(avifjpeggainmaptest PROPERTIES DISABLED True) endif() endif() + if(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) + set_tests_properties(avif16bittest PROPERTIES DISABLED True) + endif() endif() if(AVIF_BUILD_APPS) diff --git a/tests/gtest/avif16bittest.cc b/tests/gtest/avif16bittest.cc new file mode 100644 index 0000000000..ccdaa94f99 --- /dev/null +++ b/tests/gtest/avif16bittest.cc @@ -0,0 +1,141 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include + +#include "avif/avif.h" +#include "avif/avif_cxx.h" +#include "avif/internal.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace avif { +namespace { + +//------------------------------------------------------------------------------ + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + +class SampleTransformTest + : public testing::TestWithParam< + std::tuple> {}; + +//------------------------------------------------------------------------------ + +TEST_P(SampleTransformTest, Avif16bit) { + const avifSampleTransformRecipe recipe = std::get<0>(GetParam()); + const avifPixelFormat yuv_format = std::get<1>(GetParam()); + const bool create_alpha = std::get<2>(GetParam()); + const int quality = std::get<3>(GetParam()); + + const ImagePtr image = testutil::ReadImage( + data_path, "weld_16bit.png", yuv_format, /*requested_depth=*/16); + ASSERT_NE(image, nullptr); + if (create_alpha && !image->alphaPlane) { + // Simulate alpha plane with a view on luma. + image->alphaPlane = image->yuvPlanes[AVIF_CHAN_Y]; + image->alphaRowBytes = image->yuvRowBytes[AVIF_CHAN_Y]; + image->imageOwnsAlphaPlane = false; + } + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->speed = AVIF_SPEED_FASTEST; + encoder->quality = quality; + encoder->qualityAlpha = quality; + encoder->sampleTransformRecipe = recipe; + testutil::AvifRwData encoded; + ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), + AVIF_RESULT_OK); + const ImagePtr decoded = testutil::Decode(encoded.data, encoded.size); + ASSERT_NE(decoded, nullptr); + + ASSERT_EQ(image->depth, decoded->depth); + ASSERT_EQ(image->width, decoded->width); + ASSERT_EQ(image->height, decoded->height); + + EXPECT_GE(testutil::GetPsnr(*image, *decoded), + (quality == AVIF_QUALITY_LOSSLESS) ? 99.0 : 15.0); + + // Replace all 'sato' box types by "zzzz" garbage. This simulates an old + // decoder that does not recognize the Sample Transform feature. + for (size_t i = 0; i + 4 <= encoded.size; ++i) { + if (!std::memcmp(&encoded.data[i], "sato", 4)) { + std::memcpy(&encoded.data[i], "zzzz", 4); + } + } + const ImagePtr decoded_no_sato = testutil::Decode(encoded.data, encoded.size); + ASSERT_NE(decoded_no_sato, nullptr); + // Only the most significant bits of each sample can be retrieved. + // They should be encoded losslessly no matter the quantizer settings. + ImagePtr image_no_sato = testutil::CreateImage( + static_cast(image->width), static_cast(image->height), + static_cast(decoded_no_sato->depth), image->yuvFormat, + image->alphaPlane ? AVIF_PLANES_ALL : AVIF_PLANES_YUV, image->yuvRange); + ASSERT_NE(image_no_sato, nullptr); + + const uint32_t shift = + recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ? 8 : 4; + const avifImage* inputImage = image.get(); + // Postfix notation. + const avifSampleTransformToken tokens[] = { + {AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0, + /*inputImageItemIndex=*/1}, + {AVIF_SAMPLE_TRANSFORM_CONSTANT, 1 << shift, 0}, + {AVIF_SAMPLE_TRANSFORM_DIVIDE, 0, 0}}; + ASSERT_EQ(avifImageApplyOperations(image_no_sato.get(), + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, 3, + tokens, 1, &inputImage, AVIF_PLANES_ALL), + AVIF_RESULT_OK); + EXPECT_TRUE(testutil::AreImagesEqual(*image_no_sato, *decoded_no_sato)); +} + +//------------------------------------------------------------------------------ + +INSTANTIATE_TEST_SUITE_P( + Formats, SampleTransformTest, + testing::Combine( + testing::Values(AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B), + testing::Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PIXEL_FORMAT_YUV400), + /*create_alpha=*/testing::Values(false), + /*quality=*/ + testing::Values(AVIF_QUALITY_DEFAULT))); + +INSTANTIATE_TEST_SUITE_P( + BitDepthExtensions, SampleTransformTest, + testing::Combine( + testing::Values(AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B, + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B), + testing::Values(AVIF_PIXEL_FORMAT_YUV444), + /*create_alpha=*/testing::Values(false), + /*quality=*/ + testing::Values(AVIF_QUALITY_LOSSLESS))); + +INSTANTIATE_TEST_SUITE_P( + Alpha, SampleTransformTest, + testing::Combine( + testing::Values(AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B), + testing::Values(AVIF_PIXEL_FORMAT_YUV444), + /*create_alpha=*/testing::Values(true), + /*quality=*/ + testing::Values(AVIF_QUALITY_LOSSLESS))); + +// TODO(yguyon): Test grids with bit depth extensions. + +} // 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(); +} diff --git a/tests/gtest/avifsampletransformtest.cc b/tests/gtest/avifsampletransformtest.cc new file mode 100644 index 0000000000..6e5f08c849 --- /dev/null +++ b/tests/gtest/avifsampletransformtest.cc @@ -0,0 +1,176 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/avif.h" +#include "avif/avif_cxx.h" +#include "avif/internal.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace avif { +namespace { + +//------------------------------------------------------------------------------ + +class AvifExpression : public avifSampleTransformExpression { + public: + AvifExpression() : avifSampleTransformExpression{} { + if (!avifArrayCreate(this, sizeof(avifSampleTransformToken), 1)) { + abort(); + } + } + void AddConstant(int32_t constant) { + avifSampleTransformToken& token = AddToken(); + token.value = AVIF_SAMPLE_TRANSFORM_CONSTANT; + token.constant = constant; + } + void AddImage(uint8_t inputImageItemIndex) { + avifSampleTransformToken& token = AddToken(); + token.value = AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX; + token.inputImageItemIndex = inputImageItemIndex; + } + void AddOperator(uint8_t op) { + avifSampleTransformToken& token = AddToken(); + token.value = op; + } + ~AvifExpression() { avifArrayDestroy(this); } + + private: + avifSampleTransformToken& AddToken() { + avifSampleTransformToken* token = + reinterpret_cast(avifArrayPush(this)); + if (token == nullptr) abort(); + return *token; + } +}; + +//------------------------------------------------------------------------------ + +TEST(SampleTransformTest, NoExpression) { + AvifExpression empty; + ASSERT_EQ( + avifSampleTransformRecipeToExpression(AVIF_SAMPLE_TRANSFORM_NONE, &empty), + AVIF_RESULT_INVALID_ARGUMENT); + EXPECT_TRUE(avifSampleTransformExpressionIsEquivalentTo(&empty, &empty)); +} + +TEST(SampleTransformTest, NoRecipe) { + AvifExpression empty; + avifSampleTransformRecipe recipe; + ASSERT_EQ(avifSampleTransformExpressionToRecipe(&empty, &recipe), + AVIF_RESULT_OK); + EXPECT_EQ(recipe, AVIF_SAMPLE_TRANSFORM_NONE); +} + +TEST(SampleTransformTest, RecipeToExpression) { + for (avifSampleTransformRecipe recipe : + {AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B, + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B}) { + AvifExpression expression; + ASSERT_EQ(avifSampleTransformRecipeToExpression(recipe, &expression), + AVIF_RESULT_OK); + avifSampleTransformRecipe result; + ASSERT_EQ(avifSampleTransformExpressionToRecipe(&expression, &result), + AVIF_RESULT_OK); + EXPECT_EQ(recipe, result); + + EXPECT_FALSE(avifSampleTransformExpressionIsValid( + &expression, /*numInputImageItems=*/1)); + EXPECT_TRUE(avifSampleTransformExpressionIsValid(&expression, + /*numInputImageItems=*/2)); + EXPECT_TRUE(avifSampleTransformExpressionIsValid(&expression, + /*numInputImageItems=*/3)); + + EXPECT_TRUE( + avifSampleTransformExpressionIsEquivalentTo(&expression, &expression)); + } +} + +TEST(SampleTransformTest, NotEquivalent) { + AvifExpression a; + ASSERT_EQ(avifSampleTransformRecipeToExpression( + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B, &a), + AVIF_RESULT_OK); + + AvifExpression b; + ASSERT_EQ(avifSampleTransformRecipeToExpression( + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B, &a), + AVIF_RESULT_OK); + + EXPECT_FALSE(avifSampleTransformExpressionIsEquivalentTo(&a, &b)); +} + +//------------------------------------------------------------------------------ + +struct Op { + int32_t left_operand; + avifSampleTransformTokenType op; + int32_t right_operand; + uint32_t expected_result; +}; + +class SampleTransformOperationTest : public testing::TestWithParam {}; + +ImagePtr OneByOne(uint32_t depth) { + ImagePtr image(avifImageCreate(/*width=*/1, /*height=*/1, depth, + AVIF_PIXEL_FORMAT_YUV444)); + if (image.get() != nullptr && + avifImageAllocatePlanes(image.get(), AVIF_PLANES_YUV) == AVIF_RESULT_OK) { + return image; + } + return nullptr; +} + +TEST_P(SampleTransformOperationTest, Apply) { + AvifExpression expression; + // Postfix notation. + expression.AddConstant(GetParam().left_operand); + expression.AddConstant(GetParam().right_operand); + expression.AddOperator(GetParam().op); + + ImagePtr result(avifImageCreate(/*width=*/1, /*height=*/1, /*depth=*/8, + AVIF_PIXEL_FORMAT_YUV444)); + ASSERT_NE(result.get(), nullptr); + ASSERT_EQ(avifImageAllocatePlanes(result.get(), AVIF_PLANES_YUV), + AVIF_RESULT_OK); + + ASSERT_EQ(avifImageApplyExpression( + result.get(), AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32, &expression, + /*numInputImageItems=*/0, nullptr, AVIF_PLANES_YUV), + AVIF_RESULT_OK); + EXPECT_EQ(result->yuvPlanes[0][0], GetParam().expected_result); +} + +INSTANTIATE_TEST_SUITE_P( + Operations, SampleTransformOperationTest, + testing::Values(Op{1, AVIF_SAMPLE_TRANSFORM_SUM, 1, 2}, + Op{255, AVIF_SAMPLE_TRANSFORM_SUM, 255, 255}, + Op{1, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 1, 0}, + Op{255, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 255, 0}, + Op{255, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 0, 255}, + Op{0, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 255, 0}, + Op{1, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, -1, 2}, + Op{-1, AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 1, 0}, + Op{1, AVIF_SAMPLE_TRANSFORM_PRODUCT, 1, 1}, + Op{2, AVIF_SAMPLE_TRANSFORM_PRODUCT, 3, 6}, + Op{1, AVIF_SAMPLE_TRANSFORM_DIVIDE, 1, 1}, + Op{2, AVIF_SAMPLE_TRANSFORM_DIVIDE, 3, 0}, + Op{1, AVIF_SAMPLE_TRANSFORM_AND, 1, 1}, + Op{1, AVIF_SAMPLE_TRANSFORM_AND, 2, 0}, + Op{7, AVIF_SAMPLE_TRANSFORM_AND, 15, 7}, + Op{1, AVIF_SAMPLE_TRANSFORM_OR, 1, 1}, + Op{1, AVIF_SAMPLE_TRANSFORM_OR, 2, 3}, + Op{1, AVIF_SAMPLE_TRANSFORM_XOR, 3, 2}, + Op{254, AVIF_SAMPLE_TRANSFORM_NOR, 1, 0}, + Op{0, AVIF_SAMPLE_TRANSFORM_MSB, 123, 123}, + Op{61, AVIF_SAMPLE_TRANSFORM_MSB, 123, 5}, + Op{2, AVIF_SAMPLE_TRANSFORM_POW, 4, 16}, + Op{4, AVIF_SAMPLE_TRANSFORM_POW, 2, 16}, + Op{123, AVIF_SAMPLE_TRANSFORM_POW, 123, 255}, + Op{123, AVIF_SAMPLE_TRANSFORM_MIN, 124, 123}, + Op{123, AVIF_SAMPLE_TRANSFORM_MAX, 124, 124})); + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace avif