Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ci/install.sh
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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