From c1fbe2d975e91060fddbed0e71bd0818602b10f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Apr 2021 14:07:09 +1000 Subject: [PATCH 1/3] Corrected getxmp() descending into XML --- Tests/test_file_jpeg.py | 11 ++++++++++- src/PIL/JpegImagePlugin.py | 27 ++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eb566e687..f4c295e0d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -828,7 +828,16 @@ class TestFileJpeg: xmp = im.getxmp() assert isinstance(xmp, dict) - assert xmp["Description"]["Version"] == "10.4" + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["DerivedFrom"] is None + assert ( + description["Look"]["Description"]["Group"]["Alt"]["li"] == "Profiles" + ) + assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] + + # Attribute + assert description["Version"] == "10.4" @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6d4bc70c5..a545665b4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -492,12 +492,29 @@ class JpegImageFile(ImageFile.ImageFile): if segment == "APP1": marker, xmp_tags = content.rsplit(b"\x00", 1) if marker == b"http://ns.adobe.com/xap/1.0/": + + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + children = list(element) + if children: + value = {get_name(k): v for k, v in element.attrib.items()} + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + return value + else: + return element.text + 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() - } + self._xmp[get_name(root.tag)] = get_value(root) return self._xmp From ae3bdf87f0d62de786567b880916510b3b6addef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jun 2021 12:17:38 +1000 Subject: [PATCH 2/3] Added getxmp() for TIFF --- Tests/test_file_tiff.py | 10 ++++++++++ src/PIL/ImageFile.py | 25 +++++++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 25 +------------------------ src/PIL/TiffImagePlugin.py | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 531b983ae..65fdacf72 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -600,6 +600,16 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info + def test_xmp(self): + with Image.open("Tests/images/lab.tif") as im: + xmp = im.getxmp() + + assert isinstance(xmp, dict) + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index daf732de1..cfd712305 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,6 +31,7 @@ import io import struct import sys import warnings +import xml.etree.ElementTree from . import Image from ._util import isPath @@ -309,6 +310,30 @@ class ImageFile(Image.Image): return self.tell() != frame + def _getxmp(self, xmp_tags): + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + children = list(element) + if children: + value = {get_name(k): v for k, v in element.attrib.items()} + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + return value + else: + return element.text + + root = xml.etree.ElementTree.fromstring(xmp_tags) + self._xmp[get_name(root.tag)] = get_value(root) + class StubImageFile(ImageFile): """ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a545665b4..afa92304c 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -40,7 +40,6 @@ import subprocess import sys import tempfile import warnings -import xml.etree.ElementTree from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 @@ -492,29 +491,7 @@ class JpegImageFile(ImageFile.ImageFile): if segment == "APP1": marker, xmp_tags = content.rsplit(b"\x00", 1) if marker == b"http://ns.adobe.com/xap/1.0/": - - def get_name(tag): - return tag.split("}")[1] - - def get_value(element): - children = list(element) - if children: - value = {get_name(k): v for k, v in element.attrib.items()} - for child in children: - name = get_name(child.tag) - child_value = get_value(child) - if name in value: - if not isinstance(value[name], list): - value[name] = [value[name]] - value[name].append(child_value) - else: - value[name] = child_value - return value - else: - return element.text - - root = xml.etree.ElementTree.fromstring(xmp_tags) - self._xmp[get_name(root.tag)] = get_value(root) + self._getxmp(xmp_tags) return self._xmp diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc59f14d3..d41a4dbfc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1032,6 +1032,7 @@ class TiffImageFile(ImageFile.ImageFile): self.__fp = self.fp self._frame_pos = [] self._n_frames = None + self._xmp = None logger.debug("*** TiffImageFile._open ***") logger.debug(f"- __first: {self.__first}") @@ -1101,6 +1102,19 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + :returns: XMP tags in a dictionary. + """ + + if self._xmp is None: + self._xmp = {} + + if 700 in self.tag_v2: + self._getxmp(self.tag_v2[700]) + return self._xmp + def load(self): if self.tile and self.use_load_libtiff: return self._load_libtiff() From cd31dae0d17374ab3f04fe43f43ff20fd0ce2be1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jun 2021 13:57:14 +1000 Subject: [PATCH 3/3] Added getxmp() for PNG --- Tests/test_file_jpeg.py | 15 +++++++++---- Tests/test_file_png.py | 10 +++++++++ src/PIL/Image.py | 45 ++++++++++++++++++++++++++++++-------- src/PIL/ImageFile.py | 25 --------------------- src/PIL/JpegImagePlugin.py | 8 ++----- src/PIL/PngImagePlugin.py | 11 ++++++++++ src/PIL/TiffImagePlugin.py | 9 +------- 7 files changed, 71 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f4c295e0d..a090d73e4 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -830,15 +830,22 @@ class TestFileJpeg: assert isinstance(xmp, dict) description = xmp["xmpmeta"]["RDF"]["Description"] - assert description["DerivedFrom"] is None - assert ( - description["Look"]["Description"]["Group"]["Alt"]["li"] == "Profiles" - ) + assert description["DerivedFrom"] == { + "documentID": "8367D410E636EA95B7DE7EBA1C43A412", + "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", + } + assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { + "lang": "x-default", + "text": "Profiles", + } assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] # Attribute assert description["Version"] == "10.4" + with Image.open("Tests/images/hopper.jpg") as im: + assert im.getxmp() == {} + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index a75abbe96..a7e5684d3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -651,6 +651,16 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 + def test_xmp(self): + with Image.open("Tests/images/color_snakes.png") as im: + xmp = im.getxmp() + + assert isinstance(xmp, dict) + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None + def test_exif(self): # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 28bcf4f00..a73de5ed3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1317,6 +1317,33 @@ class Image: return tuple(extrema) return self.im.getextrema() + def _getxmp(self, xmp_tags): + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + value = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + root = xml.etree.ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} + def getexif(self): if self._exif is None: self._exif = Exif() @@ -1332,15 +1359,15 @@ class Image: if 0x0112 not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: - root = xml.etree.ElementTree.fromstring(xmp_tags) - for elem in root.iter(): - if elem.tag.endswith("}Description"): - orientation = elem.attrib.get( - "{http://ns.adobe.com/tiff/1.0/}Orientation" - ) - if orientation: - self._exif[0x0112] = int(orientation) - break + xmp = self._getxmp(xmp_tags) + if ( + "xmpmeta" in xmp + and "RDF" in xmp["xmpmeta"] + and "Description" in xmp["xmpmeta"]["RDF"] + ): + description = xmp["xmpmeta"]["RDF"]["Description"] + if "Orientation" in description: + self._exif[0x0112] = int(description["Orientation"]) return self._exif diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index cfd712305..daf732de1 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,7 +31,6 @@ import io import struct import sys import warnings -import xml.etree.ElementTree from . import Image from ._util import isPath @@ -310,30 +309,6 @@ class ImageFile(Image.Image): return self.tell() != frame - def _getxmp(self, xmp_tags): - def get_name(tag): - return tag.split("}")[1] - - def get_value(element): - children = list(element) - if children: - value = {get_name(k): v for k, v in element.attrib.items()} - for child in children: - name = get_name(child.tag) - child_value = get_value(child) - if name in value: - if not isinstance(value[name], list): - value[name] = [value[name]] - value[name].append(child_value) - else: - value[name] = child_value - return value - else: - return element.text - - root = xml.etree.ElementTree.fromstring(xmp_tags) - self._xmp[get_name(root.tag)] = get_value(root) - class StubImageFile(ImageFile): """ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index afa92304c..ec4e12f7b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -361,7 +361,6 @@ class JpegImageFile(ImageFile.ImageFile): self.app = {} # compatibility self.applist = [] self.icclist = [] - self._xmp = None while True: @@ -484,15 +483,12 @@ class JpegImageFile(ImageFile.ImageFile): :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/": - self._getxmp(xmp_tags) - return self._xmp + return self._getxmp(xmp_tags) + return {} def _getexif(self): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index cd97d0914..899d91108 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -978,6 +978,17 @@ class PngImageFile(ImageFile.ImageFile): return super().getexif() + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + :returns: XMP tags in a dictionary. + """ + return ( + self._getxmp(self.info["XML:com.adobe.xmp"]) + if "XML:com.adobe.xmp" in self.info + else {} + ) + def _close__fp(self): try: if self.__fp != self.fp: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d41a4dbfc..9f86587f1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1032,7 +1032,6 @@ class TiffImageFile(ImageFile.ImageFile): self.__fp = self.fp self._frame_pos = [] self._n_frames = None - self._xmp = None logger.debug("*** TiffImageFile._open ***") logger.debug(f"- __first: {self.__first}") @@ -1107,13 +1106,7 @@ class TiffImageFile(ImageFile.ImageFile): Returns a dictionary containing the XMP tags. :returns: XMP tags in a dictionary. """ - - if self._xmp is None: - self._xmp = {} - - if 700 in self.tag_v2: - self._getxmp(self.tag_v2[700]) - return self._xmp + return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} def load(self): if self.tile and self.use_load_libtiff: