Skip to content

Commit 735de39

Browse files
committed
Improved consistency of XMP handling
1 parent ca55eb5 commit 735de39

File tree

12 files changed

+31
-53
lines changed

12 files changed

+31
-53
lines changed

Tests/test_file_jpeg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ def test_getxmp(self) -> None:
943943
):
944944
assert im.getxmp() == {}
945945
else:
946+
assert "xmp" in im.info
946947
xmp = im.getxmp()
947948

948949
description = xmp["xmpmeta"]["RDF"]["Description"]

Tests/test_file_png.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ def test_getxmp(self) -> None:
683683
):
684684
assert im.getxmp() == {}
685685
else:
686+
assert "xmp" in im.info
686687
xmp = im.getxmp()
687688

688689
description = xmp["xmpmeta"]["RDF"]["Description"]

Tests/test_file_tiff.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ def test_getxmp(self) -> None:
759759
):
760760
assert im.getxmp() == {}
761761
else:
762+
assert "xmp" in im.info
762763
xmp = im.getxmp()
763764

764765
description = xmp["xmpmeta"]["RDF"]["Description"]

Tests/test_file_webp_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def test_getxmp() -> None:
129129
):
130130
assert im.getxmp() == {}
131131
else:
132+
assert "xmp" in im.info
132133
assert (
133134
im.getxmp()["xmpmeta"]["xmptk"]
134135
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "

Tests/test_image.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,10 @@ def test_exif_hide_offsets(self) -> None:
897897
assert tag not in exif.get_ifd(0x8769)
898898
assert exif.get_ifd(0xA005)
899899

900+
def test_empty_xmp(self) -> None:
901+
with Image.open("Tests/images/hopper.gif") as im:
902+
assert im.getxmp() == {}
903+
900904
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
901905
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
902906
im = Image.new("RGB", size)

docs/reference/Image.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ This helps to get the bounding box coordinates of the input image::
197197
.. automethod:: PIL.Image.Image.getpalette
198198
.. automethod:: PIL.Image.Image.getpixel
199199
.. automethod:: PIL.Image.Image.getprojection
200+
.. automethod:: PIL.Image.Image.getxmp
200201
.. automethod:: PIL.Image.Image.histogram
201202
.. automethod:: PIL.Image.Image.paste
202203
.. automethod:: PIL.Image.Image.point

src/PIL/Image.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,7 +1439,14 @@ def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]:
14391439
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
14401440
return self.im.getextrema()
14411441

1442-
def _getxmp(self, xmp_tags):
1442+
def getxmp(self):
1443+
"""
1444+
Returns a dictionary containing the XMP tags.
1445+
Requires defusedxml to be installed.
1446+
1447+
:returns: XMP tags in a dictionary.
1448+
"""
1449+
14431450
def get_name(tag):
14441451
return re.sub("^{[^}]+}", "", tag)
14451452

@@ -1466,9 +1473,10 @@ def get_value(element):
14661473
if ElementTree is None:
14671474
warnings.warn("XMP data cannot be read without defusedxml dependency")
14681475
return {}
1469-
else:
1470-
root = ElementTree.fromstring(xmp_tags)
1471-
return {get_name(root.tag): get_value(root)}
1476+
if "xmp" not in self.info:
1477+
return {}
1478+
root = ElementTree.fromstring(self.info["xmp"])
1479+
return {get_name(root.tag): get_value(root)}
14721480

14731481
def getexif(self) -> Exif:
14741482
"""

src/PIL/ImageOps.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,9 +714,8 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
714714
r'tiff:Orientation="([0-9])"',
715715
r"<tiff:Orientation>([0-9])</tiff:Orientation>",
716716
):
717-
exif_image.info["XML:com.adobe.xmp"] = re.sub(
718-
pattern, "", exif_image.info["XML:com.adobe.xmp"]
719-
)
717+
for k in ("xmp", "XML:com.adobe.xmp"):
718+
exif_image.info[k] = re.sub(pattern, "", exif_image.info[k])
720719
if not in_place:
721720
return transposed_image
722721
elif not in_place:

src/PIL/JpegImagePlugin.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def APP(self, marker):
9494
else:
9595
self.info["exif"] = s
9696
self._exif_offset = self.fp.tell() - n + 6
97+
elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
98+
self.info["xmp"] = s.split(b"\x00")[1]
9799
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
98100
# extract FlashPix information (incomplete)
99101
self.info["flashpix"] = s # FIXME: value will change
@@ -499,21 +501,6 @@ def _getexif(self):
499501
def _getmp(self):
500502
return _getmp(self)
501503

502-
def getxmp(self):
503-
"""
504-
Returns a dictionary containing the XMP tags.
505-
Requires defusedxml to be installed.
506-
507-
:returns: XMP tags in a dictionary.
508-
"""
509-
510-
for segment, content in self.applist:
511-
if segment == "APP1":
512-
marker, xmp_tags = content.split(b"\x00")[:2]
513-
if marker == b"http://ns.adobe.com/xap/1.0/":
514-
return self._getxmp(xmp_tags)
515-
return {}
516-
517504

518505
def _getexif(self):
519506
if "exif" not in self.info:

src/PIL/PngImagePlugin.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,8 @@ def chunk_iTXt(self, pos: int, length: int) -> bytes:
615615
return s
616616

617617
self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
618+
if k == "XML:com.adobe.xmp":
619+
self.im_info["xmp"] = self.im_info[k]
618620
self.check_text_memory(len(v))
619621

620622
return s
@@ -1032,19 +1034,6 @@ def getexif(self):
10321034

10331035
return super().getexif()
10341036

1035-
def getxmp(self):
1036-
"""
1037-
Returns a dictionary containing the XMP tags.
1038-
Requires defusedxml to be installed.
1039-
1040-
:returns: XMP tags in a dictionary.
1041-
"""
1042-
return (
1043-
self._getxmp(self.info["XML:com.adobe.xmp"])
1044-
if "XML:com.adobe.xmp" in self.info
1045-
else {}
1046-
)
1047-
10481037

10491038
# --------------------------------------------------------------------
10501039
# PNG writer

0 commit comments

Comments
 (0)