Skip to content

Commit

Permalink
Improved consistency of XMP handling
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed May 20, 2024
1 parent ca55eb5 commit 0f1a0fc
Show file tree
Hide file tree
Showing 13 changed files with 35 additions and 53 deletions.
1 change: 1 addition & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ def test_getxmp(self) -> None:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()

description = xmp["xmpmeta"]["RDF"]["Description"]
Expand Down
1 change: 1 addition & 0 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ def test_getxmp(self) -> None:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()

description = xmp["xmpmeta"]["RDF"]["Description"]
Expand Down
1 change: 1 addition & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ def test_getxmp(self) -> None:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()

description = xmp["xmpmeta"]["RDF"]["Description"]
Expand Down
1 change: 1 addition & 0 deletions Tests/test_file_webp_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def test_getxmp() -> None:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
assert (
im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
Expand Down
4 changes: 4 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,10 @@ def test_exif_hide_offsets(self) -> None:
assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005)

def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}

@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
Expand Down
1 change: 1 addition & 0 deletions docs/reference/Image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ This helps to get the bounding box coordinates of the input image::
.. automethod:: PIL.Image.Image.getpalette
.. automethod:: PIL.Image.Image.getpixel
.. automethod:: PIL.Image.Image.getprojection
.. automethod:: PIL.Image.Image.getxmp
.. automethod:: PIL.Image.Image.histogram
.. automethod:: PIL.Image.Image.paste
.. automethod:: PIL.Image.Image.point
Expand Down
6 changes: 3 additions & 3 deletions docs/releasenotes/8.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ is not secure.

- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
will now use ``defusedxml`` instead. If the dependency is not present, an empty
dictionary will be returned and a warning raised.
- ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
empty dictionary will be returned and a warning raised.

Deprecations
============
Expand Down
16 changes: 12 additions & 4 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,14 @@ def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]:
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()

def _getxmp(self, xmp_tags):
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""

def get_name(tag):
return re.sub("^{[^}]+}", "", tag)

Expand All @@ -1466,9 +1473,10 @@ def get_value(element):
if ElementTree is None:
warnings.warn("XMP data cannot be read without defusedxml dependency")
return {}
else:
root = ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)}
if "xmp" not in self.info:
return {}
root = ElementTree.fromstring(self.info["xmp"])
return {get_name(root.tag): get_value(root)}

def getexif(self) -> Exif:
"""
Expand Down
3 changes: 3 additions & 0 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
exif_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", exif_image.info["XML:com.adobe.xmp"]
)
exif_image.info["xmp"] = re.sub(
pattern.encode(), b"", exif_image.info["xmp"]
)
if not in_place:
return transposed_image
elif not in_place:
Expand Down
17 changes: 2 additions & 15 deletions src/PIL/JpegImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ def APP(self, marker):
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
self.info["xmp"] = s.split(b"\x00")[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
Expand Down Expand Up @@ -499,21 +501,6 @@ def _getexif(self):
def _getmp(self):
return _getmp(self)

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""

for segment, content in self.applist:
if segment == "APP1":
marker, xmp_tags = content.split(b"\x00")[:2]
if marker == b"http://ns.adobe.com/xap/1.0/":
return self._getxmp(xmp_tags)
return {}


def _getexif(self):
if "exif" not in self.info:
Expand Down
15 changes: 2 additions & 13 deletions src/PIL/PngImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,8 @@ def chunk_iTXt(self, pos: int, length: int) -> bytes:
return s
else:
return s
if k == b"XML:com.adobe.xmp":
self.im_info["xmp"] = v
try:
k = k.decode("latin-1", "strict")
lang = lang.decode("utf-8", "strict")
Expand Down Expand Up @@ -1032,19 +1034,6 @@ def getexif(self):

return super().getexif()

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return (
self._getxmp(self.info["XML:com.adobe.xmp"])
if "XML:com.adobe.xmp" in self.info
else {}
)


# --------------------------------------------------------------------
# PNG writer
Expand Down
13 changes: 4 additions & 9 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,10 @@ def _seek(self, frame: int) -> None:
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
if XMP in self.tag_v2:
self.info["xmp"] = self.tag_v2[XMP]
elif "xmp" in self.info:
del self.info["xmp"]

Check warning on line 1198 in src/PIL/TiffImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/TiffImagePlugin.py#L1198

Added line #L1198 was not covered by tests
self._reload_exif()
# fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
Expand All @@ -1202,15 +1206,6 @@ def tell(self) -> int:
"""Return the current frame number"""
return self.__frame

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}

def get_photoshop_blocks(self):
"""
Returns a dictionary of Photoshop "Image Resource Blocks".
Expand Down
9 changes: 0 additions & 9 deletions src/PIL/WebPImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,6 @@ def _getexif(self):
return None
return self.getexif()._get_merged_dict()

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}

def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
Expand Down

0 comments on commit 0f1a0fc

Please sign in to comment.