Skip to content

Commit

Permalink
Merge pull request #5565 from radarhere/xml
Browse files Browse the repository at this point in the history
Replaced xml.etree.ElementTree
  • Loading branch information
radarhere committed Jun 30, 2021
2 parents 4b4e5f0 + 45eaab9 commit a8c042b
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 51 deletions.
1 change: 1 addition & 0 deletions .ci/install.sh
Expand Up @@ -24,6 +24,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
python3 -m pip install --upgrade pip
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/macos-install.sh
Expand Up @@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op

PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-windows.yml
Expand Up @@ -51,8 +51,8 @@ jobs:
- name: Print build system information
run: python .github/workflows/system-info.py

- name: python -m pip install wheel pytest pytest-cov pytest-timeout
run: python -m pip install wheel pytest pytest-cov pytest-timeout
- name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml

- name: Install dependencies
id: install
Expand Down
48 changes: 28 additions & 20 deletions Tests/test_file_jpeg.py
Expand Up @@ -28,6 +28,11 @@
skip_unless_feature,
)

try:
import defusedxml.ElementTree as ElementTree
except ImportError:
ElementTree = None

TEST_FILE = "Tests/images/hopper.jpg"


Expand Down Expand Up @@ -825,26 +830,29 @@ def read(n=-1):

def test_getxmp(self):
with Image.open("Tests/images/xmp_test.jpg") as im:
xmp = im.getxmp()

assert isinstance(xmp, dict)

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() == {}
if ElementTree is None:
with pytest.warns(UserWarning):
assert im.getxmp() == {}
else:
xmp = im.getxmp()

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"

if ElementTree is not None:
with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {}


@pytest.mark.skipif(not is_win32(), reason="Windows only")
Expand Down
23 changes: 15 additions & 8 deletions Tests/test_file_png.py
Expand Up @@ -19,6 +19,11 @@
skip_unless_feature,
)

try:
import defusedxml.ElementTree as ElementTree
except ImportError:
ElementTree = None

# sample png stream

TEST_PNG_FILE = "Tests/images/hopper.png"
Expand Down Expand Up @@ -651,15 +656,17 @@ 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):
def test_getxmp(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
if ElementTree is None:
with pytest.warns(UserWarning):
assert im.getxmp() == {}
else:
xmp = im.getxmp()

description = xmp["xmpmeta"]["RDF"]["Description"]
assert description["PixelXDimension"] == "10"
assert description["subject"]["Seq"] is None

def test_exif(self):
# With an EXIF chunk
Expand Down
23 changes: 15 additions & 8 deletions Tests/test_file_tiff.py
Expand Up @@ -16,6 +16,11 @@
is_win32,
)

try:
import defusedxml.ElementTree as ElementTree
except ImportError:
ElementTree = None


class TestFileTiff:
def test_sanity(self, tmp_path):
Expand Down Expand Up @@ -643,15 +648,17 @@ 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):
def test_getxmp(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"]
if ElementTree is None:
with pytest.warns(UserWarning):
assert im.getxmp() == {}
else:
xmp = im.getxmp()

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
Expand Down
12 changes: 11 additions & 1 deletion docs/releasenotes/8.3.0.rst
Expand Up @@ -61,7 +61,17 @@ format, through the new ``bitmap_format`` argument::
Security
========

TODO
Parsing XML
^^^^^^^^^^^

Pillow previously parsed XMP data using Python's ``xml`` module. However, this module
is not secure.

- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
will now use ``defusedxml`` instead. If the dependency is not present, an empty
dictionary will be returned and a warning raised.

Other Changes
=============
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -2,6 +2,7 @@
black
check-manifest
coverage
defusedxml
markdown2
olefile
packaging
Expand Down
27 changes: 15 additions & 12 deletions src/PIL/Image.py
Expand Up @@ -31,14 +31,19 @@
import math
import numbers
import os
import re
import struct
import sys
import tempfile
import warnings
import xml.etree.ElementTree
from collections.abc import Callable, MutableMapping
from pathlib import Path

try:
import defusedxml.ElementTree as ElementTree
except ImportError:
ElementTree = None

# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION is deprecated and will be removed in a future release.
# Use __version__ instead.
Expand Down Expand Up @@ -1358,8 +1363,12 @@ def get_value(element):
return element.text
return value

root = xml.etree.ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)}
if ElementTree is None:
warnings.warn("XMP data cannot be read without defusedxml dependency")
return {}
else:
root = ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)}

def getexif(self):
if self._exif is None:
Expand All @@ -1381,15 +1390,9 @@ def getexif(self):
if 0x0112 not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
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"])
match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags)
if match:
self._exif[0x0112] = int(match[1])

return self._exif

Expand Down
1 change: 1 addition & 0 deletions src/PIL/JpegImagePlugin.py
Expand Up @@ -480,6 +480,7 @@ def _getmp(self):
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""

Expand Down
1 change: 1 addition & 0 deletions src/PIL/PngImagePlugin.py
Expand Up @@ -981,6 +981,7 @@ def getexif(self):
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return (
Expand Down
1 change: 1 addition & 0 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -1112,6 +1112,7 @@ def tell(self):
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
:returns: XMP tags in a dictionary.
"""
return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {}
Expand Down

0 comments on commit a8c042b

Please sign in to comment.