Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved getxmp() #5455

Merged
merged 3 commits into from Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -600,6 +600,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 @@ -1317,6 +1317,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 @@ -1332,15 +1359,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 @@ -1101,6 +1101,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