diff --git a/.ci/install.sh b/.ci/install.sh index fbe85ded7..0dbf2d690 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -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 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index b20ecb4dc..3a70c8047 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -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 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cdb8493dc..ce04ba5ca 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -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 diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index a090d73e4..3876466d3 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -28,6 +28,11 @@ from .helper import ( skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + TEST_FILE = "Tests/images/hopper.jpg" @@ -825,26 +830,28 @@ class TestFileJpeg: def test_getxmp(self): with Image.open("Tests/images/xmp_test.jpg") as im: - xmp = im.getxmp() + if ElementTree is None: + assert xmp == {} + else: + 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"] - 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" - # Attribute - assert description["Version"] == "10.4" - - with Image.open("Tests/images/hopper.jpg") as im: - assert im.getxmp() == {} + 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") diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index a7e5684d3..cfeedd932 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -19,6 +19,11 @@ from .helper import ( skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" @@ -651,15 +656,16 @@ class TestFilePng: 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() + if ElementTree is None: + assert im.getxmp() == {} + else: + xmp = im.getxmp() - assert isinstance(xmp, dict) - - description = xmp["xmpmeta"]["RDF"]["Description"] - assert description["PixelXDimension"] == "10" - assert description["subject"]["Seq"] is None + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None def test_exif(self): # With an EXIF chunk diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d3de203d8..3465d946e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -16,6 +16,11 @@ from .helper import ( is_win32, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + class TestFileTiff: def test_sanity(self, tmp_path): @@ -643,15 +648,16 @@ class TestFileTiff: 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() + if ElementTree is None: + assert im.getxmp() == {} + else: + 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"] + 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 diff --git a/requirements.txt b/requirements.txt index fd2ede5fd..38011fd39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ black check-manifest coverage +defusedxml markdown2 olefile packaging diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 587d44e97..c615e9a93 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -36,10 +36,14 @@ 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. @@ -1359,8 +1363,11 @@ class Image: return element.text return value - root = xml.etree.ElementTree.fromstring(xmp_tags) - return {get_name(root.tag): get_value(root)} + if ElementTree is None: + return {} + else: + root = ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} def getexif(self): if self._exif is None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ec4e12f7b..e0dde1fac 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -480,6 +480,7 @@ class JpegImageFile(ImageFile.ImageFile): def getxmp(self): """ Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. :returns: XMP tags in a dictionary. """ diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 899d91108..bd886e218 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -981,6 +981,7 @@ class PngImageFile(ImageFile.ImageFile): def getxmp(self): """ Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. :returns: XMP tags in a dictionary. """ return ( diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b836e9994..94aae70a2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1112,6 +1112,7 @@ class TiffImageFile(ImageFile.ImageFile): 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 {}