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
+: