diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3ee33d65f..64f509a95 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -805,6 +805,13 @@ class TestFileJpeg: # Assert the entire file has not been read assert 0 < buffer.max_pos < size + def test_getxmp(self): + with Image.open("Tests/images/xmp_test.jpg") as im: + xmp = im.getxmp() + + assert isinstance(xmp, dict) + assert xmp["Description"]["Version"] == "10.4" + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_image_getxmp.py b/Tests/test_image_getxmp.py deleted file mode 100644 index a684d6f0c..000000000 --- a/Tests/test_image_getxmp.py +++ /dev/null @@ -1,9 +0,0 @@ -from PIL import Image - - -def test_getxmp(): - with Image.open("Tests/images/xmp_test.jpg") as im: - xmp = im.getxmp() - - assert isinstance(xmp, dict) - assert xmp["Description"]["Version"] == "10.4" diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 920b77dc8..a90b272c5 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -112,6 +112,20 @@ separate histograms for each color channel, changing the tone of the image. The ``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram for all channels. +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + Security ======== diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 062fa9280..ebeaf3c74 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -528,7 +528,6 @@ class Image: self.readonly = 0 self.pyaccess = None self._exif = None - self._xmp = None def __getattr__(self, name): if name == "category": @@ -1340,27 +1339,6 @@ class Image: return self._exif - def getxmp(self): - """ - Returns a dictionary containing the xmp tags for a given image. - :returns: XMP tags in a dictionary. - """ - - if self._xmp is None: - self._xmp = {} - - for segment, content in self.applist: - if segment == "APP1": - marker, xmp_tags = content.rsplit(b"\x00", 1) - if marker == b"http://ns.adobe.com/xap/1.0/": - root = xml.etree.ElementTree.fromstring(xmp_tags) - for element in root.findall(".//"): - self._xmp[element.tag.split("}")[1]] = { - child.split("}")[1]: value - for child, value in element.attrib.items() - } - return self._xmp - def getim(self): """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ad260acbd..48e0de535 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -39,6 +39,7 @@ import subprocess import sys import tempfile import warnings +import xml.etree.ElementTree from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 @@ -358,6 +359,7 @@ class JpegImageFile(ImageFile.ImageFile): self.app = {} # compatibility self.applist = [] self.icclist = [] + self._xmp = None while True: @@ -474,6 +476,27 @@ class JpegImageFile(ImageFile.ImageFile): def _getmp(self): return _getmp(self) + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + :returns: XMP tags in a dictionary. + """ + + if self._xmp is None: + self._xmp = {} + + for segment, content in self.applist: + if segment == "APP1": + marker, xmp_tags = content.rsplit(b"\x00", 1) + if marker == b"http://ns.adobe.com/xap/1.0/": + root = xml.etree.ElementTree.fromstring(xmp_tags) + for element in root.findall(".//"): + self._xmp[element.tag.split("}")[1]] = { + child.split("}")[1]: value + for child, value in element.attrib.items() + } + return self._xmp + def _getexif(self): if "exif" not in self.info: