Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved consistency of XMP handling #8069

Merged
merged 4 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,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 @@ -909,6 +909,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 @@ -195,6 +195,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 @@ -1459,7 +1459,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: str) -> str:
return re.sub("^{[^}]+}", "", tag)

Expand All @@ -1486,9 +1493,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 @@ -95,6 +95,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 @@ -500,21 +502,6 @@ def _getexif(self) -> dict[str, Any] | None:
def _getmp(self):
return _getmp(self)

def getxmp(self) -> dict[str, Any]:
"""
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) -> dict[str, Any] | None:
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 @@ -624,6 +624,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 @@ -1053,19 +1055,6 @@ def getexif(self) -> Image.Exif:

return super().getexif()

def getxmp(self) -> dict[str, Any]:
"""
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 @@ -1197,6 +1197,10 @@
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 1203 in src/PIL/TiffImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/TiffImagePlugin.py#L1203

Added line #L1203 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 @@ -1207,15 +1211,6 @@
"""Return the current frame number"""
return self.__frame

def getxmp(self) -> dict[str, Any]:
"""
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 @@ -101,15 +101,6 @@ def _getexif(self) -> dict[str, Any] | None:
return None
return self.getexif()._get_merged_dict()

def getxmp(self) -> dict[str, Any]:
"""
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
Loading