Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 31 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,37 @@ def test_empty_xmp(self) -> None:
xmp = im.getxmp()
assert xmp == {}

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"?>\x00\x00 '
)
if ElementTree is None:
pytest.skip("defusedxml is not installed")

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

a_namespace = "http://example.com/ns/a/"
b_namespace = "http://example.com/ns/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[f"{{{a_namespace}}}id"] == "from-a"
assert desc_full[f"{{{b_namespace}}}id"] == "from-b"
Comment thread
radarhere marked this conversation as resolved.
Outdated

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``. By default, this remains ``True``, stripping each tag's XML
Comment thread
radarhere marked this conversation as resolved.
Outdated
namespace prefix as before. If set to ``False``, each tag's full
Comment thread
radarhere marked this conversation as resolved.
Outdated
``{namespace-uri}local-name`` form is kept instead, avoiding collisions between tags
that share a local name across different namespaces.

Other changes
=============
Expand Down
7 changes: 5 additions & 2 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,16 +1570,19 @@ 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)
return re.sub("^{[^}]+}", "", tag) if strip_namespaces else tag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd only check the (constant-in-this-function) option once instead of every tag?

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks!


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