From 6cb16d9408ea10d8496582acfdeac3a32640e4c3 Mon Sep 17 00:00:00 2001 From: GreedyC Date: Sat, 26 Sep 2026 06:22:34 +0300 Subject: [PATCH] Handle XML whitespace and single quotes in XMP orientation --- Tests/test_imageops.py | 50 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/13.0.0.rst | 9 +++++++ src/PIL/Image.py | 6 ++--- src/PIL/ImageOps.py | 4 +-- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 4a4826d22e3..9557e02fd61 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -484,6 +484,56 @@ def check(orientation_im: Image.Image) -> None: assert 0x0112 not in transposed_im.getexif() +@pytest.mark.parametrize("info_key", ["XML:com.adobe.xmp", "xmp"]) +@pytest.mark.parametrize("in_place", [False, True]) +@pytest.mark.parametrize( + "orientation", + [ + 'tiff:Orientation="6"', + "tiff:Orientation='6'", + 'tiff:Orientation = "6"', + "tiff:Orientation\n=\t'6'", + 'tiff:Orientation=" 6 "', + "6", + "\n 6\t", + "6", + ], +) +def test_exif_transpose_xmp_xml_syntax( + info_key: str, in_place: bool, orientation: str +) -> None: + im = Image.new("RGB", (2, 1)) + im.putpixel((0, 0), (255, 0, 0)) + im.putpixel((1, 0), (0, 0, 255)) + expected = im.transpose(Image.Transpose.ROTATE_270) + namespaces = ( + 'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ' + 'xmlns:tiff="http://ns.adobe.com/tiff/1.0/"' + ) + if orientation.startswith("<"): + xmp = ( + f"" + f"{orientation}" + ) + else: + xmp = f"" + im.info[info_key] = xmp if info_key == "XML:com.adobe.xmp" else xmp.encode() + + assert im.getexif()[0x0112] == 6 + if in_place: + ImageOps.exif_transpose(im, in_place=True) + transposed = im + else: + transposed = ImageOps.exif_transpose(im) + assert im.size == (2, 1) + assert im.getexif()[0x0112] == 6 + assert_image_equal(transposed, expected) + assert "tiff:Orientation" not in str(transposed.info[info_key]) + transposed._reload_exif() + assert 0x0112 not in transposed.getexif() + assert_image_equal(ImageOps.exif_transpose(transposed), expected) + + def test_exif_transpose_with_xmp_tuple() -> None: with Image.open("Tests/images/xmp_tags_orientation.png") as im: assert im.getexif()[0x0112] == 3 diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 694093b01ab..6a7255f4bca 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -99,6 +99,15 @@ intended for internal use, so there is no replacement. API changes =========== +XMP orientation whitespace and quotation marks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getexif` now reads XMP orientation attributes +with single quotes or whitespace around the equals sign and orientation value. +Whitespace around an orientation element's value is also supported. +:py:func:`~PIL.ImageOps.exif_transpose` removes these forms after transposition, +so reloading the metadata does not cause the image to be transposed again. + TODO ^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 873de8607bf..50cfc9e063d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1663,13 +1663,13 @@ def getexif(self) -> Exif: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") - pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' + pattern: str | bytes = r"tiff:Orientation\s*(?:=\s*[\"']\s*|>\s*)([0-9])" if not xmp_tags and (xmp_tags := self.info.get("xmp")): - pattern = rb'tiff:Orientation(="|>)([0-9])' + pattern = rb"tiff:Orientation\s*(?:=\s*[\"']\s*|>\s*)([0-9])" if xmp_tags: match = re.search(pattern, xmp_tags) if match: - self._exif[ExifTags.Base.Orientation] = int(match[2]) + self._exif[ExifTags.Base.Orientation] = int(match[1]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 593f801b031..6407e3b579f 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -750,8 +750,8 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image for key in ("XML:com.adobe.xmp", "xmp"): if key in exif_image.info: for pattern in ( - r'tiff:Orientation="([0-9])"', - r"([0-9])", + r"tiff:Orientation\s*=\s*([\"'])\s*[0-9]\s*\1", + r"\s*[0-9]\s*", ): value = exif_image.info[key] if isinstance(value, str):