Commit 0586c686f for imagemagick.org
commit 0586c686f84c8f9e22ed7d03e0dadb38efe76316
Author: Greg B <64932474+gregbenz@users.noreply.github.com>
Date: Fri Sep 4 17:11:14 2026 -0500
Preserve EXIF dimensions during transforms (#8937)
diff --git a/MagickCore/profile.c b/MagickCore/profile.c
index 38833b37c..12f28dd6a 100644
--- a/MagickCore/profile.c
+++ b/MagickCore/profile.c
@@ -2302,14 +2302,36 @@ static void WriteProfileShort(const EndianType endian,
(void) memcpy(p,buffer,2);
}
+static inline void WriteProfileDimension(const EndianType endian,
+ const size_t dimension,const ssize_t format,const int components,
+ unsigned char *entry,unsigned char *value)
+{
+ if ((components != 1) || ((format != 3) && (format != 4)))
+ return;
+ if ((format == 3) && (dimension <= MAGICK_USHORT_MAX))
+ {
+ WriteProfileShort(endian,(unsigned short) dimension,value);
+ return;
+ }
+ if (dimension > UINT32_MAX)
+ return;
+ if (format == 3)
+ WriteProfileShort(endian,4,entry+2);
+ WriteProfileLong(endian,dimension,value);
+}
+
static void SyncExifProfile(const Image *image,unsigned char *exif,
size_t length)
{
#define MaxDirectoryStack 16
#define EXIF_DELIMITER "\n"
#define EXIF_NUM_FORMATS 12
+#define TAG_IMAGE_WIDTH 0x0100
+#define TAG_IMAGE_LENGTH 0x0101
#define TAG_EXIF_OFFSET 0x8769
#define TAG_INTEROP_OFFSET 0xa005
+#define TAG_PIXEL_X_DIMENSION 0xa002
+#define TAG_PIXEL_Y_DIMENSION 0xa003
typedef struct _DirectoryInfo
{
@@ -2327,6 +2349,7 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
endian;
size_t
+ directory_length,
entry,
number_entries;
@@ -2342,7 +2365,9 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
format_bytes[] = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};
unsigned char
- *directory;
+ *directory,
+ *exif_directory,
+ *ifd0;
if (length < 16)
return;
@@ -2386,6 +2411,8 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
if ((offset < 0) || ((size_t) offset >= length))
return;
directory=exif+offset;
+ exif_directory=(unsigned char *) NULL;
+ ifd0=directory;
level=0;
entry=0;
exif_resources=NewSplayTree((int (*)(const void *,const void *)) NULL,
@@ -2403,7 +2430,13 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
/*
Determine how many entries there are in the current IFD.
*/
- number_entries=(size_t) ReadProfileShort(endian,directory);
+ directory_length=length-(size_t) (directory-exif);
+ if (directory_length < 6)
+ continue;
+ number_entries=(size_t) (unsigned short) ReadProfileShort(endian,
+ directory);
+ if (number_entries > ((directory_length-6)/12))
+ continue;
for ( ; entry < number_entries; entry++)
{
int
@@ -2417,7 +2450,9 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
number_bytes;
ssize_t
- format,
+ format;
+
+ unsigned short
tag_value;
q=(unsigned char *) (directory+2+(12*entry));
@@ -2426,7 +2461,7 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
if (GetValueFromSplayTree(exif_resources,q) == q)
break;
(void) AddValueToSplayTree(exif_resources,q,q);
- tag_value=(ssize_t) ReadProfileShort(endian,q);
+ tag_value=(unsigned short) ReadProfileShort(endian,q);
format=(ssize_t) ReadProfileShort(endian,q+2);
if ((format < 0) || ((format-1) >= EXIF_NUM_FORMATS))
break;
@@ -2453,8 +2488,26 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
}
switch (tag_value)
{
+ case TAG_IMAGE_WIDTH:
+ case TAG_IMAGE_LENGTH:
+ {
+ if (directory == ifd0)
+ WriteProfileDimension(endian,tag_value == TAG_IMAGE_WIDTH ?
+ image->columns : image->rows,format,components,q,p);
+ break;
+ }
+ case TAG_PIXEL_X_DIMENSION:
+ case TAG_PIXEL_Y_DIMENSION:
+ {
+ if (directory == exif_directory)
+ WriteProfileDimension(endian,tag_value == TAG_PIXEL_X_DIMENSION ?
+ image->columns : image->rows,format,components,q,p);
+ break;
+ }
case 0x011a:
{
+ if (directory != ifd0)
+ break;
(void) WriteProfileLong(endian,(size_t) (image->resolution.x+0.5),p);
if (number_bytes == 8)
(void) WriteProfileLong(endian,1UL,p+4);
@@ -2462,6 +2515,8 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
}
case 0x011b:
{
+ if (directory != ifd0)
+ break;
(void) WriteProfileLong(endian,(size_t) (image->resolution.y+0.5),p);
if (number_bytes == 8)
(void) WriteProfileLong(endian,1UL,p+4);
@@ -2469,6 +2524,8 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
}
case 0x0112:
{
+ if (directory != ifd0)
+ break;
if (number_bytes == 4)
{
(void) WriteProfileLong(endian,(size_t) image->orientation,p);
@@ -2480,6 +2537,8 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
}
case 0x0128:
{
+ if (directory != ifd0)
+ break;
if (number_bytes == 4)
{
(void) WriteProfileLong(endian,((size_t) image->units)+1,p);
@@ -2491,11 +2550,16 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
default:
break;
}
- if ((tag_value == TAG_EXIF_OFFSET) || (tag_value == TAG_INTEROP_OFFSET))
+ if (((tag_value == TAG_EXIF_OFFSET) ||
+ (tag_value == TAG_INTEROP_OFFSET)) && (format == 4) &&
+ (components == 1) && (number_bytes == 4))
{
offset=(ssize_t) ReadProfileLong(endian,p);
- if (((size_t) offset < length) && (level < (MaxDirectoryStack-2)))
+ if (((size_t) offset < (length-1)) &&
+ (level < (MaxDirectoryStack-2)))
{
+ if ((directory == ifd0) && (tag_value == TAG_EXIF_OFFSET))
+ exif_directory=exif+offset;
directory_stack[level].directory=directory;
entry++;
directory_stack[level].entry=entry;
@@ -2507,15 +2571,15 @@ static void SyncExifProfile(const Image *image,unsigned char *exif,
break;
offset=(ssize_t) ReadProfileLong(endian,directory+2+(12*
number_entries));
- if ((offset != 0) && ((size_t) offset < length) &&
+ if ((offset != 0) && ((size_t) offset < (length-1)) &&
(level < (MaxDirectoryStack-2)))
{
directory_stack[level].directory=exif+offset;
directory_stack[level].entry=0;
level++;
}
+ break;
}
- break;
}
}
} while (level > 0);
diff --git a/Makefile.in b/Makefile.in
index 142ed32ad..d7de93569 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -6265,6 +6265,7 @@ tests_wandtest_LDADD = $(MAGICKCORE_LIBS) $(MAGICKWAND_LIBS)
TESTS_XFAIL_TESTS =
TESTS_TESTS = \
tests/cli-colorspace.tap \
+ tests/cli-exif.tap \
tests/cli-heic.tap \
tests/cli-pcx.tap \
tests/cli-pipe.tap \
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 7a81b7d31..c9e547caa 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -39,6 +39,7 @@ TESTS_XFAIL_TESTS =
TESTS_TESTS = \
tests/cli-colorspace.tap \
+ tests/cli-exif.tap \
tests/cli-heic.tap \
tests/cli-pcx.tap \
tests/cli-pipe.tap \
diff --git a/tests/cli-exif.tap b/tests/cli-exif.tap
new file mode 100755
index 000000000..6a923fd06
--- /dev/null
+++ b/tests/cli-exif.tap
@@ -0,0 +1,194 @@
+#!/bin/sh
+#
+# Copyright 1999 ImageMagick Studio LLC, a non-profit organization
+# dedicated to making software imaging solutions freely available.
+#
+# You may not use this file except in compliance with the License. You may
+# obtain a copy of the License at
+#
+# https://imagemagick.org/license/
+#
+# 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.
+#
+# Regression tests for EXIF image dimensions.
+#
+. ./common.shi
+. ${srcdir}/tests/common.shi
+
+little_profile=cli-exif-little.exif
+big_profile=cli-exif-big.exif
+little_output=cli-exif-little.jpg
+big_output=cli-exif-big.jpg
+promotion_output=cli-exif-promotion.png
+malformed_profile=cli-exif-malformed.exif
+malformed_output=cli-exif-malformed.png
+malformed_thumbnail_profile=cli-exif-malformed-thumbnail.exif
+malformed_thumbnail_output=cli-exif-malformed-thumbnail.png
+jpeg_probe=cli-exif-probe.jpg
+png_probe=cli-exif-probe.png
+
+cleanup()
+{
+ rm -f "$little_profile" "$big_profile" "$little_output" "$big_output" \
+ "$promotion_output" "$malformed_profile" "$malformed_output" \
+ "$malformed_thumbnail_profile" "$malformed_thumbnail_output" \
+ "$jpeg_probe" "$png_probe"
+}
+
+write_profile()
+{
+ profile=$1
+ endian=$2
+
+ case "$endian" in
+ little)
+ # IFD0 uses SHORT dimensions, ExifIFD uses LONG dimensions, and IFD1
+ # contains unrelated thumbnail dimensions that must not be changed.
+ printf '\111\111\052\000\010\000\000\000\003\000\000\001\003\000\001\000\000\000\070\004\000\000\001\001\003\000\001\000\000\000\070\004\000\000\151\207\004\000\001\000\000\000\062\000\000\000\120\000\000\000\002\000\002\240\004\000\001\000\000\000\070\004\000\000\003\240\004\000\001\000\000\000\070\004\000\000\000\000\000\000\006\000\000\001\004\000\001\000\000\000\100\000\000\000\001\001\004\000\001\000\000\000\040\000\000\000\022\001\003\000\001\000\000\000\006\000\000\000\032\001\005\000\001\000\000\000\236\000\000\000\033\001\005\000\001\000\000\000\246\000\000\000\050\001\003\000\001\000\000\000\003\000\000\000\000\000\000\000\054\001\000\000\001\000\000\000\310\000\000\000\001\000\000\000' > "$profile"
+ ;;
+ big)
+ # Use the opposite field types to cover LONG dimensions in IFD0 and
+ # SHORT dimensions in ExifIFD.
+ printf '\115\115\000\052\000\000\000\010\000\003\001\000\000\004\000\000\000\001\000\000\004\070\001\001\000\004\000\000\000\001\000\000\004\070\207\151\000\004\000\000\000\001\000\000\000\062\000\000\000\120\000\002\240\002\000\003\000\000\000\001\004\070\000\000\240\003\000\003\000\000\000\001\004\070\000\000\000\000\000\000\000\002\001\000\000\004\000\000\000\001\000\000\000\100\001\001\000\004\000\000\000\001\000\000\000\040\000\000\000\000' > "$profile"
+ ;;
+ esac
+}
+
+check_value()
+{
+ description=$1
+ actual=$2
+ expected=$3
+
+ if [ "X$actual" = "X$expected" ]; then
+ echo "ok - $description"
+ else
+ echo "not ok - $description"
+ echo "# expected '$expected', got '$actual'"
+ fi
+}
+
+check_profile()
+{
+ profile=$1
+ output=$2
+ endian=$3
+
+ write_profile "$profile" "$endian"
+ if ${MAGICK} "${SRCDIR}/rose.pnm" -set profile "$profile" -resize 50% \
+ "$output" >/dev/null 2>&1; then
+ actual=`${IDENTIFY} -quiet -format '%wx%h' "$output" 2>/dev/null`
+ metadata=`${IDENTIFY} -quiet -format \
+ '%[EXIF:ImageWidth]x%[EXIF:ImageLength] %[EXIF:PixelXDimension]x%[EXIF:PixelYDimension]' \
+ "$output" 2>/dev/null`
+ thumbnail=`${IDENTIFY} -quiet -format \
+ '%[EXIF:Thumbnail.ImageWidth]x%[EXIF:Thumbnail.ImageLength]' \
+ "$output" 2>/dev/null`
+ if [ "$endian" = little ]; then
+ thumbnail_metadata=`${IDENTIFY} -quiet -format \
+ '%[EXIF:Thumbnail.Orientation] %[EXIF:Thumbnail.XResolution] %[EXIF:Thumbnail.YResolution] %[EXIF:Thumbnail.ResolutionUnit]' \
+ "$output" 2>/dev/null`
+ fi
+ else
+ actual=
+ metadata=
+ thumbnail=
+ thumbnail_metadata=
+ fi
+
+ check_value "$endian image dimensions" "$actual" "35x23"
+ check_value "$endian EXIF dimensions" "$metadata" "35x23 35x23"
+ check_value "$endian thumbnail dimensions" "$thumbnail" "64x32"
+ if [ "$endian" = little ]; then
+ check_value "little thumbnail metadata" "$thumbnail_metadata" \
+ "6 300/1 200/1 3"
+ fi
+}
+
+check_promotion()
+{
+ profile=$1
+ endian=$2
+
+ write_profile "$profile" "$endian"
+ if ${MAGICK} -size 65536x1 xc:black -set profile "$profile" \
+ "$promotion_output" >/dev/null 2>&1; then
+ metadata=`${IDENTIFY} -quiet -format \
+ '%[EXIF:ImageWidth]x%[EXIF:ImageLength] %[EXIF:PixelXDimension]x%[EXIF:PixelYDimension]' \
+ "$promotion_output" 2>/dev/null`
+ else
+ metadata=
+ fi
+ check_value "$endian SHORT promotion" "$metadata" "65536x1 65536x1"
+}
+
+check_malformed_profile()
+{
+ # The ExifIFD pointer targets the final byte. It must be ignored without
+ # disturbing valid IFD0 dimensions.
+ printf '\111\111\052\000\010\000\000\000\003\000\000\001\003\000\001\000\000\000\070\004\000\000\001\001\003\000\001\000\000\000\070\004\000\000\151\207\004\000\001\000\000\000\061\000\000\000\000\000\000\000' > "$malformed_profile"
+ if ${MAGICK} "${SRCDIR}/rose.pnm" -set profile "$malformed_profile" \
+ -resize 50% "$malformed_output" >/dev/null 2>&1; then
+ metadata=`${IDENTIFY} -quiet -format \
+ '%[EXIF:ImageWidth]x%[EXIF:ImageLength]' \
+ "$malformed_output" 2>/dev/null`
+ else
+ metadata=
+ fi
+ check_value "malformed ExifIFD pointer is ignored" "$metadata" "35x23"
+}
+
+check_malformed_thumbnail()
+{
+ # Keep IFD0 and ExifIFD complete, but truncate IFD1 immediately after its
+ # entry count. The valid primary dimensions should still be synchronized.
+ write_profile "$little_profile" little
+ dd if="$little_profile" of="$malformed_thumbnail_profile" bs=1 count=82 \
+ 2>/dev/null
+ if ${MAGICK} "${SRCDIR}/rose.pnm" \
+ -set profile "$malformed_thumbnail_profile" -resize 50% \
+ "$malformed_thumbnail_output" >/dev/null 2>&1; then
+ metadata=`${IDENTIFY} -quiet -format \
+ '%[EXIF:ImageWidth]x%[EXIF:ImageLength] %[EXIF:PixelXDimension]x%[EXIF:PixelYDimension]' \
+ "$malformed_thumbnail_output" 2>/dev/null`
+ else
+ metadata=
+ fi
+ check_value "valid dimensions before malformed IFD1" "$metadata" \
+ "35x23 35x23"
+}
+
+cleanup
+jpeg_format=
+if ${MAGICK} "${SRCDIR}/rose.pnm" "$jpeg_probe" >/dev/null 2>&1; then
+ jpeg_format=`${IDENTIFY} -quiet -format '%m' "$jpeg_probe" 2>/dev/null`
+fi
+if [ "X$jpeg_format" != "XJPEG" ]; then
+ echo "1..0 # SKIP JPEG coder unavailable"
+ cleanup
+ exit 0
+fi
+echo "1..11"
+check_profile "$little_profile" "$little_output" little
+check_profile "$big_profile" "$big_output" big
+png_format=
+if ${MAGICK} "${SRCDIR}/rose.pnm" "$png_probe" >/dev/null 2>&1; then
+ png_format=`${IDENTIFY} -quiet -format '%m' "$png_probe" 2>/dev/null`
+fi
+if [ "X$png_format" = "XPNG" ]; then
+ check_promotion "$little_profile" little
+ check_promotion "$big_profile" big
+ check_malformed_profile
+ check_malformed_thumbnail
+else
+ echo "ok - little SHORT promotion # SKIP PNG coder unavailable"
+ echo "ok - big SHORT promotion # SKIP PNG coder unavailable"
+ echo "ok - malformed ExifIFD pointer is ignored # SKIP PNG coder unavailable"
+ echo "ok - valid dimensions before malformed IFD1 # SKIP PNG coder unavailable"
+fi
+cleanup
+: