Skip to content

Commit 4204ec7

Browse files
authored
Apply clean aperture crop when decoding to PNG or JPEG. (AOMediaCodec#3041)
AOMediaCodec#2427
1 parent 8281f2e commit 4204ec7

9 files changed

Lines changed: 155 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The changes are relative to the previous release, unless the baseline is specifi
2626
* Support Sample Transform derived image items with grid input image items.
2727
* Add --sato flag to avifdec to enable Sample Transforms support at decoding.
2828
* Add --grid option to avifgainmaputil.
29+
* Apply clean aperture crop when decoding to PNG or JPEG.
2930

3031
### Changed since 1.3.0
3132

apps/shared/avifjpeg.c

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,29 +1664,39 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int
16641664
cinfo.err = jpeg_std_error(&jerr);
16651665
jpeg_create_compress(&cinfo);
16661666

1667-
avifRGBImage rgb;
1668-
avifRGBImageSetDefaults(&rgb, avif);
1669-
rgb.format = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_RGB_FORMAT_GRAY : AVIF_RGB_FORMAT_RGB;
1670-
rgb.chromaUpsampling = chromaUpsampling;
1671-
rgb.depth = 8;
1672-
if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
1667+
avifRGBImage rgbData;
1668+
avifRGBImageSetDefaults(&rgbData, avif);
1669+
rgbData.format = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_RGB_FORMAT_GRAY : AVIF_RGB_FORMAT_RGB;
1670+
rgbData.chromaUpsampling = chromaUpsampling;
1671+
rgbData.depth = 8;
1672+
if (avifRGBImageAllocatePixels(&rgbData) != AVIF_RESULT_OK) {
16731673
fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename);
16741674
goto cleanup;
16751675
}
1676-
if (avifImageYUVToRGB(avif, &rgb) != AVIF_RESULT_OK) {
1676+
if (avifImageYUVToRGB(avif, &rgbData) != AVIF_RESULT_OK) {
16771677
fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename);
16781678
goto cleanup;
16791679
}
16801680

1681+
avifRGBImage rgbView = rgbData;
1682+
if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
1683+
avifCropRect cropRect;
1684+
avifDiagnostics diag;
1685+
if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
1686+
(cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
1687+
avifRGBImageSetViewRect(&rgbView, &rgbData, &cropRect);
1688+
}
1689+
}
1690+
16811691
f = fopen(outputFilename, "wb");
16821692
if (!f) {
16831693
fprintf(stderr, "Can't open JPEG file for write: %s\n", outputFilename);
16841694
goto cleanup;
16851695
}
16861696

16871697
jpeg_stdio_dest(&cinfo, f);
1688-
cinfo.image_width = avif->width;
1689-
cinfo.image_height = avif->height;
1698+
cinfo.image_width = rgbView.width;
1699+
cinfo.image_height = rgbView.height;
16901700
const avifBool isGray = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400;
16911701
cinfo.input_components = isGray ? 1 : 3;
16921702
cinfo.in_color_space = isGray ? JCS_GRAYSCALE : JCS_RGB;
@@ -1699,21 +1709,6 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int
16991709
write_icc_profile(&cinfo, avif->icc.data, (unsigned int)avif->icc.size);
17001710
}
17011711

1702-
if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
1703-
avifCropRect cropRect;
1704-
avifDiagnostics diag;
1705-
if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
1706-
(cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
1707-
// TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement.
1708-
fprintf(stderr,
1709-
"Warning: Clean Aperture values were ignored, the output image was NOT cropped to rectangle {%u,%u,%u,%u}\n",
1710-
cropRect.x,
1711-
cropRect.y,
1712-
cropRect.width,
1713-
cropRect.height);
1714-
}
1715-
}
1716-
17171712
if (avif->exif.data && (avif->exif.size > 0)) {
17181713
size_t exifTiffHeaderOffset;
17191714
avifResult result = avifGetExifTiffHeaderOffset(avif->exif.data, avif->exif.size, &exifTiffHeaderOffset);
@@ -1787,7 +1782,7 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int
17871782
}
17881783

17891784
while (cinfo.next_scanline < cinfo.image_height) {
1790-
row_pointer[0] = &rgb.pixels[cinfo.next_scanline * rgb.rowBytes];
1785+
row_pointer[0] = &rgbView.pixels[cinfo.next_scanline * rgbView.rowBytes];
17911786
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
17921787
}
17931788

@@ -1799,6 +1794,6 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int
17991794
fclose(f);
18001795
}
18011796
jpeg_destroy_compress(&cinfo);
1802-
avifRGBImageFreePixels(&rgb);
1797+
avifRGBImageFreePixels(&rgbData);
18031798
return ret;
18041799
}

apps/shared/avifpng.c

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -627,8 +627,8 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3
627627
png_bytep * volatile rowPointers = NULL;
628628
FILE * volatile f = NULL;
629629

630-
avifRGBImage rgb;
631-
memset(&rgb, 0, sizeof(avifRGBImage));
630+
avifRGBImage rgbData;
631+
memset(&rgbData, 0, sizeof(avifRGBImage));
632632

633633
volatile int rgbDepth = requestedDepth;
634634
if (rgbDepth == 0) {
@@ -651,39 +651,55 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3
651651
rgbDepth = 8;
652652
}
653653

654-
volatile avifBool monochrome8bit = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && !avif->alphaPlane && (avif->depth == 8) &&
655-
(rgbDepth == 8);
654+
volatile avifBool hasClap = avif->transformFlags & AVIF_TRANSFORM_CLAP;
655+
volatile avifBool copyYPlane = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && !avif->alphaPlane && (avif->depth == 8) &&
656+
(rgbDepth == 8) && !hasClap;
656657

657658
volatile int colorType;
658-
if (monochrome8bit) {
659+
if (copyYPlane) {
659660
colorType = PNG_COLOR_TYPE_GRAY;
660661
} else {
661-
avifRGBImageSetDefaults(&rgb, avif);
662-
rgb.depth = rgbDepth;
662+
avifRGBImageSetDefaults(&rgbData, avif);
663+
rgbData.depth = rgbDepth;
663664
if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && avif->alphaPlane) {
664665
colorType = PNG_COLOR_TYPE_GRAY_ALPHA;
665-
rgb.format = AVIF_RGB_FORMAT_GRAYA;
666+
rgbData.format = AVIF_RGB_FORMAT_GRAYA;
666667
} else if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && !avif->alphaPlane) {
667668
colorType = PNG_COLOR_TYPE_GRAY;
668-
rgb.format = AVIF_RGB_FORMAT_GRAY;
669+
rgbData.format = AVIF_RGB_FORMAT_GRAY;
669670
} else {
670-
rgb.chromaUpsampling = chromaUpsampling;
671+
rgbData.chromaUpsampling = chromaUpsampling;
671672
colorType = PNG_COLOR_TYPE_RGBA;
672673
if (avifImageIsOpaque(avif)) {
673674
colorType = PNG_COLOR_TYPE_RGB;
674-
rgb.format = AVIF_RGB_FORMAT_RGB;
675+
rgbData.format = AVIF_RGB_FORMAT_RGB;
675676
}
676677
}
677-
if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
678+
if (avifRGBImageAllocatePixels(&rgbData) != AVIF_RESULT_OK) {
678679
fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename);
679680
goto cleanup;
680681
}
681-
if (avifImageYUVToRGB(avif, &rgb) != AVIF_RESULT_OK) {
682+
if (avifImageYUVToRGB(avif, &rgbData) != AVIF_RESULT_OK) {
682683
fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename);
683684
goto cleanup;
684685
}
685686
}
686687

688+
volatile uint32_t width = avif->width;
689+
volatile uint32_t height = avif->height;
690+
691+
avifRGBImage rgbView = rgbData;
692+
if (hasClap) {
693+
avifCropRect cropRect;
694+
avifDiagnostics diag;
695+
if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
696+
(cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
697+
avifRGBImageSetViewRect(&rgbView, &rgbData, &cropRect);
698+
width = cropRect.width;
699+
height = cropRect.height;
700+
}
701+
}
702+
687703
f = fopen(outputFilename, "wb");
688704
if (!f) {
689705
fprintf(stderr, "Can't open PNG file for write: %s\n", outputFilename);
@@ -721,7 +737,7 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3
721737
png_set_compression_level(png, compressionLevel);
722738
}
723739

724-
png_set_IHDR(png, info, avif->width, avif->height, rgbDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
740+
png_set_IHDR(png, info, width, height, rgbDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
725741

726742
const avifBool hasIcc = avif->icc.data && (avif->icc.size > 0);
727743
if (hasIcc) {
@@ -811,39 +827,25 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3
811827
png_write_chunk(png, cicp, cicpData, 4);
812828
}
813829

814-
rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * avif->height);
830+
rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * height);
815831
if (rowPointers == NULL) {
816832
fprintf(stderr, "Error writing PNG: memory allocation failure");
817833
goto cleanup;
818834
}
819835
uint8_t * row;
820836
uint32_t rowBytes;
821-
if (monochrome8bit) {
837+
if (copyYPlane) {
822838
row = avif->yuvPlanes[AVIF_CHAN_Y];
823839
rowBytes = avif->yuvRowBytes[AVIF_CHAN_Y];
824840
} else {
825-
row = rgb.pixels;
826-
rowBytes = rgb.rowBytes;
841+
row = rgbView.pixels;
842+
rowBytes = rgbView.rowBytes;
827843
}
828-
for (uint32_t y = 0; y < avif->height; ++y) {
844+
for (uint32_t y = 0; y < height; ++y) {
829845
rowPointers[y] = row;
830846
row += rowBytes;
831847
}
832848

833-
if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
834-
avifCropRect cropRect;
835-
avifDiagnostics diag;
836-
if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
837-
(cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
838-
// TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement.
839-
fprintf(stderr,
840-
"Warning: Clean Aperture values were ignored, the output image was NOT cropped to rectangle {%u,%u,%u,%u}\n",
841-
cropRect.x,
842-
cropRect.y,
843-
cropRect.width,
844-
cropRect.height);
845-
}
846-
}
847849
if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
848850
// TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Rotate the samples.
849851
fprintf(stderr,
@@ -871,6 +873,6 @@ avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint3
871873
if (rowPointers) {
872874
free(rowPointers);
873875
}
874-
avifRGBImageFreePixels(&rgb);
876+
avifRGBImageFreePixels(&rgbData);
875877
return writeResult;
876878
}

apps/shared/avifutil.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,21 @@ avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols,
649649

650650
return AVIF_TRUE;
651651
}
652+
653+
void avifRGBImageSetViewRect(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifCropRect * cropRect)
654+
{
655+
memset(dstImage, 0, sizeof(avifRGBImage));
656+
dstImage->width = cropRect->width;
657+
dstImage->height = cropRect->height;
658+
dstImage->depth = srcImage->depth;
659+
dstImage->format = srcImage->format;
660+
dstImage->alphaPremultiplied = srcImage->alphaPremultiplied;
661+
dstImage->isFloat = srcImage->isFloat;
662+
const uint32_t channelCount = avifRGBFormatChannelCount(srcImage->format);
663+
const uint32_t bytesPerChannel = srcImage->depth <= 8 ? 1 : 2;
664+
const uint32_t bytesPerSample = srcImage->format == AVIF_RGB_FORMAT_RGB_565 ? 2 : channelCount * bytesPerChannel;
665+
// This should not overflow if cropRect is a valid crop of the image.
666+
const size_t offset = (size_t)cropRect->y * srcImage->rowBytes + (size_t)cropRect->x * bytesPerSample;
667+
dstImage->pixels = srcImage->pixels + offset;
668+
dstImage->rowBytes = srcImage->rowBytes;
669+
}

apps/shared/avifutil.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint
5959
// The returned cells must be destroyed with avifImageDestroy().
6060
avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells);
6161

62+
// Performs a shallow copy of a rectangular area of an RGB image. 'dstImage' does not own the pixel data.
63+
// Assumes that cropRect is a valid cropping rectangle for srcImage. This is true if it was obtained
64+
// using avifCropRectFromCleanApertureBox().
65+
void avifRGBImageSetViewRect(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifCropRect * cropRect);
66+
6267
// This structure holds any timing data coming from source (typically non-AVIF) inputs being fed
6368
// into avifenc. If either or both values are 0, the timing is "invalid" / sentinel and the values
6469
// should be ignored. This structure is used to override the timing defaults in avifenc when the

tests/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ if(AVIF_BUILD_APPS)
266266
add_cmd_test(test_cmd_progressive ${CMAKE_CURRENT_SOURCE_DIR}/data)
267267
add_cmd_test(test_cmd_stdin ${CMAKE_CURRENT_SOURCE_DIR}/data)
268268
add_cmd_test(test_cmd_targetsize ${CMAKE_CURRENT_SOURCE_DIR}/data)
269+
add_cmd_test(test_cmd_transform ${CMAKE_CURRENT_SOURCE_DIR}/data)
269270

270271
if(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
271272
add_cmd_test(test_cmd_avifgainmaputil ${CMAKE_CURRENT_SOURCE_DIR}/data)
@@ -375,7 +376,8 @@ if(AVIF_CODEC_AVM_ENABLED)
375376
if(AVIF_BUILD_APPS)
376377
# Disable all tests that use avifenc without explicitly setting --codec=avm.
377378
set_tests_properties(
378-
test_cmd test_cmd_animation test_cmd_grid test_cmd_stdin test_cmd_targetsize PROPERTIES DISABLED True
379+
test_cmd test_cmd_animation test_cmd_grid test_cmd_stdin test_cmd_targetsize test_cmd_transform
380+
PROPERTIES DISABLED True
379381
)
380382
endif()
381383
endif()

tests/gtest/are_images_equal.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ int main(int argc, char** argv) {
6060
if (argc == 4) {
6161
if (!avif::testutil::AreImagesEqual(*decoded[0], *decoded[1],
6262
ignore_alpha)) {
63+
auto psnr =
64+
avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha);
6365
std::cerr << "Images " << argv[1] << " and " << argv[2]
64-
<< " are different." << std::endl;
66+
<< " are different (PSNR: " << psnr << ")." << std::endl;
6567
return 1;
6668
}
6769
std::cout << "Images " << argv[1] << " and " << argv[2] << " are identical."
@@ -70,7 +72,8 @@ int main(int argc, char** argv) {
7072
auto psnr = avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha);
7173
if (psnr < std::stod(argv[4])) {
7274
std::cerr << "PSNR: " << psnr << ", images " << argv[1] << " and "
73-
<< argv[2] << " are not similar." << std::endl;
75+
<< argv[2] << " are not similar enough (threshold: " << argv[4]
76+
<< ")." << std::endl;
7477
return 1;
7578
}
7679
std::cout << "PSNR: " << psnr << ", images " << argv[1] << " and "

tests/test_cmd_icc_profile.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pushd ${TMP_DIR}
7575
popd
7676
exit 0
7777
fi
78+
"${IMAGEMAGICK}" --version
7879

7980
"${AVIFENC}" -s 8 -l "${INPUT_COLOR_PNG}" -o "${ENCODED_FILE}"
8081
# Old version of ImageMagick may not support reading ICC from AVIF.

tests/test_cmd_transform.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/bin/bash
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ------------------------------------------------------------------------------
16+
#
17+
# tests for command lines using image transforms (clap/irot/imir...)
18+
19+
source $(dirname "$0")/cmd_test_common.sh || exit
20+
21+
# Input file paths.
22+
INPUT_PNG="${TESTDATA_DIR}/paris_exif_xmp_icc.jpg"
23+
# Output file names.
24+
ENCODED_FILE="encoded.avif"
25+
ENCODED_FILE_CLAP="encoded_clap.avif"
26+
27+
# Cleanup
28+
cleanup() {
29+
rm -f -r "${TMP_DIR}"
30+
}
31+
trap cleanup EXIT
32+
33+
pushd ${TMP_DIR}
34+
# Encode/decode uncropped image
35+
# Some image magick versions may drop EXIF. Remove it preemptively to avoid
36+
# ARE_IMAGES_EQUAL failing because of EXIF mismatch.
37+
"${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" -o "${ENCODED_FILE}"
38+
"${AVIFDEC}" "${ENCODED_FILE}" "${ENCODED_FILE}.png"
39+
40+
# Encode with crop
41+
"${AVIFENC}" --ignore-exif -s 10 "${INPUT_PNG}" --crop 10,50,100,25 -o "${ENCODED_FILE_CLAP}"
42+
# Decode to PNG
43+
"${AVIFDEC}" "${ENCODED_FILE_CLAP}" "${ENCODED_FILE_CLAP}.png"
44+
# Decode to JPEG
45+
"${AVIFDEC}" "${ENCODED_FILE_CLAP}" -q 100 "${ENCODED_FILE_CLAP}.jpg"
46+
47+
if command -v magick &> /dev/null
48+
then
49+
IMAGEMAGICK="magick"
50+
elif command -v convert &> /dev/null
51+
then
52+
IMAGEMAGICK="convert"
53+
else
54+
echo Missing ImageMagick, test skipped
55+
popd
56+
exit 0
57+
fi
58+
"${IMAGEMAGICK}" --version
59+
60+
"${IMAGEMAGICK}" "${ENCODED_FILE}.png" -crop 100x25+10+50 "${ENCODED_FILE}_cropped.png"
61+
"${ARE_IMAGES_EQUAL}" "${ENCODED_FILE}_cropped.png" "${ENCODED_FILE_CLAP}.png" 0
62+
"${ARE_IMAGES_EQUAL}" "${ENCODED_FILE}_cropped.png" "${ENCODED_FILE_CLAP}.jpg" 0 49
63+
popd
64+
65+
exit 0

0 commit comments

Comments
 (0)