Skip to content

Commit

Permalink
Merge pull request #5455 from radarhere/xmp
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Jun 28, 2021
2 parents 9f28e4b + cd31dae commit b5c4b9a
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 22 deletions.
18 changes: 17 additions & 1 deletion Tests/test_file_jpeg.py
Expand Up @@ -828,7 +828,23 @@ def test_getxmp(self):
xmp = im.getxmp()

assert isinstance(xmp, dict)
assert xmp["Description"]["Version"] == "10.4"

description = xmp["xmpmeta"]["RDF"]["Description"]
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")
Expand Down
10 changes: 10 additions & 0 deletions Tests/test_file_png.py
Expand Up @@ -651,6 +651,16 @@ def test_plte_length(self, tmp_path):
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:
Expand Down
10 changes: 10 additions & 0 deletions Tests/test_file_tiff.py
Expand Up @@ -643,6 +643,16 @@ def test_discard_icc_profile(self, tmp_path):
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")
Expand Down
45 changes: 36 additions & 9 deletions src/PIL/Image.py
Expand Up @@ -1318,6 +1318,33 @@ def getextrema(self):
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()
Expand All @@ -1338,15 +1365,15 @@ def getexif(self):
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

Expand Down
14 changes: 2 additions & 12 deletions src/PIL/JpegImagePlugin.py
Expand Up @@ -40,7 +40,6 @@
import sys
import tempfile
import warnings
import xml.etree.ElementTree

from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16
Expand Down Expand Up @@ -362,7 +361,6 @@ def _open(self):
self.app = {} # compatibility
self.applist = []
self.icclist = []
self._xmp = None

while True:

Expand Down Expand Up @@ -485,20 +483,12 @@ def getxmp(self):
: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
return self._getxmp(xmp_tags)
return {}


def _getexif(self):
Expand Down
11 changes: 11 additions & 0 deletions src/PIL/PngImagePlugin.py
Expand Up @@ -978,6 +978,17 @@ def getexif(self):

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:
Expand Down
7 changes: 7 additions & 0 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -1105,6 +1105,13 @@ def tell(self):
"""Return the current frame number"""
return self.__frame

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
:returns: XMP tags in a dictionary.
"""
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:
return self._load_libtiff()
Expand Down

0 comments on commit b5c4b9a

Please sign in to comment.