diff --git a/Tests/test_image.py b/Tests/test_image.py index ab0b9e6b769..c895eeeaace 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -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'\n' + b'' + b'' + b'' + b"from-a" + b"from-b" + b"" + b"" + b'\n' + ) + + 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"] = ( diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index 45667d9ec07..4db2f496701 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -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 ============= diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3493df1e467..e7145c6fa40 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -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()}