Allow padding in avifenc avifImageSplitGrid()
So that images with dimensions that cannot be evenly split into the
number of cells specified with the --grid flag can still be encoded
with the right-most and bottom-most cells padded.
Add test_cmd_grid.sh.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51b9bcf..2a6a112 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@
* Add STATIC library target avif_internal to allow tests to access functions
from internal.h when BUILD_SHARED_LIBS is ON.
+### Changed
+* The --grid flag in avifenc can be used for images that are not evenly divided
+ into cells.
+
## [0.11.1] - 2022-10-19
### Changed
diff --git a/apps/avifenc.c b/apps/avifenc.c
index ba37b72..69e28a9 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -340,25 +340,67 @@
return AVIF_TRUE;
}
-static avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells)
+// Returns the best cell size for a given horizontal or vertical dimension.
+static avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize)
{
- if ((gridSplitImage->width % gridCols) != 0) {
- fprintf(stderr, "ERROR: Can't split image width (%u) evenly into %u columns.\n", gridSplitImage->width, gridCols);
- return AVIF_FALSE;
+ assert(numPixels);
+ assert(numCells);
+
+ // ISO/IEC 23008-12:2017, Section 6.6.2.3.1:
+ // The reconstructed image is formed by tiling the input images into a grid with a column width
+ // (potentially excluding the right-most column) equal to tile_width and a row height (potentially
+ // excluding the bottom-most row) equal to tile_height, without gap or overlap, and then
+ // trimming on the right and the bottom to the indicated output_width and output_height.
+ // The priority could be to use a cell size that is a multiple of 64, but there is not always a valid one,
+ // even though it is recommended by MIAF. Just use ceil(numPixels/numCells) for simplicity and to avoid
+ // as much padding in the right-most and bottom-most cells as possible.
+ // Use uint64_t computation to avoid a potential uint32_t overflow.
+ *cellSize = (uint32_t)(((uint64_t)numPixels + numCells - 1) / numCells);
+
+ // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+ // - the tile_width shall be greater than or equal to 64, and should be a multiple of 64
+ // - the tile_height shall be greater than or equal to 64, and should be a multiple of 64
+ if (*cellSize < 64) {
+ *cellSize = 64;
+ if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
+ // Some cells would be entirely off-canvas.
+ fprintf(stderr, "ERROR: There are too many cells %s (%u) to have at least 64 pixels per cell.\n", dimensionStr, numCells);
+ return AVIF_FALSE;
+ }
}
- if ((gridSplitImage->height % gridRows) != 0) {
- fprintf(stderr, "ERROR: Can't split image height (%u) evenly into %u rows.\n", gridSplitImage->height, gridRows);
+
+ // The maximum AV1 frame size is 65536 pixels inclusive.
+ if (*cellSize > 65536) {
+ fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum AV1 frame size 65536.\n", *cellSize, dimensionStr);
return AVIF_FALSE;
}
- uint32_t cellWidth = gridSplitImage->width / gridCols;
- uint32_t cellHeight = gridSplitImage->height / gridRows;
- if ((cellWidth < 64) || (cellHeight < 64)) {
- fprintf(stderr, "ERROR: Split cell dimensions are too small (must be at least 64x64, and were %ux%u)\n", cellWidth, cellHeight);
- return AVIF_FALSE;
+ // ISO/IEC 23000-22:2019, Section 7.3.11.4.2:
+ // - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets and widths,
+ // and the output width, shall be even numbers;
+ // - when the images are in the 4:2:0 chroma sampling format both the horizontal and vertical tile
+ // offsets and widths, and the output width and height, shall be even numbers.
+ if (isSubsampled && (*cellSize & 1)) {
+ ++*cellSize;
+ if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) {
+ // Some cells would be entirely off-canvas.
+ fprintf(stderr, "ERROR: Odd cell size %u is forbidden on a %s subsampled image.\n", *cellSize - 1, dimensionStr);
+ return AVIF_FALSE;
+ }
}
- if (((cellWidth % 2) != 0) || ((cellHeight % 2) != 0)) {
- fprintf(stderr, "ERROR: Odd split cell dimensions are unsupported (%ux%u)\n", cellWidth, cellHeight);
+
+ // Each pixel is covered by exactly one cell, and each cell contains at least one pixel.
+ assert(((uint64_t)(numCells - 1) * *cellSize < (uint64_t)numPixels) && ((uint64_t)numCells * *cellSize >= (uint64_t)numPixels));
+ return AVIF_TRUE;
+}
+
+static avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells)
+{
+ uint32_t cellWidth, cellHeight;
+ avifPixelFormatInfo formatInfo;
+ avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &formatInfo);
+ if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, formatInfo.chromaShiftX, &cellWidth) ||
+ !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, formatInfo.chromaShiftY, &cellHeight)) {
return AVIF_FALSE;
}
@@ -368,47 +410,18 @@
avifImage * cellImage = avifImageCreateEmpty();
gridCells[gridIndex] = cellImage;
- const avifResult copyResult = avifImageCopy(cellImage, gridSplitImage, 0);
+ avifCropRect cellRect = { gridX * cellWidth, gridY * cellHeight, cellWidth, cellHeight };
+ if (cellRect.x + cellRect.width > gridSplitImage->width) {
+ cellRect.width = gridSplitImage->width - cellRect.x;
+ }
+ if (cellRect.y + cellRect.height > gridSplitImage->height) {
+ cellRect.height = gridSplitImage->height - cellRect.y;
+ }
+ const avifResult copyResult = avifImageSetViewRect(cellImage, gridSplitImage, &cellRect);
if (copyResult != AVIF_RESULT_OK) {
- fprintf(stderr, "ERROR: Image copy failed: %s\n", avifResultToString(copyResult));
+ fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult));
return AVIF_FALSE;
}
- cellImage->width = cellWidth;
- cellImage->height = cellHeight;
-
- const uint32_t bytesPerPixel = avifImageUsesU16(cellImage) ? 2 : 1;
-
- const uint32_t bytesPerRowY = bytesPerPixel * cellWidth;
- const uint32_t srcRowBytesY = gridSplitImage->yuvRowBytes[AVIF_CHAN_Y];
- cellImage->yuvPlanes[AVIF_CHAN_Y] =
- &gridSplitImage->yuvPlanes[AVIF_CHAN_Y][(gridX * bytesPerRowY) + (gridY * cellHeight) * srcRowBytesY];
- cellImage->yuvRowBytes[AVIF_CHAN_Y] = srcRowBytesY;
-
- if (gridSplitImage->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) {
- avifPixelFormatInfo info;
- avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &info);
-
- const uint32_t uvWidth = (cellWidth + info.chromaShiftX) >> info.chromaShiftX;
- const uint32_t uvHeight = (cellHeight + info.chromaShiftY) >> info.chromaShiftY;
- const uint32_t bytesPerRowUV = bytesPerPixel * uvWidth;
-
- const uint32_t srcRowBytesU = gridSplitImage->yuvRowBytes[AVIF_CHAN_U];
- cellImage->yuvPlanes[AVIF_CHAN_U] =
- &gridSplitImage->yuvPlanes[AVIF_CHAN_U][(gridX * bytesPerRowUV) + (gridY * uvHeight) * srcRowBytesU];
- cellImage->yuvRowBytes[AVIF_CHAN_U] = srcRowBytesU;
-
- const uint32_t srcRowBytesV = gridSplitImage->yuvRowBytes[AVIF_CHAN_V];
- cellImage->yuvPlanes[AVIF_CHAN_V] =
- &gridSplitImage->yuvPlanes[AVIF_CHAN_V][(gridX * bytesPerRowUV) + (gridY * uvHeight) * srcRowBytesV];
- cellImage->yuvRowBytes[AVIF_CHAN_V] = srcRowBytesV;
- }
-
- if (gridSplitImage->alphaPlane) {
- const uint32_t bytesPerRowA = bytesPerPixel * cellWidth;
- const uint32_t srcRowBytesA = gridSplitImage->alphaRowBytes;
- cellImage->alphaPlane = &gridSplitImage->alphaPlane[(gridX * bytesPerRowA) + (gridY * cellHeight) * srcRowBytesA];
- cellImage->alphaRowBytes = srcRowBytesA;
- }
}
}
return AVIF_TRUE;
@@ -1065,33 +1078,7 @@
returnCode = 1;
goto cleanup;
}
-
- // Verify that this cell's properties matches the first cell's properties
- if ((image->width != cellImage->width) || (image->height != cellImage->height)) {
- fprintf(stderr,
- "ERROR: Image grid dimensions mismatch, [%ux%u] vs [%ux%u]: %s\n",
- image->width,
- image->height,
- cellImage->width,
- cellImage->height,
- nextFile->filename);
- returnCode = 1;
- goto cleanup;
- }
- if (image->depth != cellImage->depth) {
- fprintf(stderr, "ERROR: Image grid depth mismatch, [%u] vs [%u]: %s\n", image->depth, cellImage->depth, nextFile->filename);
- returnCode = 1;
- goto cleanup;
- }
- if (image->yuvRange != cellImage->yuvRange) {
- fprintf(stderr,
- "ERROR: Image grid range mismatch, [%s] vs [%s]: %s\n",
- (image->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
- (nextImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
- nextFile->filename);
- returnCode = 1;
- goto cleanup;
- }
+ // Let avifEncoderAddImageGrid() verify the grid integrity (valid cell sizes, depths etc.).
}
if (gridCellIndex == 0) {
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e7c3635..5964fea 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -140,6 +140,9 @@
add_test(NAME test_cmd COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/data
)
+ add_test(NAME test_cmd_grid COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_grid.sh ${CMAKE_BINARY_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/data
+ )
add_test(NAME test_cmd_lossless COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd_lossless.sh ${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/data
)
diff --git a/tests/test_cmd_grid.sh b/tests/test_cmd_grid.sh
new file mode 100755
index 0000000..a38b5ab
--- /dev/null
+++ b/tests/test_cmd_grid.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ------------------------------------------------------------------------------
+#
+# tests for command lines (grid)
+
+# Very verbose but useful for debugging.
+set -ex
+
+if [[ "$#" -ge 1 ]]; then
+ # eval so that the passed in directory can contain variables.
+ BINARY_DIR="$(eval echo "$1")"
+else
+ # Assume "tests" is the current directory.
+ BINARY_DIR="$(pwd)/.."
+fi
+if [[ "$#" -ge 2 ]]; then
+ TESTDATA_DIR="$(eval echo "$2")"
+else
+ TESTDATA_DIR="$(pwd)/data"
+fi
+if [[ "$#" -ge 3 ]]; then
+ TMP_DIR="$(eval echo "$3")"
+else
+ TMP_DIR="$(mktemp -d)"
+fi
+
+AVIFENC="${BINARY_DIR}/avifenc"
+AVIFDEC="${BINARY_DIR}/avifdec"
+ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
+
+# Input file paths.
+INPUT_PNG="${TESTDATA_DIR}/paris_icc_exif_xmp.png" # 403 x 302 px
+# Output file names.
+ENCODED_FILE="avif_test_cmd_grid_encoded.avif"
+DECODED_FILE="avif_test_cmd_grid_decoded.png"
+ENCODED_FILE_2x2="avif_test_cmd_grid_2x2_encoded.avif"
+DECODED_FILE_2x2="avif_test_cmd_grid_2x2_decoded.png"
+ENCODED_FILE_7x5="avif_test_cmd_grid_7x5_encoded.avif"
+DECODED_FILE_7x5="avif_test_cmd_grid_7x5_decoded.png"
+
+# Cleanup
+cleanup() {
+ pushd ${TMP_DIR}
+ rm -- "${ENCODED_FILE}" "${DECODED_FILE}" \
+ "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}" \
+ "${ENCODED_FILE_7x5}" "${DECODED_FILE_7x5}"
+ popd
+}
+trap cleanup EXIT
+
+pushd ${TMP_DIR}
+ echo "Testing basic grid"
+ "${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 2x2 -o "${ENCODED_FILE_2x2}"
+ "${AVIFDEC}" "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}"
+
+ echo "Testing max grid"
+ "${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 7x5 -o "${ENCODED_FILE_7x5}"
+ "${AVIFDEC}" "${ENCODED_FILE_7x5}" "${DECODED_FILE_7x5}"
+
+ # Lossy compression should give different results for different tiles.
+ "${ARE_IMAGES_EQUAL}" "${DECODED_FILE_2x2}" "${DECODED_FILE_7x5}" 1 && exit 1
+
+ echo "Testing grid patterns"
+ for GRID in 1x1 1x2 1x3 1x4 1x5 2x1 3x1 4x1 5x1 6x1 7x1; do
+ "${AVIFENC}" -s 10 "${INPUT_PNG}" --grid ${GRID} -o "${ENCODED_FILE}"
+ "${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
+ done
+
+ echo "Testing wrong grid arguments"
+ "${AVIFENC}" "${INPUT_PNG}" --grid 0x0 -o "${ENCODED_FILE}" && exit 1
+ "${AVIFENC}" "${INPUT_PNG}" --grid 8x5 -o "${ENCODED_FILE}" && exit 1
+ "${AVIFENC}" "${INPUT_PNG}" --grid 7x6 -o "${ENCODED_FILE}" && exit 1
+ "${AVIFENC}" "${INPUT_PNG}" --grid 4294967295x4294967295 -o "${ENCODED_FILE}" && exit 1
+ "${AVIFENC}" "${INPUT_PNG}" --grid 2147483647x2147483647 -o "${ENCODED_FILE}" && exit 1
+
+ echo "Testing valid grid arguments but invalid grid image dimensions for subsampled image"
+ "${AVIFENC}" "${INPUT_PNG}" --grid 2x2 --yuv 420 -o "${ENCODED_FILE}" && exit 1
+ # Even if there is a single tile in the odd dimension, it is forbidden.
+ "${AVIFENC}" "${INPUT_PNG}" --grid 1x2 --yuv 422 -o "${ENCODED_FILE}" && exit 1
+ # 1x1 is not a real grid.
+ "${AVIFENC}" "${INPUT_PNG}" --grid 1x1 --yuv 420 -o "${ENCODED_FILE}"
+popd
+
+exit 0