Apply rotation and mirroring when decoding to PNG or JPEG. (#3059)
#2427
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70de388..ed609cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,7 +26,7 @@
* Support Sample Transform derived image items with grid input image items.
* Add --sato flag to avifdec to enable Sample Transforms support at decoding.
* Add --grid option to avifgainmaputil.
-* Apply clean aperture crop when decoding to PNG or JPEG.
+* Apply clean aperture crop, rotation and mirror when decoding to PNG or JPEG.
### Changed since 1.3.0
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 4b6fe9a..5491186 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -1678,13 +1678,15 @@
goto cleanup;
}
- avifRGBImage rgbView = rgbData;
- if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
- avifCropRect cropRect;
- avifDiagnostics diag;
- if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
- (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
- avifRGBImageSetViewRect(&rgbView, &rgbData, &cropRect);
+ // rgbView is a view on rgbData. avifApplyTransforms() may modify rgbData.
+ avifRGBImage rgbView;
+ avifResult transformResult = avifApplyTransforms(&rgbView, &rgbData, avif);
+ if (transformResult != AVIF_RESULT_OK) {
+ if (transformResult == AVIF_RESULT_INVALID_ARGUMENT) {
+ fprintf(stderr, "Warning, ignoring invalid transforms (clap/irot/imir)\n");
+ } else {
+ fprintf(stderr, "Failed to apply transforms: %s\n", avifResultToString(transformResult));
+ goto cleanup;
}
}
@@ -1724,15 +1726,14 @@
}
memcpy(exif.data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH);
memcpy(exif.data + AVIF_JPEG_EXIF_HEADER_LENGTH, avif->exif.data + exifTiffHeaderOffset, avif->exif.size - exifTiffHeaderOffset);
- // Make sure the Exif orientation matches the irot/imir values.
- // libheif does not have the same behavior. The orientation is applied to samples and orientation data is discarded there,
- // see https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/encoder_jpeg.cc#L187
- const uint8_t orientation = avifImageGetExifOrientationFromIrotImir(avif);
- result = avifSetExifOrientation(&exif, orientation);
+ // We already rotated the pixels if necessary in avifApplyTransforms(), so we set the orientation to 1 (no rotation, no mirror).
+ result = avifSetExifOrientation(&exif, 1);
if (result != AVIF_RESULT_OK) {
- // Ignore errors if the orientation is the default one because not being able to set Exif orientation now
- // means a reader will not be able to parse it later either.
- if (orientation != 1) {
+ if (result == AVIF_RESULT_INVALID_EXIF_PAYLOAD || result == AVIF_RESULT_NOT_IMPLEMENTED) {
+ // Either the Exif is invalid, or it doesn't have an orientation field.
+ // If it's invalid, we can consider it as equivalent to not having an orientation.
+ // In both cases, we can ignore the error.
+ } else {
fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
avifRWDataFree(&exif);
goto cleanup;
@@ -1747,12 +1748,6 @@
}
jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, (unsigned int)remainingExif.size);
avifRWDataFree(&exif);
- } else if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
- // There is no Exif yet, but we need to store the orientation.
- // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Add a valid Exif payload or rotate the samples.
- fprintf(stderr,
- "Warning: Orientation %u was ignored, the output image was NOT rotated or mirrored\n",
- avifImageGetExifOrientationFromIrotImir(avif));
}
if (avif->xmp.data && (avif->xmp.size > 0)) {
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 46d278a..3e6b034 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -651,9 +651,9 @@
rgbDepth = 8;
}
- volatile avifBool hasClap = avif->transformFlags & AVIF_TRANSFORM_CLAP;
+ volatile avifBool hasTransforms = avif->transformFlags & (AVIF_TRANSFORM_CLAP | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
volatile avifBool copyYPlane = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && !avif->alphaPlane && (avif->depth == 8) &&
- (rgbDepth == 8) && !hasClap;
+ (rgbDepth == 8) && !hasTransforms;
volatile int colorType;
if (copyYPlane) {
@@ -685,18 +685,15 @@
}
}
- volatile uint32_t width = avif->width;
- volatile uint32_t height = avif->height;
-
+ // rgbView is a view on rgbData. avifApplyTransforms() may modify rgbData.
avifRGBImage rgbView = rgbData;
- if (hasClap) {
- avifCropRect cropRect;
- avifDiagnostics diag;
- if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
- (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
- avifRGBImageSetViewRect(&rgbView, &rgbData, &cropRect);
- width = cropRect.width;
- height = cropRect.height;
+ avifResult transformResult = avifApplyTransforms(&rgbView, &rgbData, avif);
+ if (transformResult != AVIF_RESULT_OK) {
+ if (transformResult == AVIF_RESULT_INVALID_ARGUMENT) {
+ fprintf(stderr, "Warning, ignoring invalid transforms (clap/irot/imir)\n");
+ } else {
+ fprintf(stderr, "Failed to apply transforms: %s\n", avifResultToString(transformResult));
+ goto cleanup;
}
}
@@ -737,6 +734,9 @@
png_set_compression_level(png, compressionLevel);
}
+ volatile uint32_t width = copyYPlane ? avif->width : rgbView.width;
+ volatile uint32_t height = copyYPlane ? avif->height : rgbView.height;
+
png_set_IHDR(png, info, width, height, rgbDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
const avifBool hasIcc = avif->icc.data && (avif->icc.size > 0);
@@ -846,13 +846,6 @@
row += rowBytes;
}
- if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
- // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Rotate the samples.
- fprintf(stderr,
- "Warning: Orientation %u was ignored, the output image was NOT rotated or mirrored\n",
- avifImageGetExifOrientationFromIrotImir(avif));
- }
-
if (rgbDepth > 8) {
png_set_swap(png);
}
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index e7261d2..5476b5e 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -659,11 +659,152 @@
dstImage->format = srcImage->format;
dstImage->alphaPremultiplied = srcImage->alphaPremultiplied;
dstImage->isFloat = srcImage->isFloat;
- const uint32_t channelCount = avifRGBFormatChannelCount(srcImage->format);
- const uint32_t bytesPerChannel = srcImage->depth <= 8 ? 1 : 2;
- const uint32_t bytesPerSample = srcImage->format == AVIF_RGB_FORMAT_RGB_565 ? 2 : channelCount * bytesPerChannel;
+ const uint32_t bytesPerPixel = avifRGBImagePixelSize(srcImage);
// This should not overflow if cropRect is a valid crop of the image.
- const size_t offset = (size_t)cropRect->y * srcImage->rowBytes + (size_t)cropRect->x * bytesPerSample;
+ const size_t offset = (size_t)cropRect->y * srcImage->rowBytes + (size_t)cropRect->x * bytesPerPixel;
dstImage->pixels = srcImage->pixels + offset;
dstImage->rowBytes = srcImage->rowBytes;
}
+
+// NOTE: this saves the rotated pixels to a different image. Rotating an image in place is possible, but can be non trivial depending on the angle.
+// A 90° rotation can be implemented as a transposition operation followed by mirroring.
+// It's the transposition step that is non trivial for non-square images, see https://en.wikipedia.org/wiki/In-place_matrix_transposition
+avifResult avifRGBImageRotate(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifImageRotation * rotation)
+{
+ const uint32_t bytesPerPixel = avifRGBImagePixelSize(srcImage);
+ const uint8_t angle = rotation->angle;
+ const uint32_t newWidth = (angle == 0 || angle == 2) ? srcImage->width : srcImage->height;
+ const uint32_t newHeight = (angle == 0 || angle == 2) ? srcImage->height : srcImage->width;
+ *dstImage = *srcImage;
+ dstImage->width = newWidth;
+ dstImage->height = newHeight;
+ dstImage->pixels = NULL;
+ avifResult result = avifRGBImageAllocatePixels(dstImage);
+ if (result != AVIF_RESULT_OK) {
+ return result;
+ }
+
+ if (rotation->angle == 0) {
+ const size_t bytesPerRow = (size_t)bytesPerPixel * srcImage->width;
+ // 0 degrees. Just copy the rows as is.
+ for (uint32_t j = 0; j < srcImage->height; ++j) {
+ memcpy(dstImage->pixels + ((size_t)j * dstImage->rowBytes), srcImage->pixels + ((size_t)j * srcImage->rowBytes), bytesPerRow);
+ }
+ } else if (rotation->angle == 1) {
+ // 90 degrees anti-clockwise.
+ for (uint32_t j = 0; j < srcImage->height; ++j) {
+ for (uint32_t i = 0; i < srcImage->width; ++i) {
+ // Source pixel at (i, j) goes to destination pixel at (j, srcImage->width - 1 - i).
+ memcpy(dstImage->pixels + ((size_t)(srcImage->width - 1 - i) * dstImage->rowBytes) + ((size_t)j * bytesPerPixel),
+ srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel),
+ bytesPerPixel);
+ }
+ }
+ } else if (rotation->angle == 2) {
+ // 180 degrees.
+ for (uint32_t j = 0; j < srcImage->height; ++j) {
+ for (uint32_t i = 0; i < srcImage->width; ++i) {
+ // Source pixel at (i, j) goes to destination pixel at (srcImage->width - 1 - i, srcImage->height - 1 - j).
+ memcpy(dstImage->pixels + ((size_t)(srcImage->height - 1 - j) * dstImage->rowBytes) +
+ ((size_t)(srcImage->width - 1 - i) * bytesPerPixel),
+ srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel),
+ bytesPerPixel);
+ }
+ }
+ } else if (rotation->angle == 3) {
+ // 90 degrees clockwise.
+ for (uint32_t j = 0; j < srcImage->height; ++j) {
+ for (uint32_t i = 0; i < srcImage->width; ++i) {
+ // Source pixel at (i, j) goes to destination pixel at (srcImage->width - 1 - i, j).
+ memcpy(dstImage->pixels + ((size_t)i * dstImage->rowBytes) + ((size_t)(srcImage->height - 1 - j) * bytesPerPixel),
+ srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel),
+ bytesPerPixel);
+ }
+ }
+ } else {
+ return AVIF_RESULT_INVALID_ARGUMENT; // Invalid angle.
+ }
+ return AVIF_RESULT_OK;
+}
+
+avifResult avifRGBImageMirror(avifRGBImage * image, const avifImageMirror * mirror)
+{
+ if (mirror->axis == 0) { // Horizontal axis.
+ const uint32_t bytesPerPixel = avifRGBImagePixelSize(image);
+ // May be less than image->rowBytes e.g. if image is a cropped view.
+ const size_t bytesPerRowToMove = (size_t)bytesPerPixel * image->width;
+ // Top-to-bottom
+ uint8_t * tempRow = (uint8_t *)avifAlloc(bytesPerRowToMove);
+ if (!tempRow) {
+ return AVIF_RESULT_OUT_OF_MEMORY;
+ }
+ for (uint32_t y = 0; y < image->height / 2; ++y) {
+ uint8_t * row1 = &image->pixels[(size_t)y * image->rowBytes];
+ uint8_t * row2 = &image->pixels[(size_t)(image->height - 1 - y) * image->rowBytes];
+ memcpy(tempRow, row1, bytesPerRowToMove);
+ memcpy(row1, row2, bytesPerRowToMove);
+ memcpy(row2, tempRow, bytesPerRowToMove);
+ }
+ avifFree(tempRow);
+ } else if (mirror->axis == 1) { // Vertical axis.
+ const uint32_t bytesPerPixel = avifRGBImagePixelSize(image);
+ uint8_t tempPixel[8]; // Max pixel size should be 8 bytes (RGBA 16-bit).
+ if (bytesPerPixel > sizeof(tempPixel)) {
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+ for (uint32_t y = 0; y < image->height; ++y) {
+ uint8_t * row = &image->pixels[(size_t)y * image->rowBytes];
+ for (uint32_t x = 0; x < image->width / 2; ++x) {
+ uint8_t * pixel1 = &row[(size_t)x * bytesPerPixel];
+ uint8_t * pixel2 = &row[(size_t)(image->width - 1 - x) * bytesPerPixel];
+ memcpy(tempPixel, pixel1, bytesPerPixel);
+ memcpy(pixel1, pixel2, bytesPerPixel);
+ memcpy(pixel2, tempPixel, bytesPerPixel);
+ }
+ }
+ } else {
+ return AVIF_RESULT_INVALID_ARGUMENT; // Invalid axis value.
+ }
+
+ return AVIF_RESULT_OK;
+}
+
+avifResult avifApplyTransforms(avifRGBImage * dstView, avifRGBImage * srcImage, const avifImage * avif)
+{
+ // ISO/IEC 23000-22 (MIAF), Section 7.3.6.7:
+ // These properties, if used, shall be indicated to be applied in the following order:
+ // clean aperture first, then rotation, then mirror.
+ *dstView = *srcImage;
+ if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
+ avifCropRect cropRect;
+ avifDiagnostics diag;
+ if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
+ (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
+ avifRGBImageSetViewRect(dstView, srcImage, &cropRect);
+ } else {
+ fprintf(stderr, "Invalid clean aperture box\n");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+ }
+ if (avif->transformFlags & AVIF_TRANSFORM_IROT && avif->irot.angle != 0) {
+ avifRGBImage tmpRgbImage;
+ avifResult result = avifRGBImageRotate(&tmpRgbImage, dstView, &avif->irot);
+ if (result != AVIF_RESULT_OK) {
+ fprintf(stderr, "Failed to apply rotation\n");
+ avifRGBImageFreePixels(&tmpRgbImage);
+ return result;
+ }
+ // We assume that srcImage owned its pixels and free them before replacing it with tmpRgbImage.
+ avifRGBImageFreePixels(srcImage);
+ *srcImage = tmpRgbImage;
+ *dstView = *srcImage;
+ }
+ if (avif->transformFlags & AVIF_TRANSFORM_IMIR) {
+ avifResult result = avifRGBImageMirror(dstView, &avif->imir);
+ if (result != AVIF_RESULT_OK) {
+ fprintf(stderr, "Failed to apply mirror\n");
+ return result;
+ }
+ }
+ return AVIF_RESULT_OK;
+}
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 3ea289e..eaafd07 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -64,6 +64,19 @@
// using avifCropRectFromCleanApertureBox().
void avifRGBImageSetViewRect(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifCropRect * cropRect);
+// Rotates srcImage into dstImage. The two pointers must be different (does not rotate in place).
+// Allocates the pixels of dstImage which must be freed by the caller.
+avifResult avifRGBImageRotate(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifImageRotation * rotation);
+
+// Mirrors the image in place.
+avifResult avifRGBImageMirror(avifRGBImage * image, const avifImageMirror * mirror);
+
+// Applies clap, irot and imir transforms (in this order) specified in the avif image to srcImage.
+// The result is in dstView which does not own its pixels.
+// srcImage may also have its pixel data modified. Assumes that srcImage DOES own its pixels.
+// If an error is returned, only some of the transforms may have been applied.
+avifResult avifApplyTransforms(avifRGBImage * dstView, avifRGBImage * srcImage, const avifImage * avif);
+
// This structure holds any timing data coming from source (typically non-AVIF) inputs being fed
// into avifenc. If either or both values are 0, the timing is "invalid" / sentinel and the values
// should be ignored. This structure is used to override the timing defaults in avifenc when the
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 56faf11..c7f60d1 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -265,26 +265,26 @@
EXPECT_EQ(decoded->irot.angle, 1u);
EXPECT_EQ(decoded->imir.axis, 0u);
- // Exif orientation is kept in JPEG export.
+ // JPEG: Exif orientation should be applied when saving and removed from Exif.
ImagePtr temp_image =
WriteAndReadImage(*image, "paris_exif_orientation_5.jpg");
ASSERT_NE(temp_image, nullptr);
- EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
- EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
- EXPECT_EQ(image->irot.angle, temp_image->irot.angle);
- EXPECT_EQ(image->imir.axis, temp_image->imir.axis);
- EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched.
+ EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+ EXPECT_EQ(temp_image->transformFlags, AVIF_TRANSFORM_NONE);
+ // Samples have been rotated.
+ EXPECT_EQ(image->width, temp_image->height);
+ EXPECT_EQ(image->height, temp_image->width);
- // Exif orientation in PNG export should be ignored or discarded.
+ // PNG: Exif orientation should be applied when saving and removed from Exif.
temp_image = WriteAndReadImage(*image, "paris_exif_orientation_5.png");
ASSERT_NE(temp_image, nullptr);
EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
EXPECT_EQ(
temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
avifTransformFlags{0});
- // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Fix orientation
- // not being applied to PNG samples.
- EXPECT_EQ(image->width, temp_image->width /* should be height here */);
+ // Samples have been rotated.
+ EXPECT_EQ(image->width, temp_image->height);
+ EXPECT_EQ(image->height, temp_image->width);
}
TEST(MetadataTest, AllExifOrientations) {
@@ -325,15 +325,16 @@
EXPECT_EQ(decoded->irot.angle, 0u);
EXPECT_EQ(decoded->imir.axis, image->imir.axis);
- // Exif orientation is set equivalent to irot/imir in JPEG export.
- // Existing Exif orientation is overwritten.
+ // Exif orientation is discarded (set to 1) and samples are rotated/mirrored
+ // according to irot/imir in JPEG export.
const ImagePtr temp_image =
WriteAndReadImage(*image, "paris_exif_orientation_2.jpg");
ASSERT_NE(temp_image, nullptr);
EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
- EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
- EXPECT_EQ(image->imir.axis, temp_image->imir.axis);
- EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched.
+ EXPECT_EQ(temp_image->transformFlags, AVIF_TRANSFORM_NONE);
+ // Samples have been mirrored but you can't really tell from the dimensions.
+ EXPECT_EQ(image->width, temp_image->width);
+ EXPECT_EQ(image->height, temp_image->height);
}
TEST(MetadataTest, RotatedJpegBecauseOfIrotImir) {
@@ -356,9 +357,9 @@
EXPECT_EQ(
temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
avifTransformFlags{0});
- // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Fix orientation
- // not being applied to JPEG samples.
- EXPECT_EQ(image->width, temp_image->width /* should be height here */);
+ // Samples have been rotated.
+ EXPECT_EQ(image->width, temp_image->height);
+ EXPECT_EQ(image->height, temp_image->width);
}
TEST(MetadataTest, ExifIfdOffsetLoopingTo8) {
diff --git a/tests/test_cmd_transform.sh b/tests/test_cmd_transform.sh
index 0f5617b..75d72af 100755
--- a/tests/test_cmd_transform.sh
+++ b/tests/test_cmd_transform.sh
@@ -22,7 +22,6 @@
INPUT_PNG="${TESTDATA_DIR}/paris_exif_xmp_icc.jpg"
# Output file names.
ENCODED_FILE="encoded.avif"
-ENCODED_FILE_CLAP="encoded_clap.avif"
# Cleanup
cleanup() {
@@ -37,12 +36,51 @@
"${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" -o "${ENCODED_FILE}"
"${AVIFDEC}" "${ENCODED_FILE}" "${ENCODED_FILE}.png"
- # Encode with crop
- "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --crop 10,50,100,25 -o "${ENCODED_FILE_CLAP}"
- # Decode to PNG
- "${AVIFDEC}" "${ENCODED_FILE_CLAP}" "${ENCODED_FILE_CLAP}.png"
- # Decode to JPEG
- "${AVIFDEC}" "${ENCODED_FILE_CLAP}" -q 100 "${ENCODED_FILE_CLAP}.jpg"
+ # Crop
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --crop 10,50,100,25 -o "encoded_clap.avif"
+ "${AVIFDEC}" "encoded_clap.avif" "encoded_clap.png"
+ "${AVIFDEC}" "encoded_clap.avif" -q 100 "encoded_clap.jpg"
+
+ # Rotation (0 degrees)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --irot 0 -o "encoded_irot0.avif"
+ "${AVIFDEC}" "encoded_irot0.avif" "encoded_irot0.png"
+ "${AVIFDEC}" "encoded_irot0.avif" -q 100 "encoded_irot0.jpg"
+
+ # Rotation (90 degrees anti-clockwise)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --irot 1 -o "encoded_irot1.avif"
+ "${AVIFDEC}" "encoded_irot1.avif" "encoded_irot1.png"
+ "${AVIFDEC}" "encoded_irot1.avif" -q 100 "encoded_irot1.jpg"
+
+ # Rotation (180 degrees)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --irot 2 -o "encoded_irot2.avif"
+ "${AVIFDEC}" "encoded_irot2.avif" "encoded_irot2.png"
+ "${AVIFDEC}" "encoded_irot2.avif" -q 100 "encoded_irot2.jpg"
+
+ # Rotation (90 clockwise)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --irot 3 -o "encoded_irot3.avif"
+ "${AVIFDEC}" "encoded_irot3.avif" "encoded_irot3.png"
+ "${AVIFDEC}" "encoded_irot3.avif" -q 100 "encoded_irot3.jpg"
+
+ # Mirror (top-to-bottom)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --imir 0 -o "encoded_imir0.avif"
+ "${AVIFDEC}" "encoded_imir0.avif" "encoded_imir0.png"
+ "${AVIFDEC}" "encoded_imir0.avif" -q 100 "encoded_imir0.jpg"
+
+ # Mirror (left-to-right)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --imir 1 -o "encoded_imir1.avif"
+ "${AVIFDEC}" "encoded_imir1.avif" "encoded_imir1.png"
+ "${AVIFDEC}" "encoded_imir1.avif" -q 100 "encoded_imir1.jpg"
+
+ # Combined transforms: crop + mirror (no rotation)
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --crop 10,50,100,25 --imir 0 -o "encoded_crop_imir0.avif"
+ "${AVIFDEC}" "encoded_crop_imir0.avif" "encoded_crop_imir0.png"
+ "${AVIFDEC}" "encoded_crop_imir0.avif" -q 100 "encoded_crop_imir0.jpg"
+
+ # Combined transforms: crop + rotation + mirror
+ # MIAF (ISO/IEC 23000-22) specifies the application order: 1. clap, 2. irot, 3. imir.
+ "${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --crop 10,50,100,25 --irot 1 --imir 1 -o "encoded_combined.avif"
+ "${AVIFDEC}" "encoded_combined.avif" "encoded_combined.png"
+ "${AVIFDEC}" "encoded_combined.avif" -q 100 "encoded_combined.jpg"
if command -v magick &> /dev/null
then
@@ -57,9 +95,53 @@
fi
"${IMAGEMAGICK}" --version
- "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -crop 100x25+10+50 "${ENCODED_FILE}_cropped.png"
- "${ARE_IMAGES_EQUAL}" "${ENCODED_FILE}_cropped.png" "${ENCODED_FILE_CLAP}.png" 0
- "${ARE_IMAGES_EQUAL}" "${ENCODED_FILE}_cropped.png" "${ENCODED_FILE_CLAP}.jpg" 0 49
+ JPEG_PSNR_THRESHOLD=48
+
+ # Crop
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -crop 100x25+10+50 "encoded_clap_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_clap_ref.png" "encoded_clap.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_clap_ref.png" "encoded_clap.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Rotation (0 degrees)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -rotate 0 "encoded_irot0_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_irot0_ref.png" "encoded_irot0.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_irot0_ref.png" "encoded_irot0.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Rotation (90 degrees anti-clockwise)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -rotate -90 "encoded_irot1_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_irot1_ref.png" "encoded_irot1.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_irot1_ref.png" "encoded_irot1.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Rotation (180 degrees)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -rotate 180 "encoded_irot2_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_irot2_ref.png" "encoded_irot2.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_irot2_ref.png" "encoded_irot2.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Rotation (90 clockwise)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -rotate 90 "encoded_irot3_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_irot3_ref.png" "encoded_irot3.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_irot3_ref.png" "encoded_irot3.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Mirror (top-to-bottom)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -flip "encoded_imir0_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_imir0_ref.png" "encoded_imir0.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_imir0_ref.png" "encoded_imir0.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Mirror (left-to-right)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -flop "encoded_imir1_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_imir1_ref.png" "encoded_imir1.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_imir1_ref.png" "encoded_imir1.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Combined transforms: crop + mirror (no rotation)
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -crop 100x25+10+50 -flip "encoded_crop_imir0_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_crop_imir0_ref.png" "encoded_crop_imir0.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_crop_imir0_ref.png" "encoded_crop_imir0.jpg" 0 ${JPEG_PSNR_THRESHOLD}
+
+ # Combined transforms: crop + rotation + mirror
+ # MIAF (ISO/IEC 23000-22) specifies the application order: 1. clap, 2. irot, 3. imir.
+ "${IMAGEMAGICK}" "${ENCODED_FILE}.png" -crop 100x25+10+50 -rotate -90 -flop "encoded_combined_ref.png"
+ "${ARE_IMAGES_EQUAL}" "encoded_combined_ref.png" "encoded_combined.png" 0
+ "${ARE_IMAGES_EQUAL}" "encoded_combined_ref.png" "encoded_combined.jpg" 0 ${JPEG_PSNR_THRESHOLD}
popd
exit 0