Skip to content
Open
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
28 changes: 28 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,34 @@ def test_empty_xmp(self) -> None:
xmp = im.getxmp()
assert xmp == {}

@pytest.mark.skipif(ElementTree is None, reason="defusedxml is not installed")
def test_getxmp_strip_namespaces(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/">'
b'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">'
b'<rdf:Description rdf:about=""'
b' xmlns:a="http://example.com/ns/a/"'
b' xmlns:b="http://example.com/ns/b/">'
b"<a:id>from-a</a:id>"
b"<b:id>from-b</b:id>"
b"</rdf:Description>"
b"</rdf:RDF>"
b'</x:xmpmeta>\n<?xpacket end="w"?>'
)

stripped = im.getxmp()
desc = stripped["xmpmeta"]["RDF"]["Description"]
assert desc["id"] == ["from-a", "from-b"]

full = im.getxmp(strip_namespaces=False)
desc_full = full["{adobe:ns:meta/}xmpmeta"][
"{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF"
]["{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description"]
assert desc_full["{http://example.com/ns/a/}id"] == "from-a"
assert desc_full["{http://example.com/ns/b/}id"] == "from-b"

def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
Expand Down
12 changes: 8 additions & 4 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ TODO
API additions
=============

TODO
^^^^

TODO
Added ``strip_namespaces`` argument to ``Image.getxmp()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:py:meth:`~PIL.Image.Image.getxmp` now accepts an optional keyword argument of
``strip_namespaces``. It is set to ``True`` by default, stripping each tag's XML
namespace URI prefix as before. If set to ``False``, each tag's full
``{namespace-uri}local-name`` form is kept instead, avoiding collisions between tags
that share a local name across different namespaces.

Other changes
=============
Expand Down
16 changes: 13 additions & 3 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,16 +1570,26 @@ 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) -> dict[str, Any]:
def getxmp(self, *, strip_namespaces: bool = True) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.

:param strip_namespaces: If ``False``, keep each tag's full
``{namespace-uri}local-name`` form instead of stripping the namespace
prefix.
:returns: XMP tags in a dictionary.
"""

def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag)
if strip_namespaces:

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

else:

def get_name(tag: str) -> str:
return tag

def get_value(element: Element) -> str | dict[str, Any] | None:
value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()}
Expand Down