diff --git a/.appveyor.yml b/.appveyor.yml
index b817cd9d8..d4dd2dc95 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -21,9 +21,11 @@ environment:
install:
- '%PYTHON%\%EXECUTABLE% --version'
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
+- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-depends.zip -oc:\
+- 7z x pillow-test-images.zip -oc:\
- mv c:\pillow-depends-main c:\pillow-depends
-- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
+- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs1000w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%
diff --git a/.ci/install.sh b/.ci/install.sh
index 518b66acc..6aa122cc5 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
- python3 -m pip install numpy
+ # TODO Remove condition when NumPy supports 3.12
+ if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index dfd7d0553..1fc6262f4 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install numpy
+# TODO Remove condition when NumPy supports 3.12
+if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 7b8070d34..451181434 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -34,18 +34,34 @@ jobs:
with:
platform: x86_64
packages: >
- ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel
- libimagequant-devel libjpeg-devel liblapack-devel
- liblcms2-devel libopenjp2-devel libraqm-devel
- libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0
- make netpbm perl
+ gcc-g++
+ ghostscript
+ ImageMagick
+ jpeg
+ libfreetype-devel
+ libimagequant-devel
+ libjpeg-devel
+ liblapack-devel
+ liblcms2-devel
+ libopenjp2-devel
+ libraqm-devel
+ libtiff-devel
+ libwebp-devel
+ libxcb-devel
+ libxcb-xinerama0
+ make
+ netpbm
+ perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
- qt5-devel-tools subversion xorg-server-extra zlib-devel
+ qt5-devel-tools
+ wget
+ xorg-server-extra
+ zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v3
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 7331cf8ee..7d2b20d65 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -87,6 +87,7 @@ jobs:
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
+ gcov: true
success:
permissions:
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index ccf6e193a..737da7b94 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -45,12 +45,6 @@ jobs:
- name: Install dependencies
run: |
pacman -S --noconfirm \
- ${{ matrix.package }}-python3-cffi \
- ${{ matrix.package }}-python3-numpy \
- ${{ matrix.package }}-python3-olefile \
- ${{ matrix.package }}-python3-pip \
- ${{ matrix.package }}-python-pyqt6 \
- ${{ matrix.package }}-python3-setuptools \
${{ matrix.package }}-freetype \
${{ matrix.package }}-gcc \
${{ matrix.package }}-ghostscript \
@@ -61,7 +55,16 @@ jobs:
${{ matrix.package }}-libtiff \
${{ matrix.package }}-libwebp \
${{ matrix.package }}-openjpeg2 \
- subversion
+ ${{ matrix.package }}-python3-cffi \
+ ${{ matrix.package }}-python3-numpy \
+ ${{ matrix.package }}-python3-olefile \
+ ${{ matrix.package }}-python3-pip \
+ ${{ matrix.package }}-python3-setuptools
+
+ if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then
+ pacman -S --noconfirm \
+ ${{ matrix.package }}-python-pyqt6
+ fi
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 219189cf2..f8b050f76 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -48,5 +48,5 @@ jobs:
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
- docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
+ docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 487c3586f..306e34ca9 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
@@ -38,6 +38,12 @@ jobs:
repository: python-pillow/pillow-depends
path: winbuild\depends
+ - name: Checkout extra test images
+ uses: actions/checkout@v3
+ with:
+ repository: python-pillow/test-images
+ path: Tests\test-images
+
# sets env: pythonLocation
- name: Set up Python
uses: actions/setup-python@v4
@@ -62,7 +68,8 @@ jobs:
winbuild\depends\gs1000w32.exe /S
echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
- xcopy /S /Y winbuild\depends\test_images\* Tests\images\
+ # Install extra test images
+ xcopy /S /Y Tests\test-images\* Tests\images
# make cache key depend on VS version
& "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" `
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 11c7b77be..33958bea8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -22,6 +22,7 @@ jobs:
python-version: [
"pypy3.9",
"pypy3.8",
+ "3.12-dev",
"3.11",
"3.10",
"3.9",
@@ -107,9 +108,9 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
- file: ./coverage.xml
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
+ gcov: true
success:
permissions:
diff --git a/.gitignore b/.gitignore
index 790404535..1dd6c9175 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,7 +79,7 @@ docs/_build/
# JetBrains
.idea
-# Extra test images installed from pillow-depends/test_images
+# Extra test images installed from python-pillow/test-images
Tests/images/README.md
Tests/images/crash_1.tif
Tests/images/crash_2.tif
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d019d3e7f..45c1f3c5f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.12.0
+ rev: 23.1.0
hooks:
- id: black
args: [--target-version=py37]
@@ -9,7 +9,7 @@ repos:
types: []
- repo: https://github.com/PyCQA/isort
- rev: 5.11.1
+ rev: 5.12.0
hooks:
- id: isort
@@ -26,7 +26,7 @@ repos:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.3.1
+ rev: v1.4.2
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
@@ -39,7 +39,7 @@ repos:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
- rev: v1.9.0
+ rev: v1.10.0
hooks:
- id: python-check-blanket-noqa
- id: rst-backticks
@@ -57,7 +57,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt
- rev: 0.5.2
+ rev: 0.6.1
hooks:
- id: tox-ini-fmt
diff --git a/CHANGES.rst b/CHANGES.rst
index 904c73629..fe0230c34 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,60 @@
Changelog (Pillow)
==================
-9.4.0 (unreleased)
+9.5.0 (unreleased)
------------------
+- Added memoryview support to frombytes() #6974
+ [radarhere]
+
+- Allow comments in FITS images #6973
+ [radarhere]
+
+- Support saving PDF with different X and Y resolutions #6961
+ [jvanderneutstulen, radarhere, hugovk]
+
+- Fixed writing int as UNDEFINED tag #6950
+ [radarhere]
+
+- Raise an error if EXIF data is too long when saving JPEG #6939
+ [radarhere]
+
+- Handle more than one directory returned by pkg-config #6896
+ [sebastic, radarhere]
+
+- Do not retry past formats when loading all formats for the first time #6902
+ [radarhere]
+
+- Do not retry specified formats if they failed when opening #6893
+ [radarhere]
+
+- Do not unintentionally load TIFF format at first #6892
+ [radarhere]
+
+- Stop reading when EPS line becomes too long #6897
+ [radarhere]
+
+- Allow writing IFDRational to BYTE tag #6890
+ [radarhere]
+
+- Raise ValueError for BoxBlur filter with negative radius #6874
+ [hugovk, radarhere]
+
+- Support arbitrary number of loaded modules on Windows #6761
+ [javidcf, radarhere, nulano]
+
+9.4.0 (2023-01-02)
+------------------
+
+- Fixed null pointer dereference crash with malformed font #6846
+ [wiredfool, radarhere]
+
+- Return from ImagingFill early if image has a zero dimension #6842
+ [radarhere]
+
+- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830
+ [radarhere]
+
- Improve exception traceback readability #6836
[hugovk, radarhere]
diff --git a/LICENSE b/LICENSE
index 40aabc323..cf65e86d7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2022 by Alex Clark and contributors
+ Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License:
@@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply
with the following terms and conditions:
-Permission to use, copy, modify, and distribute this software and its
-associated documentation for any purpose and without fee is hereby granted,
+Permission to use, copy, modify and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appears in all copies, and that
both that copyright notice and this permission notice appear in supporting
documentation, and that the name of Secret Labs AB or the author not be
diff --git a/README.md b/README.md
index 8ee68f9b8..af1ca57c2 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,8 @@
## Python Imaging Library (Fork)
-Pillow is the friendly PIL fork by [Alex Clark and
-Contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
+Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
+contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
As of 2019, Pillow development is
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
@@ -88,6 +88,10 @@ As of 2019, Pillow development is
+
diff --git a/RELEASING.md b/RELEASING.md
index b05067484..c203a9c12 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related
## Publicize Release
-* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
+* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
## Documentation
diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py
index 08a55d349..c600c45ed 100644
--- a/Tests/check_fli_overflow.py
+++ b/Tests/check_fli_overflow.py
@@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli"
def test_fli_overflow():
-
# this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im:
im.load()
diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py
index d8d645189..f4a129f50 100644
--- a/Tests/check_png_dos.py
+++ b/Tests/check_png_dos.py
@@ -23,7 +23,6 @@ def test_ignore_dos_text():
def test_dos_text():
-
try:
im = Image.open(TEST_FILE)
im.load()
diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784
new file mode 100644
index 000000000..0465e48c2
--- /dev/null
+++ b/Tests/fonts/fuzz_font-5203009437302784
@@ -0,0 +1,10 @@
+STARTFONT
+FONT
+SIZE 10
+FONTBOUNDINGBOX
+CHARS
+STARTCHAR
+ENCODING
+BBX 2 5
+ENDCHAR
+ENDFONT
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 629e9ac00..dc111c38b 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -57,6 +57,6 @@ def test_fuzz_fonts(path):
with open(path, "rb") as f:
try:
fuzzers.fuzz_font(f.read())
- except (Image.DecompressionBombError, Image.DecompressionBombWarning):
+ except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError):
pass
assert True
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index ed9aff9cc..002a44a4f 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -18,7 +18,6 @@ def test_bad():
"""These shouldn't crash/dos, but they shouldn't return anything
either"""
for f in get_files("b"):
-
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
try:
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index 30ed4a808..3375eb6b2 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -11,6 +11,11 @@ from PIL import _deprecate
"Old thing is deprecated and will be removed in Pillow 10 "
r"\(2023-07-01\)\. Use new thing instead\.",
),
+ (
+ 11,
+ "Old thing is deprecated and will be removed in Pillow 11 "
+ r"\(2024-10-15\)\. Use new thing instead\.",
+ ),
(
None,
r"Old thing is deprecated and will be removed in a future version\. "
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 5f6d52355..9e79937e9 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -141,7 +141,6 @@ def test_rgba_bitfields():
# This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to RGBA
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
-
# So before the comparing the image, swap the channels
b, g, r = im.split()[1:]
im = Image.merge("RGB", (r, g, b))
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index e330404d6..76f185b9a 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "BUFR"
@@ -31,7 +30,6 @@ def test_invalid_file():
def test_load():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError):
im.load()
diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py
index 0f09c4b99..ef378b24a 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -15,7 +15,6 @@ def test_sanity():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.size == (128, 128)
assert isinstance(im, DcxImagePlugin.DcxImageFile)
@@ -54,7 +53,6 @@ def test_invalid_file():
def test_tell():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act
frame = im.tell()
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 015dda992..ac6e84447 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -80,7 +80,6 @@ def test_invalid_file():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk():
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
-
assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100)
assert cmyk_image.format == "EPS"
diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py
index 447888acd..3048827e0 100644
--- a/Tests/test_file_fits.py
+++ b/Tests/test_file_fits.py
@@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "FITS"
assert im.size == (128, 128)
@@ -45,6 +44,12 @@ def test_naxis_zero():
pass
+def test_comment():
+ image_data = b"SIMPLE = T / comment string"
+ with pytest.raises(OSError):
+ FitsImagePlugin.FitsImageFile(BytesIO(image_data))
+
+
def test_stub_deprecated():
class Handler:
opened = False
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index b8b999d70..70d4d76db 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -64,7 +64,6 @@ def test_context_manager():
def test_tell():
# Arrange
with Image.open(static_test_file) as im:
-
# Act
frame = im.tell()
@@ -110,7 +109,6 @@ def test_eoferror():
def test_seek_tell():
with Image.open(animated_test_file) as im:
-
layer_number = im.tell()
assert layer_number == 0
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index d48fc1442..bce72d192 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -158,39 +158,42 @@ def test_optimize():
assert test_bilevel(1) == 799
-def test_optimize_correctness():
- # 256 color Palette image, posterize to > 128 and < 128 levels
- # Size bigger and smaller than 512x512
+@pytest.mark.parametrize(
+ "colors, size, expected_palette_length",
+ (
+ # These do optimize the palette
+ (256, 511, 256),
+ (255, 511, 255),
+ (129, 511, 129),
+ (128, 511, 128),
+ (64, 511, 64),
+ (4, 511, 4),
+ # These don't optimize the palette
+ (128, 513, 256),
+ (64, 513, 256),
+ (4, 513, 256),
+ ),
+)
+def test_optimize_correctness(colors, size, expected_palette_length):
+ # 256 color Palette image, posterize to > 128 and < 128 levels.
+ # Size bigger and smaller than 512x512.
# Check the palette for number of colors allocated.
- # Check for correctness after conversion back to RGB
- def check(colors, size, expected_palette_length):
- # make an image with empty colors in the start of the palette range
- im = Image.frombytes(
- "P", (colors, colors), bytes(range(256 - colors, 256)) * colors
- )
- im = im.resize((size, size))
- outfile = BytesIO()
- im.save(outfile, "GIF")
- outfile.seek(0)
- with Image.open(outfile) as reloaded:
- # check palette length
- palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)
- assert expected_palette_length == palette_length
+ # Check for correctness after conversion back to RGB.
- assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
+ # make an image with empty colors in the start of the palette range
+ im = Image.frombytes(
+ "P", (colors, colors), bytes(range(256 - colors, 256)) * colors
+ )
+ im = im.resize((size, size))
+ outfile = BytesIO()
+ im.save(outfile, "GIF")
+ outfile.seek(0)
+ with Image.open(outfile) as reloaded:
+ # check palette length
+ palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)
+ assert expected_palette_length == palette_length
- # These do optimize the palette
- check(256, 511, 256)
- check(255, 511, 255)
- check(129, 511, 129)
- check(128, 511, 128)
- check(64, 511, 64)
- check(4, 511, 4)
-
- # These don't optimize the palette
- check(128, 513, 256)
- check(64, 513, 256)
- check(4, 513, 256)
+ assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
def test_optimize_full_l():
@@ -206,7 +209,7 @@ def test_optimize_if_palette_can_be_reduced_by_half():
im = im.resize((591, 443))
im_rgb = im.convert("RGB")
- for (optimize, colors) in ((False, 256), (True, 8)):
+ for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO()
im_rgb.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded:
@@ -218,7 +221,6 @@ def test_roundtrip(tmp_path):
im = hopper()
im.save(out)
with Image.open(out) as reread:
-
assert_image_similar(reread.convert("RGB"), im, 50)
@@ -229,7 +231,6 @@ def test_roundtrip2(tmp_path):
im2 = im.copy()
im2.save(out)
with Image.open(out) as reread:
-
assert_image_similar(reread.convert("RGB"), hopper(), 50)
@@ -239,7 +240,6 @@ def test_roundtrip_save_all(tmp_path):
im = hopper()
im.save(out, save_all=True)
with Image.open(out) as reread:
-
assert_image_similar(reread.convert("RGB"), im, 50)
# Multiframe image
@@ -281,13 +281,11 @@ def test_headers_saving_for_animated_gifs(tmp_path):
important_headers = ["background", "version", "duration", "loop"]
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
-
info = im.info.copy()
out = str(tmp_path / "temp.gif")
im.save(out, save_all=True)
with Image.open(out) as reread:
-
for header in important_headers:
assert info[header] == reread.info[header]
@@ -305,7 +303,6 @@ def test_palette_handling(tmp_path):
im2.save(f, optimize=True)
with Image.open(f) as reloaded:
-
assert_image_similar(im, reloaded.convert("RGB"), 10)
@@ -321,7 +318,6 @@ def test_palette_434(tmp_path):
orig = "Tests/images/test.colors.gif"
with Image.open(orig) as im:
-
with roundtrip(im) as reloaded:
assert_image_similar(im, reloaded, 1)
with roundtrip(im, optimize=True) as reloaded:
@@ -572,7 +568,6 @@ def test_save_dispose(tmp_path):
)
with Image.open(out) as img:
-
for i in range(2):
img.seek(img.tell() + 1)
assert img.disposal_method == i + 1
@@ -770,7 +765,6 @@ def test_multiple_duration(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=duration_list
)
with Image.open(out) as reread:
-
for duration in duration_list:
assert reread.info["duration"] == duration
try:
@@ -783,7 +777,6 @@ def test_multiple_duration(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list)
)
with Image.open(out) as reread:
-
for duration in duration_list:
assert reread.info["duration"] == duration
try:
@@ -841,7 +834,6 @@ def test_identical_frames(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=duration_list
)
with Image.open(out) as reread:
-
# Assert that the first three frames were combined
assert reread.n_frames == 2
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index fd427746e..768ac12bd 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "GRIB"
@@ -31,7 +30,6 @@ def test_invalid_file():
def test_load():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError):
im.load()
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index 20b4b9619..98dc5443c 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "HDF5"
@@ -29,7 +28,6 @@ def test_invalid_file():
def test_load():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError):
im.load()
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index 55632909c..42275424d 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -16,7 +16,6 @@ def test_sanity():
# Loading this icon by default should result in the largest size
# (512x512@2x) being loaded
with Image.open(TEST_FILE) as im:
-
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
im.load()
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index afb17b1af..9c1c3cf17 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -175,7 +175,6 @@ def test_save_256x256(tmp_path):
# Act
im.save(outfile)
with Image.open(outfile) as im_saved:
-
# Assert
assert im_saved.size == (256, 256)
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index 5cf93713b..425e690d6 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -51,7 +51,6 @@ def test_context_manager():
def test_tell():
# Arrange
with Image.open(TEST_IM) as im:
-
# Act
frame = im.tell()
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 2d0e6977a..2d99528d3 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg"
def test_getiptcinfo_jpg_none():
# Arrange
with hopper() as im:
-
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
@@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none():
def test_getiptcinfo_jpg_found():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
@@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found():
def test_getiptcinfo_tiff_none():
# Arrange
with Image.open("Tests/images/hopper.tif") as im:
-
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index eabc6bf75..4981e15af 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -57,7 +57,6 @@ class TestFileJpeg:
return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode)))
def test_sanity(self):
-
# internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
@@ -271,7 +270,10 @@ class TestFileJpeg:
# https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg")
im = hopper()
- im.save(f, "JPEG", quality=90, exif=b"1" * 65532)
+ im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
+
+ with pytest.raises(ValueError):
+ im.save(f, "JPEG", quality=90, exif=b"1" * 65534)
def test_exif_typeerror(self):
with Image.open("Tests/images/exif_typeerror.jpg") as im:
@@ -368,7 +370,6 @@ class TestFileJpeg:
def test_exif_gps_typeerror(self):
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
-
# Should not raise a TypeError
im._getexif()
@@ -447,7 +448,7 @@ class TestFileJpeg:
ims = im.get_child_images()
assert len(ims) == 1
- assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png")
+ assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1)
def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
@@ -682,7 +683,6 @@ class TestFileJpeg:
# Shouldn't raise error
fn = "Tests/images/sugarshack_bad_mpo_header.jpg"
with pytest.warns(UserWarning, Image.open, fn) as im:
-
# Assert
assert im.format == "JPEG"
@@ -704,7 +704,6 @@ class TestFileJpeg:
# Arrange
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
-
# Act
im.save(outfile, "JPEG", dpi=im.info["dpi"])
@@ -731,7 +730,6 @@ class TestFileJpeg:
# This Photoshop CC 2017 image has DPI in EXIF not metadata
# EXIF XResolution is (2000000, 10000)
with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
-
# Act / Assert
assert im.info.get("dpi") == (200, 200)
@@ -740,7 +738,6 @@ class TestFileJpeg:
# This image has DPI in EXIF not metadata
# EXIF XResolution is 72
with Image.open("Tests/images/exif-72dpi-int.jpg") as im:
-
# Act / Assert
assert im.info.get("dpi") == (72, 72)
@@ -749,7 +746,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with EXIF resolution unit set to cm:
# exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg
with Image.open("Tests/images/exif-200dpcm.jpg") as im:
-
# Act / Assert
assert im.info.get("dpi") == (508, 508)
@@ -758,7 +754,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with EXIF resolution set to 0/0:
# exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg
with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im:
-
# Act / Assert
# This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72)
@@ -767,7 +762,6 @@ class TestFileJpeg:
# Arrange
# 0x011A tag in this exif contains string '300300\x02'
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
-
# Act / Assert
# This should return the default
assert im.info.get("dpi") == (72, 72)
@@ -777,7 +771,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
# exiftool "-*resolution*"= photoshop-200dpi.jpg
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
-
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# https://exiv2.org/tags.html
@@ -787,7 +780,6 @@ class TestFileJpeg:
# This is no-dpi-in-exif with the tiff header of the exif block
# hexedited from MM * to FF FF FF FF
with Image.open("Tests/images/invalid-exif.jpg") as im:
-
# This should return the default, and not a SyntaxError or
# OSError for unidentified image.
assert im.info.get("dpi") == (72, 72)
@@ -810,7 +802,6 @@ class TestFileJpeg:
def test_invalid_exif_x_resolution(self):
# When no x or y resolution is defined in EXIF
with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im:
-
# This should return the default, and not a ValueError or
# OSError for an unidentified image.
assert im.info.get("dpi") == (72, 72)
@@ -820,7 +811,6 @@ class TestFileJpeg:
# This image has been manually hexedited to have an IFD offset of 10,
# in contrast to normal 8
with Image.open("Tests/images/exif-ifd-offset.jpg") as im:
-
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 0229b2243..de622c478 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -270,7 +270,6 @@ def test_rgba():
# Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2:
-
# Act
j2k.load()
jp2.load()
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 1109cd15e..f886d3aae 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase):
pilim = hopper()
def save_bytesio(compression=None):
-
buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)
@@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase):
def test_multipage_compression(self):
with Image.open("Tests/images/compression.tif") as im:
-
im.seek(0)
assert im._compression == "tiff_ccitt"
assert im.size == (10, 10)
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index 3e5476222..f0dedc2de 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -168,8 +168,7 @@ def test_mp_no_data():
def test_mp_attribute(test_file):
with Image.open(test_file) as im:
mpinfo = im._getmp()
- frame_number = 0
- for mpentry in mpinfo[0xB002]:
+ for frame_number, mpentry in enumerate(mpinfo[0xB002]):
mpattr = mpentry["Attribute"]
if frame_number:
assert not mpattr["RepresentativeImageFlag"]
@@ -180,7 +179,6 @@ def test_mp_attribute(test_file):
assert mpattr["ImageDataFormat"] == "JPEG"
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
assert mpattr["Reserved"] == 0
- frame_number += 1
@pytest.mark.parametrize("test_file", test_files)
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index 50d7c590b..497052b05 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -44,7 +44,6 @@ def test_open_windows_v1():
# Arrange
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert_image_equal(im, hopper("1"))
assert isinstance(im, MspImagePlugin.MspImageFile)
@@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path):
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_open_windows_v2():
-
files = (
os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR)
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index 9667b6a4a..2afec9960 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -80,6 +80,34 @@ def test_resolution(tmp_path):
assert size == (61.44, 61.44)
+@pytest.mark.parametrize(
+ "params",
+ (
+ {"dpi": (75, 150)},
+ {"dpi": (75, 150), "resolution": 200},
+ ),
+)
+def test_dpi(params, tmp_path):
+ im = hopper()
+
+ outfile = str(tmp_path / "temp.pdf")
+ im.save(outfile, **params)
+
+ with open(outfile, "rb") as fp:
+ contents = fp.read()
+
+ size = tuple(
+ float(d)
+ for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
+ )
+ assert size == (122.88, 61.44)
+
+ size = tuple(
+ float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
+ )
+ assert size == (122.88, 61.44)
+
+
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@@ -89,7 +117,6 @@ def test_save_all(tmp_path):
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
-
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, save_all=True)
@@ -123,7 +150,6 @@ def test_save_all(tmp_path):
def test_multiframe_normal_save(tmp_path):
# Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im:
-
outfile = str(tmp_path / "temp.pdf")
im.save(outfile)
@@ -286,6 +312,7 @@ def test_pdf_append_to_bytesio():
@pytest.mark.timeout(1)
+@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline):
malicious = b" trailer<<>>" + newline * 3456
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 9481cd5dd..c4db97905 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -78,7 +78,6 @@ class TestFilePng:
return chunks
def test_sanity(self, tmp_path):
-
# internal version number
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
@@ -156,7 +155,6 @@ class TestFilePng:
assert im.info == {"spam": "egg"}
def test_bad_itxt(self):
-
im = load(HEAD + chunk(b"iTXt") + TAIL)
assert im.info == {}
@@ -201,7 +199,6 @@ class TestFilePng:
assert im.info["spam"].tkey == "Spam"
def test_interlace(self):
-
test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im:
assert_image(im, "P", (162, 150))
@@ -495,7 +492,6 @@ class TestFilePng:
# Check reading images with null tRNS value, issue #1239
test_file = "Tests/images/tRNS_null_1x1.png"
with Image.open(test_file) as im:
-
assert im.info["transparency"] == 0
def test_save_icc_profile(self):
@@ -593,7 +589,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self):
with Image.open("Tests/images/hopper.png") as im:
- assert "comment" in im.text.keys()
+ assert "comment" in im.text
for k, v in {
"date:create": "2014-09-04T09:37:08+03:00",
"date:modify": "2014-09-04T09:37:08+03:00",
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 4f934375c..036cb9d4b 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -77,7 +77,6 @@ def test_eoferror():
def test_seek_tell():
with Image.open(test_file) as im:
-
layer_number = im.tell()
assert layer_number == 1
@@ -95,7 +94,6 @@ def test_seek_tell():
def test_seek_eoferror():
with Image.open(test_file) as im:
-
with pytest.raises(EOFError):
im.seek(-1)
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 0e3b705a2..011e208d8 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -79,7 +79,6 @@ def test_is_spider_image():
def test_tell():
# Arrange
with Image.open(TEST_FILE) as im:
-
# Act
index = im.tell()
diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py
index 05c78c316..edb320603 100644
--- a/Tests/test_file_sun.py
+++ b/Tests/test_file_sun.py
@@ -16,7 +16,6 @@ def test_sanity():
# Act
with Image.open(test_file) as im:
-
# Assert
assert im.size == (128, 128)
diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py
index 5daab47fc..799c243d6 100644
--- a/Tests/test_file_tar.py
+++ b/Tests/test_file_tar.py
@@ -10,18 +10,21 @@ from .helper import is_pypy
TEST_TAR_FILE = "Tests/images/hopper.tar"
-def test_sanity():
- for codec, test_path, format in [
- ["zlib", "hopper.png", "PNG"],
- ["jpg", "hopper.jpg", "JPEG"],
- ]:
- if features.check(codec):
- with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
- with Image.open(tar) as im:
- im.load()
- assert im.mode == "RGB"
- assert im.size == (128, 128)
- assert im.format == format
+@pytest.mark.parametrize(
+ "codec, test_path, format",
+ (
+ ("zlib", "hopper.png", "PNG"),
+ ("jpg", "hopper.jpg", "JPEG"),
+ ),
+)
+def test_sanity(codec, test_path, format):
+ if features.check(codec):
+ with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
+ with Image.open(tar) as im:
+ im.load()
+ assert im.mode == "RGB"
+ assert im.size == (128, 128)
+ assert im.format == format
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 7d8b5139a..bac00e855 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -78,7 +78,6 @@ def test_id_field():
# Act
with Image.open(test_file) as im:
-
# Assert
assert im.size == (100, 100)
@@ -89,7 +88,6 @@ def test_id_field_rle():
# Act
with Image.open(test_file) as im:
-
# Assert
assert im.size == (199, 199)
@@ -171,7 +169,6 @@ def test_save_id_section(tmp_path):
test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im:
-
# Save with no id section
im.save(out, id_section="")
with Image.open(out) as test_im:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 4f3c8e390..70142747c 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -25,7 +25,6 @@ except ImportError:
class TestFileTiff:
def test_sanity(self, tmp_path):
-
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename)
@@ -157,7 +156,6 @@ class TestFileTiff:
def test_xyres_tiff(self):
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
-
# legacy api
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
@@ -171,7 +169,6 @@ class TestFileTiff:
def test_xyres_fallback_tiff(self):
filename = "Tests/images/compression.tif"
with Image.open(filename) as im:
-
# v2 api
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
@@ -186,7 +183,6 @@ class TestFileTiff:
def test_int_resolution(self):
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
-
# Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71
@@ -381,7 +377,6 @@ class TestFileTiff:
def test___str__(self):
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
-
# Act
ret = str(im.ifd)
@@ -392,7 +387,6 @@ class TestFileTiff:
# Arrange
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
-
# v2 interface
v2_tags = {
256: 55,
@@ -630,7 +624,6 @@ class TestFileTiff:
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
with Image.open(filename) as im:
-
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
assert im.tag[Y_RESOLUTION][0][0] == 36
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 48797ea08..fdabae3a3 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path):
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
-
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
@@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path):
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
-
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata():
with Image.open("Tests/images/hopper_g4.tif") as img:
-
assert {
"YResolution": IFDRational(4294967295, 113653537),
"PlanarConfiguration": 1,
@@ -202,14 +199,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path):
assert reloaded.tag_v2[271] == expected
-def test_writing_int_to_bytes(tmp_path):
+@pytest.mark.parametrize("value", (1, IFDRational(1)))
+def test_writing_other_types_to_bytes(value, tmp_path):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[700]
assert tag.type == TiffTags.BYTE
- info[700] = 1
+ info[700] = value
out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info)
@@ -218,6 +216,22 @@ def test_writing_int_to_bytes(tmp_path):
assert reloaded.tag_v2[700] == b"\x01"
+def test_writing_other_types_to_undefined(tmp_path):
+ im = hopper()
+ info = TiffImagePlugin.ImageFileDirectory_v2()
+
+ tag = TiffTags.TAGS_V2[33723]
+ assert tag.type == TiffTags.UNDEFINED
+
+ info[33723] = 1
+
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.tag_v2[33723] == b"1"
+
+
def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059]
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index 4f513d82b..037479f9f 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -18,10 +18,8 @@ except ImportError:
def test_read_exif_metadata():
-
file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image:
-
assert image.format == "WEBP"
exif_data = image.info.get("exif", None)
assert exif_data
@@ -64,10 +62,8 @@ def test_write_exif_metadata():
def test_read_icc_profile():
-
file_path = "Tests/images/flower2.webp"
with Image.open(file_path) as image:
-
assert image.format == "WEBP"
assert image.info.get("icc_profile", None)
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index 439cb15bc..7c8b54fd1 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -6,7 +6,6 @@ from .helper import assert_image_similar_tofile, hopper
def test_load_raw():
-
# Test basic EMF open and rendering
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py
index 9c54c6755..d2c05b78a 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -44,7 +44,6 @@ def test_open():
# Act
with Image.open(filename) as im:
-
# Assert
assert im.mode == "1"
assert im.size == (128, 128)
@@ -57,7 +56,6 @@ def test_open_filename_with_underscore():
# Act
with Image.open(filename) as im:
-
# Assert
assert im.mode == "1"
assert im.size == (128, 128)
diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py
index ae53d2b63..9efe7ec14 100644
--- a/Tests/test_file_xvthumb.py
+++ b/Tests/test_file_xvthumb.py
@@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/hopper.p7"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
-
# Assert
assert im.format == "XVThumb"
diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py
new file mode 100644
index 000000000..27663f396
--- /dev/null
+++ b/Tests/test_font_crash.py
@@ -0,0 +1,22 @@
+import pytest
+
+from PIL import Image, ImageDraw, ImageFont
+
+from .helper import skip_unless_feature
+
+
+class TestFontCrash:
+ def _fuzz_font(self, font):
+ # from fuzzers.fuzz_font
+ font.getbbox("ABC")
+ font.getmask("test text")
+ with Image.new(mode="RGBA", size=(200, 200)) as im:
+ draw = ImageDraw.Draw(im)
+ draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
+ draw.text((10, 10), "Test Text", font=font, fill="#000")
+
+ @skip_unless_feature("freetype2")
+ def test_segfault(self):
+ with pytest.raises(OSError):
+ font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
+ self._fuzz_font(font)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index a37c90296..85e3ff55b 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -69,7 +69,6 @@ class TestImage:
assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self):
-
im = Image.new("L", (100, 100))
assert repr(im)[:45] == ">> im.save('Tests/images/hopper_45.png')
with Image.open("Tests/images/hopper_45.png") as target:
- for (resample, epsilon) in (
+ for resample, epsilon in (
(Image.Resampling.NEAREST, 10),
(Image.Resampling.BILINEAR, 5),
(Image.Resampling.BICUBIC, 0),
diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py
index 178cfcef3..a12ce329f 100644
--- a/Tests/test_image_tobitmap.py
+++ b/Tests/test_image_tobitmap.py
@@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper
def test_sanity():
-
with pytest.raises(ValueError):
hopper().tobitmap()
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index a78349801..64a5c9459 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -42,12 +42,12 @@ class TestImageTransform:
def test_extent(self):
im = hopper("RGB")
(w, h) = im.size
- # fmt: off
- transformed = im.transform(im.size, Image.Transform.EXTENT,
- (0, 0,
- w//2, h//2), # ul -> lr
- Image.Resampling.BILINEAR)
- # fmt: on
+ transformed = im.transform(
+ im.size,
+ Image.Transform.EXTENT,
+ (0, 0, w // 2, h // 2), # ul -> lr
+ Image.Resampling.BILINEAR,
+ )
scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h))
@@ -58,13 +58,12 @@ class TestImageTransform:
# one simple quad transform, equivalent to scale & crop upper left quad
im = hopper("RGB")
(w, h) = im.size
- # fmt: off
- transformed = im.transform(im.size, Image.Transform.QUAD,
- (0, 0, 0, h//2,
- # ul -> ccw around quad:
- w//2, h//2, w//2, 0),
- Image.Resampling.BILINEAR)
- # fmt: on
+ transformed = im.transform(
+ im.size,
+ Image.Transform.QUAD,
+ (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad
+ Image.Resampling.BILINEAR,
+ )
scaled = im.transform(
(w, h),
@@ -99,16 +98,21 @@ class TestImageTransform:
# this should be a checkerboard of halfsized hoppers in ul, lr
im = hopper("RGBA")
(w, h) = im.size
- # fmt: off
- transformed = im.transform(im.size, Image.Transform.MESH,
- [((0, 0, w//2, h//2), # box
- (0, 0, 0, h,
- w, h, w, 0)), # ul -> ccw around quad
- ((w//2, h//2, w, h), # box
- (0, 0, 0, h,
- w, h, w, 0))], # ul -> ccw around quad
- Image.Resampling.BILINEAR)
- # fmt: on
+ transformed = im.transform(
+ im.size,
+ Image.Transform.MESH,
+ (
+ (
+ (0, 0, w // 2, h // 2), # box
+ (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad
+ ),
+ (
+ (w // 2, h // 2, w, h), # box
+ (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad
+ ),
+ ),
+ Image.Resampling.BILINEAR,
+ )
scaled = im.transform(
(w // 2, h // 2),
@@ -174,11 +178,13 @@ class TestImageTransform:
im = op(im, (40, 10))
- colors = im.getcolors()
- assert colors == [
- (20 * 10, opaque),
- (20 * 10, transparent),
- ]
+ colors = sorted(im.getcolors())
+ assert colors == sorted(
+ (
+ (20 * 10, opaque),
+ (20 * 10, transparent),
+ )
+ )
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode):
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index b839a7b14..d0fea3854 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -50,7 +50,6 @@ def test_add():
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
-
# Act
new = ImageChops.add(im1, im2)
@@ -63,7 +62,6 @@ def test_add_scale_offset():
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
-
# Act
new = ImageChops.add(im1, im2, scale=2.5, offset=100)
@@ -87,7 +85,6 @@ def test_add_modulo():
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
-
# Act
new = ImageChops.add_modulo(im1, im2)
@@ -111,7 +108,6 @@ def test_blend():
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
-
# Act
new = ImageChops.blend(im1, im2, 0.5)
@@ -137,7 +133,6 @@ def test_darker_image():
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
-
# Act
new = ImageChops.darker(im1, im2)
@@ -149,7 +144,6 @@ def test_darker_pixel():
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
-
# Act
new = ImageChops.darker(im1, im2)
@@ -161,7 +155,6 @@ def test_difference():
# Arrange
with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1:
with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2:
-
# Act
new = ImageChops.difference(im1, im2)
@@ -173,7 +166,6 @@ def test_difference_pixel():
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2:
-
# Act
new = ImageChops.difference(im1, im2)
@@ -195,7 +187,6 @@ def test_duplicate():
def test_invert():
# Arrange
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im:
-
# Act
new = ImageChops.invert(im)
@@ -209,7 +200,6 @@ def test_lighter_image():
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
-
# Act
new = ImageChops.lighter(im1, im2)
@@ -221,7 +211,6 @@ def test_lighter_pixel():
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
-
# Act
new = ImageChops.lighter(im1, im2)
@@ -275,7 +264,6 @@ def test_offset():
xoffset = 45
yoffset = 20
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im:
-
# Act
new = ImageChops.offset(im, xoffset, yoffset)
@@ -292,7 +280,6 @@ def test_screen():
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
-
# Act
new = ImageChops.screen(im1, im2)
@@ -305,7 +292,6 @@ def test_subtract():
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
-
# Act
new = ImageChops.subtract(im1, im2)
@@ -319,7 +305,6 @@ def test_subtract_scale_offset():
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
-
# Act
new = ImageChops.subtract(im1, im2, scale=2.5, offset=100)
@@ -332,7 +317,6 @@ def test_subtract_clip():
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
-
# Act
new = ImageChops.subtract(im1, im2)
@@ -344,7 +328,6 @@ def test_subtract_modulo():
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
-
# Act
new = ImageChops.subtract_modulo(im1, im2)
@@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip():
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
-
# Act
new = ImageChops.subtract_modulo(im1, im2)
@@ -370,7 +352,6 @@ def test_soft_light():
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
-
# Act
new = ImageChops.soft_light(im1, im2)
@@ -383,7 +364,6 @@ def test_hard_light():
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
-
# Act
new = ImageChops.hard_light(im1, im2)
@@ -396,7 +376,6 @@ def test_overlay():
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
-
# Act
new = ImageChops.overlay(im1, im2)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 4c4c41b7b..d4723c924 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -52,7 +52,6 @@ def test_sanity():
def test_valueerror():
with Image.open("Tests/images/chi.gif") as im:
-
draw = ImageDraw.Draw(im)
draw.line((0, 0), fill=(0, 0, 0))
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index fc0fbfb9b..412bc10d9 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile:
def test_parser(self):
def roundtrip(format):
-
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
im = im.convert("1")
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index 317db4c01..fa88065f4 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -64,7 +64,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
)
p.communicate()
else:
- if not shutil.which("wl-paste"):
+ if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises(
NotImplementedError,
match="wl-paste or xclip is required for"
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index c9b2fd865..d390f3c1e 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -21,7 +21,6 @@ deformer = Deformer()
def test_sanity():
-
ImageOps.autocontrast(hopper("L"))
ImageOps.autocontrast(hopper("RGB"))
@@ -419,7 +418,6 @@ def test_autocontrast_cutoff():
def test_autocontrast_mask_toy_input():
# Test the mask argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
-
rect_mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(rect_mask)
x0 = img.size[0] // 4
@@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input():
def test_autocontrast_mask_real_input():
# Test the autocontrast with a rectangular mask
with Image.open("Tests/images/iptc.jpg") as img:
-
rect_mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(rect_mask)
x0, y0 = img.size[0] // 2, img.size[1] // 2
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index 5bda28117..ac99ef381 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile
def test_sanity():
-
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256
@@ -23,7 +22,6 @@ def test_reload():
def test_getcolor():
-
palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0
assert len(palette.colors) == 0
@@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette):
def test_file(tmp_path):
-
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
f = str(tmp_path / "temp.lut")
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index de3920cf5..8f8a9f449 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -8,7 +8,6 @@ from PIL import Image, ImagePath
def test_path():
-
p = ImagePath.Path(list(range(10)))
# sequence interface
@@ -58,10 +57,7 @@ def test_path():
assert list(p) == [(0.0, 1.0)]
arr = array.array("f", [0, 1])
- if hasattr(arr, "tobytes"):
- p = ImagePath.Path(arr.tobytes())
- else:
- p = ImagePath.Path(arr.tostring())
+ p = ImagePath.Path(arr.tobytes())
assert list(p) == [(0.0, 1.0)]
diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py
index 6af7e7602..62f528332 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature
def test_sanity(tmp_path):
-
test_file = str(tmp_path / "temp.im")
im = hopper("RGB")
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index 5717fe150..b3b5db13f 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -6,7 +6,6 @@ from .helper import hopper
def test_sanity():
-
im = hopper()
st = ImageStat.Stat(im)
@@ -31,7 +30,6 @@ def test_sanity():
def test_hopper():
-
im = hopper()
st = ImageStat.Stat(im)
@@ -45,7 +43,6 @@ def test_hopper():
def test_constant():
-
im = Image.new("L", (128, 128), 128)
st = ImageStat.Stat(im)
diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py
index 37ed3659d..f6818be46 100644
--- a/Tests/test_lib_image.py
+++ b/Tests/test_lib_image.py
@@ -4,7 +4,6 @@ from PIL import Image
def test_setmode():
-
im = Image.new("L", (1, 1), 255)
im.im.setmode("1")
assert im.im.getpixel((0, 0)) == 255
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index efcdab9ec..dcdee3d41 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -42,7 +42,6 @@ def test_basic(tmp_path, mode):
im_in.save(filename)
with Image.open(filename) as im_out:
-
verify(im_in)
verify(im_out)
@@ -87,7 +86,6 @@ def test_tobytes():
def test_convert():
-
im = original.copy()
verify(im.convert("I;16"))
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 3de7ec30f..a8bbcbdb8 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array():
test_file = "Tests/images/hopper.png"
with Image.open(test_file) as im:
-
# Act/Assert
with warnings.catch_warnings():
array(im)
diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index ea9b33dfc..43e244c7b 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -88,9 +88,8 @@ def test_parsing():
b"D:20180729214124+08'00'": "20180729134124",
b"D:20180729214124-05'00'": "20180730024124",
}.items():
- d = PdfParser.get_value(b"<" + name.encode() + b" (" + date + b")>>", 0)[
- 0
- ]
+ b = b"<" + name.encode() + b" (" + date + b")>>"
+ d = PdfParser.get_value(b, 0)[0]
assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index 23eb9e39f..2f6d05888 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path):
def test_pickle_tell():
# Arrange
with Image.open("Tests/images/hopper.webp") as image:
-
# Act: roundtrip
unpickled_image = pickle.loads(pickle.dumps(image))
diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py
index 1fc816146..4929fa933 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -6,7 +6,7 @@ with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
-from .helper import assert_image_equal, assert_image_equal_tofile, hopper
+from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@@ -48,7 +48,7 @@ if ImageQt.qt_is_installed:
def roundtrip(expected):
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb
- assert_image_equal(result, expected.convert("RGB"))
+ assert_image_similar(result, expected.convert("RGB"), 1)
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index 12f475df0..6e3fcec90 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -7,7 +7,6 @@ from .helper import hopper
def _test_equal(num, denom, target):
-
t = IFDRational(num, denom)
assert target == t
@@ -15,7 +14,6 @@ def _test_equal(num, denom, target):
def test_sanity():
-
_test_equal(1, 1, 1)
_test_equal(1, 1, Fraction(1, 1))
diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py
index 34197c14f..5bd9bacdb 100644
--- a/Tests/test_webp_leaks.py
+++ b/Tests/test_webp_leaks.py
@@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp"
@skip_unless_feature("webp")
class TestWebPLeaks(PillowLeakTestCase):
-
mem_limit = 3 * 1024 # kb
iterations = 100
diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh
index d9608e782..a318bfafd 100755
--- a/depends/download-and-extract.sh
+++ b/depends/download-and-extract.sh
@@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then
wget -O $archive.tar.gz $url
fi
-rm -r $archive
+rmdir $archive
tar -xvzf $archive.tar.gz
diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh
index 02da12d61..1ef6f4e97 100755
--- a/depends/install_extra_test_images.sh
+++ b/depends/install_extra_test_images.sh
@@ -1,15 +1,12 @@
-#!/bin/bash
+#!/usr/bin/env bash
# install extra test images
-# Use SVN to just fetch a single Git subdirectory
-svn_export()
-{
- if [ ! -z $1 ]; then
- echo ""
- echo "Retrying svn export..."
- echo ""
- fi
+archive=test-images-main
- svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images
-}
-svn_export || svn_export retry || svn_export retry || svn_export retry
+./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz
+
+mv $archive/* ../Tests/images/
+
+# Cleanup old tarball and empty directory
+rm $archive.tar.gz
+rmdir $archive
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index 64dd024bd..8b847b894 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install libimagequant
-archive=libimagequant-4.0.4
+archive=libimagequant-4.1.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh
index 992503650..d1b31cfa5 100755
--- a/depends/install_raqm.sh
+++ b/depends/install_raqm.sh
@@ -2,7 +2,7 @@
# install raqm
-archive=libraqm-0.9.0
+archive=libraqm-0.10.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/depends/install_webp.sh b/depends/install_webp.sh
index 05867b7d4..f8b985a7a 100755
--- a/depends/install_webp.sh
+++ b/depends/install_webp.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install webp
-archive=libwebp-1.2.4
+archive=libwebp-1.3.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/docs/COPYING b/docs/COPYING
index 25f03b343..bc44ba388 100644
--- a/docs/COPYING
+++ b/docs/COPYING
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2022 by Alex Clark and contributors
+ Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:
diff --git a/docs/conf.py b/docs/conf.py
index 04823e2d7..e1ffa49b8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -52,8 +52,10 @@ master_doc = "index"
# General information about the project.
project = "Pillow (PIL Fork)"
-copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors"
-author = "Fredrik Lundh, Alex Clark and Contributors"
+copyright = (
+ "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
+)
+author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -243,7 +245,7 @@ latex_documents = [
master_doc,
"PillowPILFork.tex",
"Pillow (PIL Fork) Documentation",
- "Alex Clark",
+ "Jeffrey A. Clark (Alex)",
"manual",
)
]
@@ -293,7 +295,7 @@ texinfo_documents = [
"Pillow (PIL Fork) Documentation",
author,
"PillowPILFork",
- "Pillow is the friendly PIL fork by Alex Clark and Contributors.",
+ "Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.",
"Miscellaneous",
)
]
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index dec652df8..4d48b822a 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -74,40 +74,18 @@ Constants
A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
+.. note::
+
+ Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that
+ was reversed in Pillow 9.4.0 and those constants will now remain available.
+ See :ref:`restored-image-constants`
+
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
-``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
-``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
-``Image.ORDERED`` ``Image.Dither.ORDERED``
-``Image.RASTERIZE`` ``Image.Dither.RASTERIZE``
-``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG``
-``Image.WEB`` ``Image.Palette.WEB``
-``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE``
-``Image.AFFINE`` ``Image.Transform.AFFINE``
-``Image.EXTENT`` ``Image.Transform.EXTENT``
-``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE``
-``Image.QUAD`` ``Image.Transform.QUAD``
-``Image.MESH`` ``Image.Transform.MESH``
-``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT``
-``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM``
-``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90``
-``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180``
-``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270``
-``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
-``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
-``Image.BOX`` ``Image.Resampling.BOX``
-``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
-``Image.LINEAR`` ``Image.Resampling.BILINEAR``
-``Image.HAMMING`` ``Image.Resampling.HAMMING``
-``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
-``Image.CUBIC`` ``Image.Resampling.BICUBIC``
-``Image.LANCZOS`` ``Image.Resampling.LANCZOS``
-``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS``
-``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT``
-``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE``
-``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE``
-``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT``
+``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR``
+``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC``
+``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS``
``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL``
``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst
index 01f75e9a3..0aa2f1119 100644
--- a/docs/handbook/concepts.rst
+++ b/docs/handbook/concepts.rst
@@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re
supports the following standard modes:
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
- * ``L`` (8-bit pixels, black and white)
+ * ``L`` (8-bit pixels, grayscale)
* ``P`` (8-bit pixels, mapped to any other mode using a color palette)
* ``RGB`` (3x8-bit pixels, true color)
* ``RGBA`` (4x8-bit pixels, true color with transparency mask)
@@ -148,44 +148,44 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. py:currentmodule:: PIL.Image
-.. data:: NEAREST
+.. data:: Resampling.NEAREST
Pick one nearest pixel from the input image. Ignore all other input pixels.
-.. data:: BOX
+.. data:: Resampling.BOX
Each pixel of source image contributes to one pixel of the
destination image with identical weights.
- For upscaling is equivalent of :data:`NEAREST`.
+ For upscaling is equivalent of :data:`Resampling.NEAREST`.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0
-.. data:: BILINEAR
+.. data:: Resampling.BILINEAR
For resize calculate the output pixel value using linear interpolation
on all pixels that may contribute to the output value.
For other transformations linear interpolation over a 2x2 environment
in the input image is used.
-.. data:: HAMMING
+.. data:: Resampling.HAMMING
- Produces a sharper image than :data:`BILINEAR`, doesn't have dislocations
- on local level like with :data:`BOX`.
+ Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
+ dislocations on local level like with :data:`Resampling.BOX`.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0
-.. data:: BICUBIC
+.. data:: Resampling.BICUBIC
For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value.
For other transformations cubic interpolation over a 4x4 environment
in the input image is used.
-.. data:: LANCZOS
+.. data:: Resampling.LANCZOS
Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value.
@@ -198,19 +198,19 @@ pixel, the Python Imaging Library provides different resampling *filters*.
Filters comparison table
~~~~~~~~~~~~~~~~~~~~~~~~
-+----------------+-------------+-----------+-------------+
-| Filter | Downscaling | Upscaling | Performance |
-| | quality | quality | |
-+================+=============+===========+=============+
-|:data:`NEAREST` | | | ⭐⭐⭐⭐⭐ |
-+----------------+-------------+-----------+-------------+
-|:data:`BOX` | ⭐ | | ⭐⭐⭐⭐ |
-+----------------+-------------+-----------+-------------+
-|:data:`BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ |
-+----------------+-------------+-----------+-------------+
-|:data:`HAMMING` | ⭐⭐ | | ⭐⭐⭐ |
-+----------------+-------------+-----------+-------------+
-|:data:`BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
-+----------------+-------------+-----------+-------------+
-|:data:`LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
-+----------------+-------------+-----------+-------------+
++---------------------------+-------------+-----------+-------------+
+| Filter | Downscaling | Upscaling | Performance |
+| | quality | quality | |
++===========================+=============+===========+=============+
+|:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ |
++---------------------------+-------------+-----------+-------------+
+|:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ |
++---------------------------+-------------+-----------+-------------+
+|:data:`Resampling.BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ |
++---------------------------+-------------+-----------+-------------+
+|:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ |
++---------------------------+-------------+-----------+-------------+
+|:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
++---------------------------+-------------+-----------+-------------+
+|:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
++---------------------------+-------------+-----------+-------------+
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index a41ef7cf8..6058149cb 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo.
Either an integer or a float.
**dpi**
- A tuple of (x_resolution, y_resolution), with inches as the resolution
+ A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution
unit. For consistency with other image formats, the x and y resolutions
of the dpi will be rounded to the nearest integer.
@@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If present and true, instructs the WebP writer to use lossless compression.
**quality**
- Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest
+ Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest
size and 100 the largest. For lossless, this parameter is the amount
of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100.
@@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
The exif data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
+**xmp**
+ The XMP data to include in the saved file. Only supported if
+ the system WebP library was built with webpmux support.
+
Saving sequences
~~~~~~~~~~~~~~~~
@@ -1493,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
image, will determine the physical dimensions of the page that will be
saved in the PDF.
+**dpi**
+ A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution
+ unit. If both the ``resolution`` parameter and the ``dpi`` parameter are
+ present, ``resolution`` will be ignored.
+
**title**
The document’s title. If not appending to an existing PDF file, this will
default to the filename.
diff --git a/docs/index.rst b/docs/index.rst
index 5bcd5afa5..418844ba7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,7 +1,7 @@
Pillow
======
-Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
+Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_.
@@ -73,6 +73,22 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
+
Overview
========
diff --git a/docs/installation.rst b/docs/installation.rst
index 42fe8c254..1b5719a8e 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
- * Pillow has been tested with libimagequant **2.6-4.0.4**
+ * Pillow has been tested with libimagequant **2.6-4.1**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@@ -369,21 +369,21 @@ Build Options
available, as many as are present.
* Build flags: ``--disable-zlib``, ``--disable-jpeg``,
- ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``,
- ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``,
- ``--disable-imagequant``, ``--disable-xcb``.
+ ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``,
+ ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``,
+ ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``.
Disable building the corresponding feature even if the development
libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``,
- ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``,
- ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``,
- ``--enable-imagequant``, ``--enable-xcb``.
+ ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``,
+ ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``,
+ ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``.
Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
-* Build flags: ``--vendor-raqm --vendor-fribidi``
+* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``.
These flags are used to compile a modified version of libraqm and
a shim that dynamically loads libfribidi at runtime. These are
used to compile the standard Pillow wheels. Compiling libraqm requires
@@ -442,21 +442,23 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | PyPy3 | |
+| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
-| | PyPy3 | |
+| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
-| | | s390x, x86-64 |
+| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
+| | 3.12, PyPy3 | |
+| +----------------------------+---------------------+
+| | 3.10 | arm64v8, ppc64le, |
+| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 |
-| | PyPy3 | |
+| | 3.12, PyPy3 | |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+
@@ -478,13 +480,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
-| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+
-| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 |
+| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
| +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 7f6f666c3..ad0abbbd9 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -430,6 +430,7 @@ See :ref:`concept-filters` for details.
.. autoclass:: Resampling
:members:
:undoc-members:
+ :noindex:
Some deprecated filters are also available under the following names:
diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst
index dda814c1f..0b3eeeb49 100644
--- a/docs/releasenotes/2.7.0.rst
+++ b/docs/releasenotes/2.7.0.rst
@@ -29,84 +29,78 @@ Image resizing filters
Image resizing methods :py:meth:`~PIL.Image.Image.resize` and
:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells
which filter should be used for resampling. Possible values are:
-:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`,
-:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`.
-Almost all of them were changed in this version.
+``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them
+were changed in this version.
Bicubic and bilinear downscaling
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-From the beginning :py:data:`~PIL.Image.BILINEAR` and
-:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations
-and used a fixed number of pixels from the source image for every destination
-pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for
-:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for
-downscaling. At the same time, a high quality convolutions-based algorithm with
-flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter.
+From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine
+transformations and used a fixed number of pixels from the source image for
+every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``).
+This gave an unsatisfactory result for downscaling. At the same time, a high
+quality convolutions-based algorithm with flexible kernel was used for
+``ANTIALIAS`` filter.
Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used
for all of these three filters.
If you have previously used any tricks to maintain quality when downscaling with
-:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters
-(for example, reducing within several steps), they are unnecessary now.
+``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several
+steps), they are unnecessary now.
Antialias renamed to Lanczos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-A new :py:data:`PIL.Image.LANCZOS` constant was added instead of
-:py:data:`~PIL.Image.ANTIALIAS`.
+A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``.
-When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only
-high-quality filter based on convolutions. It's name was supposed to reflect
-this. Starting from Pillow 2.7.0 all resize method are based on convolutions.
-All of them are antialias from now on. And the real name of the
-:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter.
+When ``ANTIALIAS`` was initially added, it was the only high-quality filter
+based on convolutions. It's name was supposed to reflect this. Starting from
+Pillow 2.7.0 all resize method are based on convolutions. All of them are
+antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos
+filter.
-The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility
-and is an alias for :py:data:`~PIL.Image.LANCZOS`.
+The ``ANTIALIAS`` constant is left for backward compatibility and is an alias
+for ``LANCZOS``.
Lanczos upscaling quality
^^^^^^^^^^^^^^^^^^^^^^^^^
-The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was
-almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed.
+The image upscaling quality with ``LANCZOS`` filter was almost the same as
+``BILINEAR`` due to a bug. This has been fixed.
Bicubic upscaling quality
^^^^^^^^^^^^^^^^^^^^^^^^^
-The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced
-sharp, slightly pixelated image for upscaling. Bicubic for convolutions is
-more soft.
+The ``BICUBIC`` filter for affine transformations produced sharp, slightly
+pixelated image for upscaling. Bicubic for convolutions is more soft.
Resize performance
^^^^^^^^^^^^^^^^^^
In most cases, convolution is more a expensive algorithm for downscaling
because it takes into account all the pixels of source image. Therefore
-:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters'
-performance can be lower than before. On the other hand the quality of
-:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to
-:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks
-you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling,
-which will give a huge improvement in performance.
+``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before.
+On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to
+``NEAREST``. So if such quality is suitable for your tasks you can switch to
+``NEAREST`` filter for downscaling, which will give a huge improvement in
+performance.
At the same time performance of convolution resampling for downscaling has been
improved by around a factor of two compared to the previous version.
-The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has
-remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by
-1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times.
+The upscaling performance of the ``LANCZOS`` filter has remained the same. For
+``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four
+times.
Default filter for thumbnails
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was
-changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`.
-Antialias was chosen because all the other filters gave poor quality for
-reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been
-replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and
-:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after
-downscaling with libjpeg, which uses supersampling internally, not convolutions.
+changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the
+other filters gave poor quality for reduction. Starting from Pillow 2.7.0,
+``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and
+``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which
+uses supersampling internally, not convolutions.
Image transposition
-------------------
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 48ce6fef7..e97b58a41 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -53,6 +53,11 @@ Constants
A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
+.. note::
+
+ Some of these deprecations were restored in Pillow 9.4.0. See
+ :ref:`restored-image-constants`
+
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst
index e4e1e40fe..0af5bc8ca 100644
--- a/docs/releasenotes/9.4.0.rst
+++ b/docs/releasenotes/9.4.0.rst
@@ -1,30 +1,6 @@
9.4.0
-----
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
-TODO
-
-Deprecations
-============
-
-TODO
-^^^^
-
-TODO
-
-API Changes
-===========
-
-TODO
-^^^^
-
-TODO
-
API Additions
=============
@@ -96,10 +72,21 @@ When saving a JPEG image, a comment can now be written from
Security
========
-TODO
-^^^^
+Fix memory DOS in ImageFont
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+A corrupt or specially crafted TTF font could have font metrics that lead to
+unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not
+check the image size before allocating memory for it. This dates to the PIL
+fork. Pillow 8.2.0 added a check for large sizes, but did not consider the
+case where one dimension is zero.
+
+Null pointer dereference crash in ImageFont
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a
+crash. An error is now raised instead. This has been present since
+Pillow 8.0.0.
Other Changes
=============
@@ -109,3 +96,40 @@ Added support for DDS L and LA images
Support has been added to read and write L and LA DDS images in the uncompressed
format, known as "luminance" textures.
+
+.. _restored-image-constants:
+
+Constants
+^^^^^^^^^
+
+In Pillow 9.1.0, the following constants were deprecated. That has been reversed and
+these constants will now remain available.
+
+- ``Image.NONE``
+- ``Image.NEAREST``
+- ``Image.ORDERED``
+- ``Image.RASTERIZE``
+- ``Image.FLOYDSTEINBERG``
+- ``Image.WEB``
+- ``Image.ADAPTIVE``
+- ``Image.AFFINE``
+- ``Image.EXTENT``
+- ``Image.PERSPECTIVE``
+- ``Image.QUAD``
+- ``Image.MESH``
+- ``Image.FLIP_LEFT_RIGHT``
+- ``Image.FLIP_TOP_BOTTOM``
+- ``Image.ROTATE_90``
+- ``Image.ROTATE_180``
+- ``Image.ROTATE_270``
+- ``Image.TRANSPOSE``
+- ``Image.TRANSVERSE``
+- ``Image.BOX``
+- ``Image.BILINEAR``
+- ``Image.HAMMING``
+- ``Image.BICUBIC``
+- ``Image.LANCZOS``
+- ``Image.MEDIANCUT``
+- ``Image.MAXCOVERAGE``
+- ``Image.FASTOCTREE``
+- ``Image.LIBIMAGEQUANT``
diff --git a/setup.cfg b/setup.cfg
index b562e2934..824cae088 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,8 +4,8 @@ description = Python Imaging Library (Fork)
long_description = file: README.md
long_description_content_type = text/markdown
url = https://python-pillow.org
-author = Alex Clark (PIL Fork Author)
-author_email = aclark@python-pillow.org
+author = Jeffrey A. Clark (Alex)
+author_email = aclark@aclark.net
license = HPND
classifiers =
Development Status :: 6 - Mature
@@ -32,6 +32,7 @@ project_urls =
Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
Twitter=https://twitter.com/PythonPillow
+ Mastodon=https://fosstodon.org/@pillow
[options]
packages = PIL
diff --git a/setup.py b/setup.py
index 243365681..8f7f223f8 100755
--- a/setup.py
+++ b/setup.py
@@ -263,18 +263,18 @@ def _pkg_config(name):
if not DEBUG:
command_libs.append("--silence-errors")
command_cflags.append("--silence-errors")
- libs = (
+ libs = re.split(
+ r"(^|\s+)-L",
subprocess.check_output(command_libs, stderr=stderr)
.decode("utf8")
- .strip()
- .replace("-L", "")
- )
- cflags = (
- subprocess.check_output(command_cflags)
+ .strip(),
+ )[::2][1:]
+ cflags = re.split(
+ r"(^|\s+)-I",
+ subprocess.check_output(command_cflags, stderr=stderr)
.decode("utf8")
- .strip()
- .replace("-I", "")
- )
+ .strip(),
+ )[::2][1:]
return libs, cflags
except Exception:
pass
@@ -430,7 +430,6 @@ class pil_build_ext(build_ext):
return sdk_path
def build_extensions(self):
-
library_dirs = []
include_dirs = []
@@ -473,8 +472,12 @@ class pil_build_ext(build_ext):
else:
lib_root = include_root = root
- _add_directory(library_dirs, lib_root)
- _add_directory(include_dirs, include_root)
+ if lib_root is not None:
+ for lib_dir in lib_root:
+ _add_directory(library_dirs, lib_dir)
+ if include_root is not None:
+ for include_dir in include_root:
+ _add_directory(include_dirs, include_dir)
# respect CFLAGS/CPPFLAGS/LDFLAGS
for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"):
@@ -913,7 +916,6 @@ class pil_build_ext(build_ext):
self.summary_report(feature)
def summary_report(self, feature):
-
print("-" * 68)
print("PIL SETUP SUMMARY")
print("-" * 68)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index e0498d2a8..9fe4be544 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -226,7 +226,6 @@ class BmpImageFile(ImageFile.ImageFile):
# --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
-
# ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536):
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
@@ -363,7 +362,6 @@ class BmpRleDecoder(ImageFile.PyDecoder):
# Image plugin for the DIB format (BMP alias)
# =============================================================================
class DibImageFile(BmpImageFile):
-
format = "DIB"
format_description = "Windows Bitmap"
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index a0da1b786..0425bbd75 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -33,12 +33,10 @@ def _accept(prefix):
class BufrStubImageFile(ImageFile.StubImageFile):
-
format = "BUFR"
format_description = "BUFR"
def _open(self):
-
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index 81c0314f0..cde9d42f0 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -37,13 +37,11 @@ def _accept(prefix):
class DcxImageFile(PcxImageFile):
-
format = "DCX"
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
def _open(self):
-
# Header
s = self.fp.read(4)
if not _accept(s):
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 016e3c135..60cb46df9 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -173,11 +173,13 @@ class PSFile:
self.fp.seek(offset, whence)
def readline(self):
- s = [self.char or b""]
- self.char = None
+ s = []
+ if self.char:
+ s.append(self.char)
+ self.char = None
c = self.fp.read(1)
- while (c not in b"\r\n") and len(c):
+ while (c not in b"\r\n") and len(c) and len(b"".join(s).strip(b"\r\n")) <= 255:
s.append(c)
c = self.fp.read(1)
@@ -284,7 +286,6 @@ class EpsImageFile(ImageFile.ImageFile):
# Scan for an "ImageData" descriptor
while s[:1] == "%":
-
if len(s) > 255:
msg = "not an EPS file"
raise SyntaxError(msg)
@@ -315,22 +316,22 @@ class EpsImageFile(ImageFile.ImageFile):
raise OSError(msg)
def _find_offset(self, fp):
+ s = fp.read(4)
- s = fp.read(160)
-
- if s[:4] == b"%!PS":
+ if s == b"%!PS":
# for HEAD without binary preview
fp.seek(0, io.SEEK_END)
length = fp.tell()
offset = 0
- elif i32(s, 0) == 0xC6D3D0C5:
+ elif i32(s) == 0xC6D3D0C5:
# FIX for: Some EPS file not handled correctly / issue #302
# EPS can contain binary data
# or start directly with latin coding
# more info see:
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
- offset = i32(s, 4)
- length = i32(s, 8)
+ s = fp.read(8)
+ offset = i32(s)
+ length = i32(s, 4)
else:
msg = "not an EPS file"
raise SyntaxError(msg)
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 536bc1fe6..1359aeb12 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -19,7 +19,6 @@ def _accept(prefix):
class FitsImageFile(ImageFile.ImageFile):
-
format = "FITS"
format_description = "FITS"
@@ -33,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile):
keyword = header[:8].strip()
if keyword == b"END":
break
- value = header[8:].strip()
+ value = header[8:].split(b"/")[0].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py
index 86eb2d5a2..50948ec42 100644
--- a/src/PIL/FitsStubImagePlugin.py
+++ b/src/PIL/FitsStubImagePlugin.py
@@ -44,7 +44,6 @@ def register_handler(handler):
class FITSStubImageFile(ImageFile.StubImageFile):
-
format = FitsImagePlugin.FitsImageFile.format
format_description = FitsImagePlugin.FitsImageFile.format_description
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 66681939d..f4e89a03e 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -40,13 +40,11 @@ def _accept(prefix):
class FliImageFile(ImageFile.ImageFile):
-
format = "FLI"
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
def _open(self):
-
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py
index c5fc80b37..5ec0a6632 100644
--- a/src/PIL/FontFile.py
+++ b/src/PIL/FontFile.py
@@ -36,7 +36,6 @@ class FontFile:
bitmap = None
def __init__(self):
-
self.info = {}
self.glyph = [None] * 256
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index 8ddc6b40b..d145d01f7 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -48,7 +48,6 @@ def _accept(prefix):
class FpxImageFile(ImageFile.ImageFile):
-
format = "FPX"
format_description = "FlashPix"
@@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile):
self.tile = []
for i in range(0, len(s), length):
-
x1 = min(xsize, x + xtile)
y1 = min(ysize, y + ytile)
@@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile):
)
elif compression == 1:
-
# FIXME: the fill decoder is not implemented
self.tile.append(
(
@@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile):
)
elif compression == 2:
-
internal_color_conversion = s[14]
jpeg_tables = s[15]
rawmode = self.rawmode
@@ -234,7 +230,6 @@ class FpxImageFile(ImageFile.ImageFile):
self.fp = None
def load(self):
-
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index 828a45ced..994a6e8eb 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -37,7 +37,6 @@ def _accept(prefix):
class GbrImageFile(ImageFile.ImageFile):
-
format = "GBR"
format_description = "GIMP brush file"
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index 3875dc866..7dda4f143 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile):
format_description = "GD uncompressed images"
def _open(self):
-
# Header
s = self.fp.read(1037)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index d01315b20..eadee1560 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -61,7 +61,6 @@ def _accept(prefix):
class GifImageFile(ImageFile.ImageFile):
-
format = "GIF"
format_description = "Compuserve GIF"
_close_exclusive_fp_after_loading = False
@@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile):
return False
def _open(self):
-
# Screen
s = self.fp.read(13)
if not _accept(s):
@@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile):
raise EOFError(msg) from e
def _seek(self, frame, update_image=True):
-
if frame == 0:
# rewind
self.__offset = 0
@@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile):
interlace = None
frame_dispose_extent = None
while True:
-
if not s:
s = self.fp.read(1)
if not s or s == b";":
@@ -487,7 +483,7 @@ def _normalize_mode(im):
if Image.getmodebase(im.mode) == "RGB":
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
if im.palette.mode == "RGBA":
- for rgba in im.palette.colors.keys():
+ for rgba in im.palette.colors:
if rgba[3] == 0:
im.info["transparency"] = im.palette.colors[rgba]
break
@@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame):
def _write_multiple_frames(im, fp, palette):
-
duration = im.encoderinfo.get("duration")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
@@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags):
def _save_netpbm(im, fp, filename):
-
# Unused by default.
# To use, uncomment the register_save call at the end of the file.
#
diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py
index b5c5e3ca4..8e801be0b 100644
--- a/src/PIL/GimpGradientFile.py
+++ b/src/PIL/GimpGradientFile.py
@@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
class GradientFile:
-
gradient = None
def getpalette(self, entries=256):
-
palette = []
ix = 0
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
for i in range(entries):
-
x = i / (entries - 1)
while x1 < x:
@@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format."""
def __init__(self, fp):
-
if fp.readline()[:13] != b"GIMP Gradient":
msg = "not a GIMP gradient file"
raise SyntaxError(msg)
@@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile):
gradient = []
for i in range(count):
-
s = fp.readline().split()
w = [float(x) for x in s[:11]]
diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py
index 2e9cbe58d..d38892894 100644
--- a/src/PIL/GimpPaletteFile.py
+++ b/src/PIL/GimpPaletteFile.py
@@ -25,7 +25,6 @@ class GimpPaletteFile:
rawmode = "RGB"
def __init__(self, fp):
-
self.palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
@@ -33,7 +32,6 @@ class GimpPaletteFile:
raise SyntaxError(msg)
for i in range(256):
-
s = fp.readline()
if not s:
break
@@ -55,5 +53,4 @@ class GimpPaletteFile:
self.palette = b"".join(self.palette)
def getpalette(self):
-
return self.palette, self.rawmode
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index 2088eb7b0..8a799f19c 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -33,12 +33,10 @@ def _accept(prefix):
class GribStubImageFile(ImageFile.StubImageFile):
-
format = "GRIB"
format_description = "GRIB"
def _open(self):
-
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index d6f283739..bba05ed65 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -33,12 +33,10 @@ def _accept(prefix):
class HDF5StubImageFile(ImageFile.StubImageFile):
-
format = "HDF5"
format_description = "HDF5"
def _open(self):
-
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index e76d0c35a..c2f050edd 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size):
class IcnsFile:
-
SIZES = {
(512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
(512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
@@ -189,7 +188,7 @@ class IcnsFile:
def itersizes(self):
sizes = []
for size, fmts in self.SIZES.items():
- for (fmt, reader) in fmts:
+ for fmt, reader in fmts:
if fmt in self.dct:
sizes.append(size)
break
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 568e6d38d..a188f8fdc 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -185,7 +185,7 @@ class IcoFile:
return {(h["width"], h["height"]) for h in self.entry}
def getentryindex(self, size, bpp=False):
- for (i, h) in enumerate(self.entry):
+ for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
return i
return 0
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 875a20326..746743f65 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -115,13 +115,11 @@ def number(s):
class ImImageFile(ImageFile.ImageFile):
-
format = "IM"
format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False
def _open(self):
-
# Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header.
@@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile):
self.rawmode = "L"
while True:
-
s = self.fp.read(1)
# Some versions of IFUNC uses \n\r instead of \r\n...
@@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile):
raise SyntaxError(msg) from e
if m:
-
k, v = m.group(1, 2)
# Don't know if this is the correct encoding,
@@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile):
n += 1
else:
-
msg = "Syntax error in IM header: " + s.decode("ascii", "replace")
raise SyntaxError(msg)
@@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
-
# ifunc95 formats
try:
# use bit decoder (if necessary)
@@ -332,7 +326,6 @@ SAVE = {
def _save(im, fp, filename):
-
try:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index b22060965..63bad83a1 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -65,21 +65,16 @@ def __getattr__(name):
if name in categories:
deprecate("Image categories", 10, "is_animated", plural=True)
return categories[name]
- elif name in ("NEAREST", "NONE"):
- deprecate(name, 10, "Resampling.NEAREST or Dither.NONE")
- return 0
old_resampling = {
"LINEAR": "BILINEAR",
"CUBIC": "BICUBIC",
"ANTIALIAS": "LANCZOS",
}
if name in old_resampling:
- deprecate(name, 10, f"Resampling.{old_resampling[name]}")
+ deprecate(
+ name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}"
+ )
return Resampling[old_resampling[name]]
- for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
- if name in enum.__members__:
- deprecate(name, 10, f"{enum.__name__}.{name}")
- return enum[name]
msg = f"module '{__name__}' has no attribute '{name}'"
raise AttributeError(msg)
@@ -158,6 +153,7 @@ def isImageType(t):
#
# Constants
+
# transpose
class Transpose(IntEnum):
FLIP_LEFT_RIGHT = 0
@@ -218,6 +214,12 @@ class Quantize(IntEnum):
LIBIMAGEQUANT = 3
+module = sys.modules[__name__]
+for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
+ for item in enum:
+ setattr(module, item.name, item.value)
+
+
if hasattr(core, "DEFAULT_STRATEGY"):
DEFAULT_STRATEGY = core.DEFAULT_STRATEGY
FILTERED = core.FILTERED
@@ -390,7 +392,6 @@ def init():
def _getdecoder(mode, decoder_name, args, extra=()):
-
# tweak arguments
if args is None:
args = ()
@@ -414,7 +415,6 @@ def _getdecoder(mode, decoder_name, args, extra=()):
def _getencoder(mode, encoder_name, args, extra=()):
-
# tweak arguments
if args is None:
args = ()
@@ -1270,7 +1270,8 @@ class Image:
currently implemented only for JPEG and MPO images.
:param mode: The requested mode.
- :param size: The requested size.
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
"""
pass
@@ -1431,6 +1432,11 @@ class Image:
return {get_name(root.tag): get_value(root)}
def getexif(self):
+ """
+ Gets EXIF data from the image.
+
+ :returns: an :py:class:`~PIL.Image.Exif` object.
+ """
if self._exif is None:
self._exif = Exif()
self._exif._loaded = False
@@ -2550,7 +2556,8 @@ class Image:
apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original
image.
- :param size: Requested size.
+ :param size: The requested size in pixels, as a 2-tuple:
+ (width, height).
:param resample: Optional resampling filter. This can be one
of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
@@ -2637,7 +2644,8 @@ class Image:
given size, and the same mode as the original, and copies data
to the new image using the given transform.
- :param size: The output size.
+ :param size: The output size in pixels, as a 2-tuple:
+ (width, height).
:param method: The transformation method. This is one of
:py:data:`Transform.EXTENT` (cut out a rectangular subregion),
:py:data:`Transform.AFFINE` (affine transform),
@@ -3263,9 +3271,15 @@ def open(fp, mode="r", formats=None):
im = _open_core(fp, filename, prefix, formats)
- if im is None:
+ if im is None and formats is ID:
+ checked_formats = formats.copy()
if init():
- im = _open_core(fp, filename, prefix, formats)
+ im = _open_core(
+ fp,
+ filename,
+ prefix,
+ tuple(format for format in formats if format not in checked_formats),
+ )
if im:
im._exclusive_fp = exclusive_fp
@@ -3396,7 +3410,8 @@ def register_open(id, factory, accept=None):
reject images having another format.
"""
id = id.upper()
- ID.append(id)
+ if id not in ID:
+ ID.append(id)
OPEN[id] = factory, accept
@@ -3591,6 +3606,39 @@ atexit.register(core.clear_cache)
class Exif(MutableMapping):
+ """
+ This class provides read and write access to EXIF image data::
+
+ from PIL import Image
+ im = Image.open("exif.png")
+ exif = im.getexif() # Returns an instance of this class
+
+ Information can be read and written, iterated over or deleted::
+
+ print(exif[274]) # 1
+ exif[274] = 2
+ for k, v in exif.items():
+ print("Tag", k, "Value", v) # Tag 274 Value 2
+ del exif[274]
+
+ To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd`
+ returns a dictionary::
+
+ from PIL import ExifTags
+ im = Image.open("exif_gps.jpg")
+ exif = im.getexif()
+ gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo)
+ print(gps_ifd)
+
+ Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``,
+ ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``.
+
+ :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data::
+
+ print(exif[ExifTags.Base.Software]) # PIL
+ print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99
+ """
+
endian = None
bigtiff = False
@@ -3837,7 +3885,7 @@ class Exif(MutableMapping):
def __str__(self):
if self._info is not None:
# Load all keys into self._data
- for tag in self._info.keys():
+ for tag in self._info:
self[tag]
return str(self._data)
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index ce29a163b..163828d31 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -928,8 +928,8 @@ def floodfill(image, xy, value, border=None, thresh=0):
full_edge = set()
while edge:
new_edge = set()
- for (x, y) in edge: # 4 adjacent method
- for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
+ for x, y in edge: # 4 adjacent method
+ for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
# If already processed, or if a coordinate is negative, skip
if (s, t) in full_edge or s < 0 or t < 0:
continue
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 12391955f..132490a8e 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -395,7 +395,6 @@ class Parser:
# parse what we have
if self.decoder:
-
if self.offset > 0:
# skip header
skip = min(len(self.data), self.offset)
@@ -420,14 +419,12 @@ class Parser:
self.data = self.data[n:]
elif self.image:
-
# if we end up here with no decoder, this file cannot
# be incrementally parsed. wait until we've gotten all
# available data
pass
else:
-
# attempt to open this file
try:
with io.BytesIO(self.data) as fp:
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 59e2c18b9..63d6dcf5c 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -183,6 +183,9 @@ class BoxBlur(MultibandFilter):
name = "BoxBlur"
def __init__(self, radius):
+ if radius < 0:
+ msg = "radius must be >= 0"
+ raise ValueError(msg)
self.radius = radius
def filter(self, image):
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index b144c3dd2..bd13c391e 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -90,7 +90,6 @@ class ImageFont:
"""PIL font wrapper"""
def _load_pilfont(self, filename):
-
with open(filename, "rb") as fp:
image = None
for ext in (".png", ".gif", ".pbm"):
@@ -116,7 +115,6 @@ class ImageFont:
image.close()
def _load_pilfont_data(self, file, image):
-
# read PILfont header
if file.readline() != b"PILfont\n":
msg = "Not a PILfont file"
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index e2168ce62..301c593c7 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
# Create the mapping (2-color)
if mid is None:
-
range_map = range(0, whitepoint - blackpoint)
for i in range_map:
@@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
# Create the mapping (3-color)
else:
-
range_map1 = range(0, midpoint - blackpoint)
range_map2 = range(0, whitepoint - midpoint)
@@ -248,7 +246,8 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
:param size: The requested output size in pixels, given as a
(width, height) tuple.
:param method: Resampling method to use. Default is
- :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
:return: An image.
"""
@@ -276,7 +275,8 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
:param size: The requested output size in pixels, given as a
(width, height) tuple.
:param method: Resampling method to use. Default is
- :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
:param color: The background color of the padded image.
:param centering: Control the position of the original image within the
padded version.
@@ -328,7 +328,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
:param image: The image to rescale.
:param factor: The expansion factor, as a float.
:param resample: Resampling method to use. Default is
- :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
if factor == 1:
@@ -425,7 +426,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
:param size: The requested output size in pixels, given as a
(width, height) tuple.
:param method: Resampling method to use. Default is
- :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
:param bleed: Remove a border around the outside of the image from all
four edges. The value is a decimal percentage (use 0.01 for
one percent). The default value is 0 (no border).
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index fe0d32155..e455c0459 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -248,11 +248,9 @@ def wedge(mode="RGB"):
def load(filename):
-
# FIXME: supports GIMP gradients only
with open(filename, "rb") as fp:
-
for paletteHandler in [
GimpPaletteFile.GimpPaletteFile,
GimpGradientFile.GimpGradientFile,
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 29d900bef..f0e73fb90 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -390,7 +390,6 @@ else:
if __name__ == "__main__":
-
if len(sys.argv) < 2:
print("Syntax: python3 ImageShow.py imagefile [title]")
sys.exit()
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index 09a6356fa..ef569ed2e 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -97,7 +97,6 @@ class PhotoImage:
"""
def __init__(self, image=None, size=None, **kw):
-
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
@@ -209,7 +208,6 @@ class BitmapImage:
"""
def __init__(self, image=None, **kw):
-
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index cfeadd53c..ac267457b 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)")
class ImtImageFile(ImageFile.ImageFile):
-
format = "IMT"
format_description = "IM Tools"
def _open(self):
-
# Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header.
@@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile):
xsize = ysize = 0
while True:
-
if buffer:
s = buffer[:1]
buffer = buffer[1:]
@@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile):
break
if s == b"\x0C":
-
# image data begins
self.tile = [
(
@@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile):
break
else:
-
# read key/value pair
if b"\n" not in buffer:
buffer += self.fp.read(100)
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 774817569..4c47b55c1 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -48,7 +48,6 @@ def dump(c):
class IptcImageFile(ImageFile.ImageFile):
-
format = "IPTC"
format_description = "IPTC/NAA"
@@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile):
return tag, size
def _open(self):
-
# load descriptive fields
while True:
offset = self.fp.tell()
@@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile):
]
def load(self):
-
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 9657ae9d0..71ae84c04 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -41,7 +41,7 @@ import sys
import tempfile
import warnings
-from . import Image, ImageFile, TiffImagePlugin
+from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
@@ -344,12 +344,10 @@ def _accept(prefix):
class JpegImageFile(ImageFile.ImageFile):
-
format = "JPEG"
format_description = "JPEG (ISO 10918)"
def _open(self):
-
s = self.fp.read(3)
if not _accept(s):
@@ -370,7 +368,6 @@ class JpegImageFile(ImageFile.ImageFile):
self.icclist = []
while True:
-
i = s[0]
if i == 0xFF:
s = s + self.fp.read(1)
@@ -418,7 +415,6 @@ class JpegImageFile(ImageFile.ImageFile):
return s
def draft(self, mode, size):
-
if len(self.tile) != 1:
return
@@ -455,7 +451,6 @@ class JpegImageFile(ImageFile.ImageFile):
return self.mode, box
def load_djpeg(self):
-
# ALTERNATIVE: handle JPEGs via the IJG command line utilities
f, path = tempfile.mkstemp()
@@ -524,6 +519,8 @@ def _getmp(self):
head = file_contents.read(8)
endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<"
# process dictionary
+ from . import TiffImagePlugin
+
try:
info = TiffImagePlugin.ImageFileDirectory_v2(head)
file_contents.seek(info.next)
@@ -733,10 +730,10 @@ def _save(im, fp, filename):
extra = info.get("extra", b"")
+ MAX_BYTES_IN_MARKER = 65533
icc_profile = info.get("icc_profile")
if icc_profile:
ICC_OVERHEAD_LEN = 14
- MAX_BYTES_IN_MARKER = 65533
MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN
markers = []
while icc_profile:
@@ -767,6 +764,9 @@ def _save(im, fp, filename):
exif = info.get("exif", b"")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
+ if len(exif) > MAX_BYTES_IN_MARKER:
+ msg = "EXIF data is too long"
+ raise ValueError(msg)
# get keyword arguments
im.encoderconfig = (
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 8d4d826aa..17c008b9a 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -30,12 +30,10 @@ def _accept(s):
class McIdasImageFile(ImageFile.ImageFile):
-
format = "MCIDAS"
format_description = "McIdas area file"
def _open(self):
-
# parse area file directory
s = self.fp.read(256)
if not _accept(s) or len(s) != 256:
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index e7e1054a3..8dd9f2909 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -34,13 +34,11 @@ def _accept(prefix):
class MicImageFile(TiffImagePlugin.TiffImageFile):
-
format = "MIC"
format_description = "Microsoft Image Composer"
_close_exclusive_fp_after_loading = False
def _open(self):
-
# read the OLE directory and see if this is a likely
# to be a Microsoft Image Composer file
diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index 2d799d6d8..d96d3a11c 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -58,12 +58,10 @@ class BitStream:
class MpegImageFile(ImageFile.ImageFile):
-
format = "MPEG"
format_description = "MPEG"
def _open(self):
-
s = BitStream(self.fp)
if s.read(32) != 0x1B3:
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index b1ec2c7bc..f9261c77d 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -101,7 +101,6 @@ def _save_all(im, fp, filename):
class MpoImageFile(JpegImagePlugin.JpegImageFile):
-
format = "MPO"
format_description = "MPO (CIPA DC-007)"
_close_exclusive_fp_after_loading = False
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 5420894dc..c6567b2ae 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -44,12 +44,10 @@ def _accept(prefix):
class MspImageFile(ImageFile.ImageFile):
-
format = "MSP"
format_description = "Windows Paint"
def _open(self):
-
# Header
s = self.fp.read(32)
if not _accept(s):
@@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
-
img = io.BytesIO()
blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
try:
@@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder)
def _save(im, fp, filename):
-
if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py
index 07acd5580..4a2c497fc 100644
--- a/src/PIL/PaletteFile.py
+++ b/src/PIL/PaletteFile.py
@@ -22,11 +22,9 @@ class PaletteFile:
rawmode = "RGB"
def __init__(self, fp):
-
self.palette = [(i, i, i) for i in range(256)]
while True:
-
s = fp.readline()
if not s:
@@ -50,5 +48,4 @@ class PaletteFile:
self.palette = b"".join(self.palette)
def getpalette(self):
-
return self.palette, self.rawmode
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index 109aad9ab..a88a90791 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
def _save(im, fp, filename):
-
if im.mode == "P":
-
# we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field
@@ -147,14 +145,12 @@ def _save(im, fp, filename):
version = 1
elif im.mode == "1":
-
# monochrome -- write it inverted, as is the Palm standard
rawmode = "1;I"
bpp = 1
version = 0
else:
-
msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg)
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index 5802d386a..e390f3fe5 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -24,12 +24,10 @@ from . import Image, ImageFile
class PcdImageFile(ImageFile.ImageFile):
-
format = "PCD"
format_description = "Kodak PhotoCD"
def _open(self):
-
# rough
self.fp.seek(2048)
s = self.fp.read(2048)
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index ecce1b097..d5f510f03 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile):
name = "name"
def __init__(self, fp, charset_encoding="iso8859-1"):
-
self.charset_encoding = charset_encoding
magic = l32(fp.read(4))
@@ -92,7 +91,6 @@ class PcfFontFile(FontFile.FontFile):
self.glyph[ch] = glyph
def _getformat(self, tag):
-
format, size, offset = self.toc[tag]
fp = self.fp
@@ -108,7 +106,6 @@ class PcfFontFile(FontFile.FontFile):
return fp, format, i16, i32
def _load_properties(self):
-
#
# font properties
@@ -136,7 +133,6 @@ class PcfFontFile(FontFile.FontFile):
return properties
def _load_metrics(self):
-
#
# font metrics
@@ -147,7 +143,6 @@ class PcfFontFile(FontFile.FontFile):
append = metrics.append
if (format & 0xFF00) == 0x100:
-
# "compressed" metrics
for i in range(i16(fp.read(2))):
left = i8(fp.read(1)) - 128
@@ -160,7 +155,6 @@ class PcfFontFile(FontFile.FontFile):
append((xsize, ysize, left, right, width, ascent, descent, 0))
else:
-
# "jumbo" metrics
for i in range(i32(fp.read(4))):
left = i16(fp.read(2))
@@ -176,7 +170,6 @@ class PcfFontFile(FontFile.FontFile):
return metrics
def _load_bitmaps(self, metrics):
-
#
# bitmap data
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 3202475dc..f42c2456b 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -45,12 +45,10 @@ def _accept(prefix):
class PcxImageFile(ImageFile.ImageFile):
-
format = "PCX"
format_description = "Paintbrush"
def _open(self):
-
# header
s = self.fp.read(128)
if not _accept(s):
@@ -143,7 +141,6 @@ SAVE = {
def _save(im, fp, filename):
-
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index baad4939f..4fa1998ba 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False):
else:
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b")
- resolution = im.encoderinfo.get("resolution", 72.0)
+ dpi = im.encoderinfo.get("dpi")
+ if dpi:
+ x_resolution = dpi[0]
+ y_resolution = dpi[1]
+ else:
+ x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
info = {
"title": None
@@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False):
stream=stream,
Type=PdfParser.PdfName("XObject"),
Subtype=PdfParser.PdfName("Image"),
- Width=width, # * 72.0 / resolution,
- Height=height, # * 72.0 / resolution,
+ Width=width, # * 72.0 / x_resolution,
+ Height=height, # * 72.0 / y_resolution,
Filter=filter,
BitsPerComponent=bits,
Decode=decode,
@@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False):
MediaBox=[
0,
0,
- width * 72.0 / resolution,
- height * 72.0 / resolution,
+ width * 72.0 / x_resolution,
+ height * 72.0 / y_resolution,
],
Contents=contents_refs[page_number],
)
@@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False):
# page contents
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
- width * 72.0 / resolution,
- height * 72.0 / resolution,
+ width * 72.0 / x_resolution,
+ height * 72.0 / y_resolution,
)
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index aa5ea2fbb..1b3cb52a2 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -328,9 +328,7 @@ def pdf_repr(x):
return b"null"
elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)):
return bytes(x)
- elif isinstance(x, int):
- return str(x).encode("us-ascii")
- elif isinstance(x, float):
+ elif isinstance(x, (int, float)):
return str(x).encode("us-ascii")
elif isinstance(x, time.struct_time):
return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")"
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index 8d0a34dba..7eb82228a 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -35,12 +35,10 @@ def _accept(prefix):
class PixarImageFile(ImageFile.ImageFile):
-
format = "PIXAR"
format_description = "PIXAR raster image"
def _open(self):
-
# assuming a 4-byte magic label
s = self.fp.read(4)
if not _accept(s):
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index b6626bbc5..9078957dc 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -161,7 +161,6 @@ def _crc32(data, seed=0):
class ChunkStream:
def __init__(self, fp):
-
self.fp = fp
self.queue = []
@@ -195,7 +194,6 @@ class ChunkStream:
self.queue = self.fp = None
def push(self, cid, pos, length):
-
self.queue.append((cid, pos, length))
def call(self, cid, pos, length):
@@ -230,7 +228,6 @@ class ChunkStream:
self.fp.read(4)
def verify(self, endchunk=b"IEND"):
-
# Simple approach; just calculate checksum for all remaining
# blocks. Must be called directly after open.
@@ -397,7 +394,6 @@ class PngStream(ChunkStream):
self._seq_num = self.rewind_state["seq_num"]
def chunk_iCCP(self, pos, length):
-
# ICC profile
s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains:
@@ -425,7 +421,6 @@ class PngStream(ChunkStream):
return s
def chunk_IHDR(self, pos, length):
-
# image header
s = ImageFile._safe_read(self.fp, length)
if length < 13:
@@ -446,7 +441,6 @@ class PngStream(ChunkStream):
return s
def chunk_IDAT(self, pos, length):
-
# image data
if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
@@ -459,12 +453,10 @@ class PngStream(ChunkStream):
raise EOFError
def chunk_IEND(self, pos, length):
-
# end of PNG image
raise EOFError
def chunk_PLTE(self, pos, length):
-
# palette
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
@@ -472,7 +464,6 @@ class PngStream(ChunkStream):
return s
def chunk_tRNS(self, pos, length):
-
# transparency
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
@@ -524,7 +515,6 @@ class PngStream(ChunkStream):
return s
def chunk_pHYs(self, pos, length):
-
# pixels per unit
s = ImageFile._safe_read(self.fp, length)
if length < 9:
@@ -542,7 +532,6 @@ class PngStream(ChunkStream):
return s
def chunk_tEXt(self, pos, length):
-
# text
s = ImageFile._safe_read(self.fp, length)
try:
@@ -562,7 +551,6 @@ class PngStream(ChunkStream):
return s
def chunk_zTXt(self, pos, length):
-
# compressed text
s = ImageFile._safe_read(self.fp, length)
try:
@@ -597,7 +585,6 @@ class PngStream(ChunkStream):
return s
def chunk_iTXt(self, pos, length):
-
# international text
r = s = ImageFile._safe_read(self.fp, length)
try:
@@ -721,12 +708,10 @@ def _accept(prefix):
class PngImageFile(ImageFile.ImageFile):
-
format = "PNG"
format_description = "Portable network graphics"
def _open(self):
-
if not _accept(self.fp.read(8)):
msg = "not a PNG file"
raise SyntaxError(msg)
@@ -740,7 +725,6 @@ class PngImageFile(ImageFile.ImageFile):
self.png = PngStream(self.fp)
while True:
-
#
# get next chunk
@@ -1264,7 +1248,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
mode = im.mode
if mode == "P":
-
#
# attempt to minimize storage requirements for palette images
if "bits" in im.encoderinfo:
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index dee2f1e15..5aa418044 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -51,7 +51,6 @@ def _accept(prefix):
class PpmImageFile(ImageFile.ImageFile):
-
format = "PPM"
format_description = "Pbmplus image"
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index c1ca30a03..5a5d60d56 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -51,13 +51,11 @@ def _accept(prefix):
class PsdImageFile(ImageFile.ImageFile):
-
format = "PSD"
format_description = "Adobe Photoshop"
_close_exclusive_fp_after_loading = False
def _open(self):
-
read = self.fp.read
#
@@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes):
raise SyntaxError(msg)
for _ in range(abs(ct)):
-
# bounding box
y0 = i32(read(4))
x0 = i32(read(4))
@@ -238,21 +235,18 @@ def _layerinfo(fp, ct_bytes):
layers.append((name, mode, (x0, y0, x1, y1)))
# get tiles
- i = 0
- for name, mode, bbox in layers:
+ for i, (name, mode, bbox) in enumerate(layers):
tile = []
for m in mode:
t = _maketile(fp, m, bbox, 1)
if t:
tile.extend(t)
layers[i] = name, mode, bbox, tile
- i += 1
return layers
def _maketile(file, mode, bbox, channels):
-
tile = None
read = file.read
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index d533c55e5..3662ffd15 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -49,12 +49,10 @@ MODES = {
##
# Image plugin for SGI images.
class SgiImageFile(ImageFile.ImageFile):
-
format = "SGI"
format_description = "SGI Image File Format"
def _open(self):
-
# HEAD
headlen = 512
s = self.fp.read(headlen)
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 1192c2d73..eac27e679 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -91,7 +91,6 @@ def isSpiderImage(filename):
class SpiderImageFile(ImageFile.ImageFile):
-
format = "SPIDER"
format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False
@@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# --------------------------------------------------------------------
# Image series
+
# given a list of filenames, return a list of images
def loadImageSeries(filelist=None):
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
@@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile)
Image.register_save(SpiderImageFile.format, _save_spider)
if __name__ == "__main__":
-
if len(sys.argv) < 2:
print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
sys.exit()
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index c64de4444..6712583d7 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -30,12 +30,10 @@ def _accept(prefix):
class SunImageFile(ImageFile.ImageFile):
-
format = "SUN"
format_description = "Sun Raster File"
def _open(self):
-
# The Sun Raster file header is 32 bytes in length
# and has the following format:
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index 20e8a083f..32928f6af 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO):
self.fh = open(tarfile, "rb")
while True:
-
s = self.fh.read(512)
if len(s) != 512:
msg = "unexpected end of tar file"
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 53fe6ef5c..67dfc3d3c 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -46,12 +46,10 @@ MODES = {
class TgaImageFile(ImageFile.ImageFile):
-
format = "TGA"
format_description = "Targa"
def _open(self):
-
# process header
s = self.fp.read(18)
@@ -174,7 +172,6 @@ SAVE = {
def _save(im, fp, filename):
-
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 431edfd9b..aaaf8fcb9 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -257,7 +257,7 @@ OPEN_INFO = {
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
}
-MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())
+MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
PREFIXES = [
b"MM\x00\x2A", # Valid TIFF header with big-endian byte order
@@ -722,6 +722,8 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(1) # Basic type, except for the legacy API.
def write_byte(self, data):
+ if isinstance(data, IFDRational):
+ data = int(data)
if isinstance(data, int):
data = bytes((data,))
return data
@@ -762,6 +764,8 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(7)
def write_undefined(self, value):
+ if isinstance(value, int):
+ value = str(value).encode("ascii", "replace")
return value
@_register_loader(10, 8)
@@ -791,7 +795,6 @@ class ImageFileDirectory_v2(MutableMapping):
return ret
def load(self, fp):
-
self.reset()
self._offset = fp.tell()
@@ -936,7 +939,6 @@ class ImageFileDirectory_v2(MutableMapping):
return result
def save(self, fp):
-
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
@@ -1057,7 +1059,6 @@ ImageFileDirectory = ImageFileDirectory_v1
class TiffImageFile(ImageFile.ImageFile):
-
format = "TIFF"
format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False
@@ -1222,7 +1223,7 @@ class TiffImageFile(ImageFile.ImageFile):
# load IFD data from fp before it is closed
exif = self.getexif()
- for key in TiffTags.TAGS_V2_GROUPS.keys():
+ for key in TiffTags.TAGS_V2_GROUPS:
if key not in exif:
continue
exif.get_ifd(key)
@@ -1580,7 +1581,6 @@ SAVE_INFO = {
def _save(im, fp, filename):
-
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
@@ -1629,7 +1629,7 @@ def _save(im, fp, filename):
if isinstance(info, ImageFileDirectory_v1):
info = info.to_v2()
for key in info:
- if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():
+ if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS:
ifd[key] = info.get_ifd(key)
else:
ifd[key] = info.get(key)
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 9b5277138..ac048ba56 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -312,7 +312,7 @@ TAGS = {
34910: "HylaFAX FaxRecvTime",
36864: "ExifVersion",
36867: "DateTimeOriginal",
- 36868: "DateTImeDigitized",
+ 36868: "DateTimeDigitized",
37121: "ComponentsConfiguration",
37122: "CompressedBitsPerPixel",
37724: "ImageSourceData",
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index 0dc695a88..e4f47aa04 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -28,7 +28,6 @@ from ._binary import i32le as i32
class WalImageFile(ImageFile.ImageFile):
-
format = "WAL"
format_description = "Quake2 Texture"
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 1d074f78c..d060dd4b8 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -35,7 +35,6 @@ def _accept(prefix):
class WebPImageFile(ImageFile.ImageFile):
-
format = "WEBP"
format_description = "WebP image"
__loaded = 0
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 639730b8e..0ecab56a8 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -75,7 +75,6 @@ def _accept(prefix):
class WmfStubImageFile(ImageFile.StubImageFile):
-
format = "WMF"
format_description = "Windows Metafile"
@@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile):
s = self.fp.read(80)
if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00":
-
# placeable windows metafile
# get units per inch
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index f0e05e867..aa4a01f4e 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -41,12 +41,10 @@ def _accept(prefix):
class XVThumbImageFile(ImageFile.ImageFile):
-
format = "XVThumb"
format_description = "XV thumbnail image"
def _open(self):
-
# check magic
if not _accept(self.fp.read(6)):
msg = "not an XV thumbnail file"
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index ad18e0031..3c12564c9 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -44,12 +44,10 @@ def _accept(prefix):
class XbmImageFile(ImageFile.ImageFile):
-
format = "XBM"
format_description = "X11 Bitmap"
def _open(self):
-
m = xbm_head.match(self.fp.read(512))
if not m:
@@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile):
def _save(im, fp, filename):
-
if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index 5fae4cd68..5d5bdc3ed 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -33,12 +33,10 @@ def _accept(prefix):
class XpmImageFile(ImageFile.ImageFile):
-
format = "XPM"
format_description = "X11 Pixel Map"
def _open(self):
-
if not _accept(self.fp.read(9)):
msg = "not an XPM file"
raise SyntaxError(msg)
@@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile):
palette = [b"\0\0\0"] * 256
for _ in range(pal):
-
s = self.fp.readline()
if s[-2:] == b"\r\n":
s = s[:-2]
@@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile):
s = s[2:-2].split()
for i in range(0, len(s), 2):
-
if s[i] == b"c":
-
# process colour key
rgb = s[i + 1]
if rgb == b"None":
@@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile):
break
else:
-
# missing colour key
msg = "cannot read this XPM file"
raise ValueError(msg)
@@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
def load_read(self, bytes):
-
#
# load all image data in one chunk
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index ab48f74e8..bb245de6d 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -1,11 +1,11 @@
"""Pillow (Fork of the Python Imaging Library)
-Pillow is the friendly PIL fork by Alex Clark and Contributors.
+Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.
https://github.com/python-pillow/Pillow/
Pillow is forked from PIL 1.1.7.
-PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
+PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Copyright (c) 1999 by Secret Labs AB.
Use PIL.__version__ for this Pillow version.
@@ -76,6 +76,10 @@ _plugins = [
class UnidentifiedImageError(OSError):
"""
Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified.
+
+ If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES`
+ to true may allow the image to be opened after all. The setting will ignore missing
+ data and checksum failures.
"""
pass
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 7c4b1623d..fa6e1d00c 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 10:
removed = "Pillow 10 (2023-07-01)"
+ elif when == 11:
+ removed = "Pillow 11 (2024-10-15)"
else:
msg = f"Unknown removal version, update {__name__}?"
raise ValueError(msg)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 1cc1d0f1c..7baa9fb6c 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "9.4.0.dev0"
+__version__ = "9.5.0.dev0"
diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c
index 16b9a2edd..ad503baec 100644
--- a/src/Tk/tkImaging.c
+++ b/src/Tk/tkImaging.c
@@ -310,7 +310,7 @@ load_tkinter_funcs(void) {
* Return 0 for success, non-zero for failure.
*/
- HMODULE hMods[1024];
+ HMODULE* hMods = NULL;
HANDLE hProcess;
DWORD cbNeeded;
unsigned int i;
@@ -327,33 +327,48 @@ load_tkinter_funcs(void) {
/* Returns pseudo-handle that does not need to be closed */
hProcess = GetCurrentProcess();
+ /* Allocate module handlers array */
+ if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) {
+#if defined(__CYGWIN__)
+ PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed");
+#else
+ PyErr_SetFromWindowsErr(0);
+#endif
+ return 1;
+ }
+ if (!(hMods = (HMODULE*) malloc(cbNeeded))) {
+ PyErr_NoMemory();
+ return 1;
+ }
+
/* Iterate through modules in this process looking for Tcl / Tk names */
- if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
+ if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) {
for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
if (!found_tcl) {
found_tcl = get_tcl(hMods[i]);
if (found_tcl == -1) {
- return 1;
+ break;
}
}
if (!found_tk) {
found_tk = get_tk(hMods[i]);
if (found_tk == -1) {
- return 1;
+ break;
}
}
if (found_tcl && found_tk) {
- return 0;
+ break;
}
}
}
+ free(hMods);
if (found_tcl == 0) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines");
- } else {
+ } else if (found_tk == 0) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines");
}
- return 1;
+ return (int) ((found_tcl != 1) || (found_tk != 1));
}
#else /* not Windows */
diff --git a/src/_imaging.c b/src/_imaging.c
index 05e1370f6..cece2e93a 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -3984,8 +3984,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args);
extern PyObject *
PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args);
extern PyObject *
-PyImaging_ListWindowsWin32(PyObject *self, PyObject *args);
-extern PyObject *
PyImaging_EventLoopWin32(PyObject *self, PyObject *args);
extern PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args);
@@ -4069,7 +4067,6 @@ static PyMethodDef functions[] = {
{"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS},
{"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS},
{"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS},
- {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS},
{"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS},
#endif
#ifdef HAVE_XCB
diff --git a/src/_imagingft.c b/src/_imagingft.c
index b52d6353e..0db17a5a6 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -921,6 +921,12 @@ font_render(FontObject *self, PyObject *args) {
yy = -(py + glyph_slot->bitmap_top);
}
+ // Null buffer, is dereferenced in FT_Bitmap_Convert
+ if (!bitmap.buffer && bitmap.rows) {
+ PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph");
+ goto glyph_error;
+ }
+
/* convert non-8bpp bitmaps */
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
diff --git a/src/decode.c b/src/decode.c
index 7a9b956c5..82a3af832 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) {
static PyObject *
_decode(ImagingDecoderObject *decoder, PyObject *args) {
- UINT8 *buffer;
- Py_ssize_t bufsize;
+ Py_buffer buffer;
int status;
ImagingSectionCookie cookie;
- if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) {
+ if (!PyArg_ParseTuple(args, "y*", &buffer)) {
return NULL;
}
@@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) {
ImagingSectionEnter(&cookie);
}
- status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize);
+ status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len);
if (!decoder->pulls_fd) {
ImagingSectionLeave(&cookie);
diff --git a/src/display.c b/src/display.c
index 0ce10e249..a50fc3e24 100644
--- a/src/display.c
+++ b/src/display.c
@@ -421,79 +421,6 @@ error:
return NULL;
}
-static BOOL CALLBACK
-list_windows_callback(HWND hwnd, LPARAM lParam) {
- PyObject *window_list = (PyObject *)lParam;
- PyObject *item;
- PyObject *title;
- RECT inner, outer;
- int title_size;
- int status;
-
- /* get window title */
- title_size = GetWindowTextLength(hwnd);
- if (title_size > 0) {
- title = PyUnicode_FromStringAndSize(NULL, title_size);
- if (title) {
- GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1);
- }
- } else {
- title = PyUnicode_FromString("");
- }
- if (!title) {
- return 0;
- }
-
- /* get bounding boxes */
- GetClientRect(hwnd, &inner);
- GetWindowRect(hwnd, &outer);
-
- item = Py_BuildValue(
- F_HANDLE "N(iiii)(iiii)",
- hwnd,
- title,
- inner.left,
- inner.top,
- inner.right,
- inner.bottom,
- outer.left,
- outer.top,
- outer.right,
- outer.bottom);
- if (!item) {
- return 0;
- }
-
- status = PyList_Append(window_list, item);
-
- Py_DECREF(item);
-
- if (status < 0) {
- return 0;
- }
-
- return 1;
-}
-
-PyObject *
-PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) {
- PyObject *window_list;
-
- window_list = PyList_New(0);
- if (!window_list) {
- return NULL;
- }
-
- EnumWindows(list_windows_callback, (LPARAM)window_list);
-
- if (PyErr_Occurred()) {
- Py_DECREF(window_list);
- return NULL;
- }
-
- return window_list;
-}
-
/* -------------------------------------------------------------------- */
/* Windows clipboard grabber */
diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c
index 2e45a3358..5afe7cf50 100644
--- a/src/libImaging/BoxBlur.c
+++ b/src/libImaging/BoxBlur.c
@@ -237,6 +237,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) {
if (n < 1) {
return ImagingError_ValueError("number of passes must be greater than zero");
}
+ if (radius < 0) {
+ return ImagingError_ValueError("radius must be >= 0");
+ }
if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type ||
imIn->bands != imOut->bands || imIn->xsize != imOut->xsize ||
diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c
index f72060228..5b6bfb89c 100644
--- a/src/libImaging/Fill.c
+++ b/src/libImaging/Fill.c
@@ -24,6 +24,11 @@ ImagingFill(Imaging im, const void *colour) {
int x, y;
ImagingSectionCookie cookie;
+ /* 0-width or 0-height image. No need to do anything */
+ if (!im->linesize || !im->ysize) {
+ return im;
+ }
+
if (im->type == IMAGING_TYPE_SPECIAL) {
/* use generic API */
ImagingAccess access = ImagingAccessNew(im);
diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md
index 3354a4d25..315e0c8d8 100644
--- a/src/thirdparty/raqm/README.md
+++ b/src/thirdparty/raqm/README.md
@@ -11,7 +11,7 @@ It currently provides bidirectional text support (using [FriBiDi][1] or
As a result, Raqm can support most writing systems covered by Unicode.
The documentation can be accessed on the web at:
-> http://host-oman.github.io/libraqm/
+> https://host-oman.github.io/libraqm/
Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for
digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”.
@@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm:
[1]: https://github.com/fribidi/fribidi
[2]: https://github.com/Tehreer/SheenBidi
[3]: https://github.com/harfbuzz/harfbuzz
-[4]: https://freetype.org/
+[4]: https://www.freetype.org
[5]: https://www.gtk.org/gtk-doc
diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h
index 78b70a561..bdb6fb662 100644
--- a/src/thirdparty/raqm/raqm-version.h
+++ b/src/thirdparty/raqm/raqm-version.h
@@ -32,10 +32,10 @@
#define _RAQM_VERSION_H_
#define RAQM_VERSION_MAJOR 0
-#define RAQM_VERSION_MINOR 9
+#define RAQM_VERSION_MINOR 10
#define RAQM_VERSION_MICRO 0
-#define RAQM_VERSION_STRING "0.9.0"
+#define RAQM_VERSION_STRING "0.10.0"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \
diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c
index 13f6e1f02..770ea3018 100644
--- a/src/thirdparty/raqm/raqm.c
+++ b/src/thirdparty/raqm/raqm.c
@@ -171,19 +171,23 @@
typedef FriBidiLevel _raqm_bidi_level_t;
#endif
-typedef struct {
+typedef struct
+{
FT_Face ftface;
int ftloadflags;
hb_language_t lang;
hb_script_t script;
+ int spacing_after;
} _raqm_text_info;
typedef struct _raqm_run raqm_run_t;
-struct _raqm {
+struct _raqm
+{
int ref_count;
uint32_t *text;
+ uint16_t *text_utf16;
char *text_utf8;
size_t text_len;
size_t text_capacity_bytes;
@@ -205,7 +209,8 @@ struct _raqm {
int invisible_glyph;
};
-struct _raqm_run {
+struct _raqm_run
+{
uint32_t pos;
uint32_t len;
@@ -217,9 +222,13 @@ struct _raqm_run {
raqm_run_t *next;
};
-static uint32_t
-_raqm_u8_to_u32_index (raqm_t *rq,
- uint32_t index);
+static size_t
+_raqm_encoding_to_u32_index (raqm_t *rq,
+ size_t index);
+
+static bool
+_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char,
+ hb_codepoint_t r_char);
static void
_raqm_init_text_info (raqm_t *rq)
@@ -231,6 +240,7 @@ _raqm_init_text_info (raqm_t *rq)
rq->text_info[i].ftloadflags = -1;
rq->text_info[i].lang = default_lang;
rq->text_info[i].script = HB_SCRIPT_INVALID;
+ rq->text_info[i].spacing_after = 0;
}
}
@@ -263,6 +273,8 @@ _raqm_compare_text_info (_raqm_text_info a,
if (a.script != b.script)
return false;
+ /* Spacing shouldn't break runs, so we don't compare them here. */
+
return true;
}
@@ -273,6 +285,7 @@ _raqm_free_text(raqm_t* rq)
rq->text = NULL;
rq->text_info = NULL;
rq->text_utf8 = NULL;
+ rq->text_utf16 = NULL;
rq->text_len = 0;
rq->text_capacity_bytes = 0;
}
@@ -280,12 +293,15 @@ _raqm_free_text(raqm_t* rq)
static bool
_raqm_alloc_text(raqm_t *rq,
size_t len,
- bool need_utf8)
+ bool need_utf8,
+ bool need_utf16)
{
/* Allocate contiguous memory block for texts and text_info */
size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len;
if (need_utf8)
mem_size += sizeof (char) * len;
+ else if (need_utf16)
+ mem_size += sizeof (uint16_t) * len;
if (mem_size > rq->text_capacity_bytes)
{
@@ -302,6 +318,7 @@ _raqm_alloc_text(raqm_t *rq,
rq->text_info = (_raqm_text_info*)(rq->text + len);
rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL;
+ rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL;
return true;
}
@@ -357,7 +374,7 @@ _raqm_free_runs (raqm_run_t *runs)
* Return value:
* A newly allocated #raqm_t with a reference count of 1. The initial reference
* count should be released with raqm_destroy() when you are done using the
- * #raqm_t. Returns %NULL in case of error.
+ * #raqm_t. Returns `NULL` in case of error.
*
* Since: 0.1
*/
@@ -381,6 +398,7 @@ raqm_create (void)
rq->invisible_glyph = 0;
rq->text = NULL;
+ rq->text_utf16 = NULL;
rq->text_utf8 = NULL;
rq->text_info = NULL;
rq->text_capacity_bytes = 0;
@@ -498,7 +516,7 @@ raqm_clear_contents (raqm_t *rq)
* separately can give improper output.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.1
*/
@@ -518,7 +536,7 @@ raqm_set_text (raqm_t *rq,
if (!len)
return true;
- if (!_raqm_alloc_text(rq, len, false))
+ if (!_raqm_alloc_text(rq, len, false, false))
return false;
rq->text_len = len;
@@ -575,6 +593,53 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode)
return (out_utf32 - unicode);
}
+static void *
+_raqm_get_utf16_codepoint (const void *str,
+ uint32_t *out_codepoint)
+{
+ const uint16_t *s = (const uint16_t *)str;
+
+ if (s[0] > 0xD800 && s[0] < 0xDBFF)
+ {
+ if (s[1] > 0xDC00 && s[1] < 0xDFFF)
+ {
+ uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1));
+ uint32_t W = (s[0] >> 6) & ((1 << 5) - 1);
+ *out_codepoint = (W+1) << 16 | X;
+ s += 2;
+ }
+ else
+ {
+ /* A single high surrogate, this is an error. */
+ *out_codepoint = s[0];
+ s += 1;
+ }
+ }
+ else
+ {
+ *out_codepoint = s[0];
+ s += 1;
+ }
+ return (void *)s;
+}
+
+static size_t
+_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode)
+{
+ size_t in_len = 0;
+ uint32_t *out_utf32 = unicode;
+ const uint16_t *in_utf16 = text;
+
+ while ((*in_utf16 != '\0') && (in_len < len))
+ {
+ in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32);
+ ++out_utf32;
+ ++in_len;
+ }
+
+ return (out_utf32 - unicode);
+}
+
/**
* raqm_set_text_utf8:
* @rq: a #raqm_t.
@@ -584,7 +649,7 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode)
* Same as raqm_set_text(), but for text encoded in UTF-8 encoding.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.1
*/
@@ -604,7 +669,7 @@ raqm_set_text_utf8 (raqm_t *rq,
if (!len)
return true;
- if (!_raqm_alloc_text(rq, len, true))
+ if (!_raqm_alloc_text(rq, len, true, false))
return false;
rq->text_len = _raqm_u8_to_u32 (text, len, rq->text);
@@ -614,6 +679,44 @@ raqm_set_text_utf8 (raqm_t *rq,
return true;
}
+/**
+ * raqm_set_text_utf16:
+ * @rq: a #raqm_t.
+ * @text: a UTF-16 encoded text string.
+ * @len: the length of @text in UTF-16 shorts.
+ *
+ * Same as raqm_set_text(), but for text encoded in UTF-16 encoding.
+ *
+ * Return value:
+ * `true` if no errors happened, `false` otherwise.
+ *
+ * Since: 0.10
+ */
+bool
+raqm_set_text_utf16 (raqm_t *rq,
+ const uint16_t *text,
+ size_t len)
+{
+ if (!rq || !text)
+ return false;
+
+ /* Call raqm_clear_contents to reuse this raqm_t */
+ if (rq->text_len)
+ return false;
+
+ /* Empty string, don’t fail but do nothing */
+ if (!len)
+ return true;
+
+ if (!_raqm_alloc_text(rq, len, false, true))
+ return false;
+
+ rq->text_len = _raqm_u16_to_u32 (text, len, rq->text);
+ memcpy (rq->text_utf16, text, sizeof (uint16_t) * len);
+ _raqm_init_text_info (rq);
+
+ return true;
+}
/**
* raqm_set_par_direction:
* @rq: a #raqm_t.
@@ -640,7 +743,7 @@ raqm_set_text_utf8 (raqm_t *rq,
* text.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.1
*/
@@ -673,7 +776,7 @@ raqm_set_par_direction (raqm_t *rq,
* parts of the text.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Stability:
* Unstable
@@ -687,7 +790,7 @@ raqm_set_language (raqm_t *rq,
size_t len)
{
hb_language_t language;
- size_t end = start + len;
+ size_t end;
if (!rq)
return false;
@@ -695,11 +798,8 @@ raqm_set_language (raqm_t *rq,
if (!rq->text_len)
return true;
- if (rq->text_utf8)
- {
- start = _raqm_u8_to_u32_index (rq, start);
- end = _raqm_u8_to_u32_index (rq, end);
- }
+ end = _raqm_encoding_to_u32_index (rq, start + len);
+ start = _raqm_encoding_to_u32_index (rq, start);
if (start >= rq->text_len || end > rq->text_len)
return false;
@@ -716,11 +816,37 @@ raqm_set_language (raqm_t *rq,
return true;
}
+static bool
+_raqm_add_font_feature (raqm_t *rq,
+ hb_feature_t fea)
+{
+ void* new_features;
+
+ if (!rq)
+ return false;
+
+ new_features = realloc (rq->features,
+ sizeof (hb_feature_t) * (rq->features_len + 1));
+ if (!new_features)
+ return false;
+
+ if (fea.start != HB_FEATURE_GLOBAL_START)
+ fea.start = _raqm_encoding_to_u32_index (rq, fea.start);
+ if (fea.end != HB_FEATURE_GLOBAL_END)
+ fea.end = _raqm_encoding_to_u32_index (rq, fea.end);
+
+ rq->features = new_features;
+ rq->features[rq->features_len] = fea;
+ rq->features_len++;
+
+ return true;
+}
+
/**
* raqm_add_font_feature:
* @rq: a #raqm_t.
* @feature: (transfer none): a font feature string.
- * @len: length of @feature, -1 for %NULL-terminated.
+ * @len: length of @feature, -1 for `NULL`-terminated.
*
* Adds a font feature to be used by the #raqm_t during text layout. This is
* usually used to turn on optional font features that are not enabled by
@@ -734,7 +860,7 @@ raqm_set_language (raqm_t *rq,
* end of the features list and can potentially override previous features.
*
* Return value:
- * %true if parsing @feature succeeded, %false otherwise.
+ * `true` if parsing @feature succeeded, `false` otherwise.
*
* Since: 0.1
*/
@@ -751,16 +877,7 @@ raqm_add_font_feature (raqm_t *rq,
ok = hb_feature_from_string (feature, len, &fea);
if (ok)
- {
- void* new_features = realloc (rq->features,
- sizeof (hb_feature_t) * (rq->features_len + 1));
- if (!new_features)
- return false;
-
- rq->features = new_features;
- rq->features[rq->features_len] = fea;
- rq->features_len++;
- }
+ _raqm_add_font_feature (rq, fea);
return ok;
}
@@ -817,7 +934,7 @@ _raqm_set_freetype_face (raqm_t *rq,
* See also raqm_set_freetype_face_range().
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.1
*/
@@ -832,21 +949,23 @@ raqm_set_freetype_face (raqm_t *rq,
* raqm_set_freetype_face_range:
* @rq: a #raqm_t.
* @face: an #FT_Face.
- * @start: index of first character that should use @face.
- * @len: number of characters using @face.
+ * @start: index of first character that should use @face from the input string.
+ * @len: number of elements using @face.
*
* Sets an #FT_Face to be used for @len-number of characters staring at @start.
- * The @start and @len are input string array indices (i.e. counting bytes in
- * UTF-8 and scaler values in UTF-32).
+ * The @start and @len are input string array indices, counting elements
+ * according to the underlying encoding. @start must always be aligned to the
+ * start of an encoded codepoint, and @len must always end at a codepoint's
+ * final element.
*
* This method can be used repeatedly to set different faces for different
* parts of the text. It is the responsibility of the client to make sure that
- * face ranges cover the whole text.
+ * face ranges cover the whole text, and is properly aligned.
*
* See also raqm_set_freetype_face().
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.1
*/
@@ -856,7 +975,7 @@ raqm_set_freetype_face_range (raqm_t *rq,
size_t start,
size_t len)
{
- size_t end = start + len;
+ size_t end;
if (!rq)
return false;
@@ -864,11 +983,8 @@ raqm_set_freetype_face_range (raqm_t *rq,
if (!rq->text_len)
return true;
- if (rq->text_utf8)
- {
- start = _raqm_u8_to_u32_index (rq, start);
- end = _raqm_u8_to_u32_index (rq, end);
- }
+ end = _raqm_encoding_to_u32_index (rq, start + len);
+ start = _raqm_encoding_to_u32_index (rq, start);
return _raqm_set_freetype_face (rq, face, start, end);
}
@@ -909,7 +1025,7 @@ _raqm_set_freetype_load_flags (raqm_t *rq,
* older version the flags will be ignored.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.3
*/
@@ -943,7 +1059,7 @@ raqm_set_freetype_load_flags (raqm_t *rq,
* See also raqm_set_freetype_load_flags().
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.9
*/
@@ -953,7 +1069,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq,
size_t start,
size_t len)
{
- size_t end = start + len;
+ size_t end;
if (!rq)
return false;
@@ -961,15 +1077,161 @@ raqm_set_freetype_load_flags_range (raqm_t *rq,
if (!rq->text_len)
return true;
- if (rq->text_utf8)
- {
- start = _raqm_u8_to_u32_index (rq, start);
- end = _raqm_u8_to_u32_index (rq, end);
- }
+ end = _raqm_encoding_to_u32_index (rq, start + len);
+ start = _raqm_encoding_to_u32_index (rq, start);
return _raqm_set_freetype_load_flags (rq, flags, start, end);
}
+static bool
+_raqm_set_spacing (raqm_t *rq,
+ int spacing,
+ bool word_spacing,
+ size_t start,
+ size_t end)
+{
+ if (!rq)
+ return false;
+
+ if (!rq->text_len)
+ return true;
+
+ if (start >= rq->text_len || end > rq->text_len)
+ return false;
+
+ if (!rq->text_info)
+ return false;
+
+ for (size_t i = start; i < end; i++)
+ {
+ bool set_spacing = i == 0;
+ if (!set_spacing)
+ set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]);
+
+ if (set_spacing)
+ {
+ if (word_spacing)
+ {
+ if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1]))
+ {
+ /* CSS word seperators, word spacing is only applied on these.*/
+ if (rq->text[i] == 0x0020 || /* Space */
+ rq->text[i] == 0x00A0 || /* No Break Space */
+ rq->text[i] == 0x1361 || /* Ethiopic Word Space */
+ rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */
+ rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */
+ rq->text[i] == 0x1039F || /* Ugaric Word Divider */
+ rq->text[i] == 0x1091F) /* Phoenician Word Separator */
+ {
+ rq->text_info[i].spacing_after = spacing;
+ }
+ }
+ }
+ else
+ {
+ rq->text_info[i].spacing_after = spacing;
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * raqm_set_letter_spacing_range:
+ * @rq: a #raqm_t.
+ * @spacing: amount of spacing in Freetype Font Units (26.6 format).
+ * @start: index of first character that should use @spacing.
+ * @len: number of characters using @spacing.
+ *
+ * Set the letter spacing or tracking for a given range, the value
+ * will be added onto the advance and offset for RTL, and the advance for
+ * other directions. Letter spacing will be applied between characters, so
+ * the last character will not have spacing applied after it.
+ * Note that not all scripts have a letter-spacing tradition,
+ * for example, Arabic does not, while Devanagari does.
+ *
+ * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`” font
+ * features to the internal features list, so call this function after setting
+ * the font features for best spacing results.
+ *
+ * Return value:
+ * `true` if no errors happened, `false` otherwise.
+ *
+ * Since: 0.10
+ */
+bool
+raqm_set_letter_spacing_range(raqm_t *rq,
+ int spacing,
+ size_t start,
+ size_t len)
+{
+ size_t end;
+
+ if (!rq)
+ return false;
+
+ if (!rq->text_len)
+ return true;
+
+ end = start + len - 1;
+
+ if (spacing != 0)
+ {
+#define NUM_TAGS 5
+ static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" };
+ for (size_t i = 0; i < NUM_TAGS; i++)
+ {
+ hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end };
+ _raqm_add_font_feature (rq, fea);
+ }
+#undef NUM_TAGS
+ }
+
+ start = _raqm_encoding_to_u32_index (rq, start);
+ end = _raqm_encoding_to_u32_index (rq, end);
+
+ return _raqm_set_spacing (rq, spacing, false, start, end);
+}
+
+/**
+ * raqm_set_word_spacing_range:
+ * @rq: a #raqm_t.
+ * @spacing: amount of spacing in Freetype Font Units (26.6 format).
+ * @start: index of first character that should use @spacing.
+ * @len: number of characters using @spacing.
+ *
+ * Set the word spacing for a given range. Word spacing will only be applied to
+ * 'word separator' characters, such as 'space', 'no break space' and
+ * Ethiopic word separator'.
+ * The value will be added onto the advance and offset for RTL, and the advance
+ * for other directions.
+ *
+ * Return value:
+ * `true` if no errors happened, `false` otherwise.
+ *
+ * Since: 0.10
+ */
+bool
+raqm_set_word_spacing_range(raqm_t *rq,
+ int spacing,
+ size_t start,
+ size_t len)
+{
+ size_t end;
+
+ if (!rq)
+ return false;
+
+ if (!rq->text_len)
+ return true;
+
+ end = _raqm_encoding_to_u32_index (rq, start + len);
+ start = _raqm_encoding_to_u32_index (rq, start);
+
+ return _raqm_set_spacing (rq, spacing, true, start, end);
+}
+
/**
* raqm_set_invisible_glyph:
* @rq: a #raqm_t.
@@ -984,7 +1246,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq,
* If @gid is a positive number, it will be used for invisible glyphs.
*
* Return value:
- * %true if no errors happened, %false otherwise.
+ * `true` if no errors happened, `false` otherwise.
*
* Since: 0.6
*/
@@ -1014,7 +1276,7 @@ _raqm_shape (raqm_t *rq);
* text shaping, and any other part of the layout process.
*
* Return value:
- * %true if the layout process was successful, %false otherwise.
+ * `true` if the layout process was successful, `false` otherwise.
*
* Since: 0.1
*/
@@ -1048,7 +1310,9 @@ raqm_layout (raqm_t *rq)
static uint32_t
_raqm_u32_to_u8_index (raqm_t *rq,
uint32_t index);
-
+static uint32_t
+_raqm_u32_to_u16_index (raqm_t *rq,
+ uint32_t index);
/**
* raqm_get_glyphs:
* @rq: a #raqm_t.
@@ -1059,7 +1323,7 @@ _raqm_u32_to_u8_index (raqm_t *rq,
* information.
*
* Return value: (transfer none):
- * An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq
+ * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq
* and must not be freed.
*
* Since: 0.1
@@ -1147,6 +1411,12 @@ raqm_get_glyphs (raqm_t *rq,
RAQM_TEST ("\n");
#endif
}
+ else if (rq->text_utf16)
+ {
+ for (size_t i = 0; i < count; i++)
+ rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq,
+ rq->glyphs[i].cluster);
+ }
return rq->glyphs;
}
@@ -1194,8 +1464,10 @@ raqm_get_direction_at_index (raqm_t *rq,
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
{
- if (run->pos <= index && index < run->pos + run->len) {
- switch (run->direction) {
+ if (run->pos <= index && index < run->pos + run->len)
+ {
+ switch (run->direction)
+ {
case HB_DIRECTION_LTR:
return RAQM_DIRECTION_LTR;
case HB_DIRECTION_RTL:
@@ -1227,7 +1499,8 @@ _raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level)
return dir;
}
-typedef struct {
+typedef struct
+{
size_t pos;
size_t len;
_raqm_bidi_level_t level;
@@ -1264,10 +1537,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count)
line = SBParagraphCreateLine (par, 0, par_len);
*run_count = SBLineGetRunCount (line);
- if (SBParagraphGetBaseLevel (par) == 0)
- rq->resolved_dir = RAQM_DIRECTION_LTR;
- else
+ if (SBParagraphGetBaseLevel (par) == 1)
rq->resolved_dir = RAQM_DIRECTION_RTL;
+ else
+ rq->resolved_dir = RAQM_DIRECTION_LTR;
runs = malloc (sizeof (_raqm_bidi_run) * (*run_count));
if (runs)
@@ -1418,10 +1691,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count)
rq->text_len, &par_type,
levels);
- if (par_type == FRIBIDI_PAR_LTR)
- rq->resolved_dir = RAQM_DIRECTION_LTR;
- else
+ if (par_type == FRIBIDI_PAR_RTL)
rq->resolved_dir = RAQM_DIRECTION_RTL;
+ else
+ rq->resolved_dir = RAQM_DIRECTION_LTR;
if (max_level == 0)
goto done;
@@ -1447,22 +1720,15 @@ _raqm_itemize (raqm_t *rq)
bool ok = true;
#ifdef RAQM_TESTING
- switch (rq->base_dir)
- {
- case RAQM_DIRECTION_RTL:
- RAQM_TEST ("Direction is: RTL\n\n");
- break;
- case RAQM_DIRECTION_LTR:
- RAQM_TEST ("Direction is: LTR\n\n");
- break;
- case RAQM_DIRECTION_TTB:
- RAQM_TEST ("Direction is: TTB\n\n");
- break;
- case RAQM_DIRECTION_DEFAULT:
- default:
- RAQM_TEST ("Direction is: DEFAULT\n\n");
- break;
- }
+ static char *dir_names[] = {
+ "DEFAULT",
+ "RTL",
+ "LTR",
+ "TTB"
+ };
+
+ assert (rq->base_dir < sizeof (dir_names));
+ RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]);
#endif
if (!_raqm_resolve_scripts (rq))
@@ -1483,9 +1749,9 @@ _raqm_itemize (raqm_t *rq)
runs->len = rq->text_len;
runs->level = 0;
}
- } else {
- runs = _raqm_bidi_itemize (rq, &run_count);
}
+ else
+ runs = _raqm_bidi_itemize (rq, &run_count);
if (!runs)
{
@@ -1494,6 +1760,9 @@ _raqm_itemize (raqm_t *rq)
}
#ifdef RAQM_TESTING
+ assert (rq->resolved_dir < sizeof (dir_names));
+ if (rq->base_dir == RAQM_DIRECTION_DEFAULT)
+ RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]);
RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count);
RAQM_TEST ("BiDi Runs:\n");
@@ -1617,7 +1886,8 @@ done:
}
/* Stack to handle script detection */
-typedef struct {
+typedef struct
+{
size_t capacity;
size_t size;
int *pair_index;
@@ -1910,15 +2180,47 @@ _raqm_shape (raqm_t *rq)
{
FT_Matrix matrix;
+ hb_glyph_info_t *info;
hb_glyph_position_t *pos;
unsigned int len;
FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL);
pos = hb_buffer_get_glyph_positions (run->buffer, &len);
+ info = hb_buffer_get_glyph_infos (run->buffer, &len);
+
for (unsigned int i = 0; i < len; i++)
{
_raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix);
_raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix);
+
+ bool set_spacing = false;
+ if (run->direction == HB_DIRECTION_RTL)
+ {
+ set_spacing = i == 0;
+ if (!set_spacing)
+ set_spacing = info[i].cluster != info[i-1].cluster;
+ }
+ else
+ {
+ set_spacing = i == len - 1;
+ if (!set_spacing)
+ set_spacing = info[i].cluster != info[i+1].cluster;
+ }
+
+ _raqm_text_info rq_info = rq->text_info[info[i].cluster];
+
+ if (rq_info.spacing_after != 0 && set_spacing)
+ {
+ if (run->direction == HB_DIRECTION_TTB)
+ pos[i].y_advance -= rq_info.spacing_after;
+ else if (run->direction == HB_DIRECTION_RTL)
+ {
+ pos[i].x_advance += rq_info.spacing_after;
+ pos[i].x_offset += rq_info.spacing_after;
+ }
+ else
+ pos[i].x_advance += rq_info.spacing_after;
+ }
}
}
}
@@ -1954,9 +2256,9 @@ _raqm_u32_to_u8_index (raqm_t *rq,
}
/* Convert index from UTF-8 to UTF-32 */
-static uint32_t
+static size_t
_raqm_u8_to_u32_index (raqm_t *rq,
- uint32_t index)
+ size_t index)
{
const unsigned char *s = (const unsigned char *) rq->text_utf8;
const unsigned char *t = s;
@@ -1982,9 +2284,64 @@ _raqm_u8_to_u32_index (raqm_t *rq,
return length;
}
-static bool
-_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char,
- hb_codepoint_t r_char);
+/* Count equivalent UTF-16 short in codepoint */
+static size_t
+_raqm_count_codepoint_utf16_short (uint32_t chr)
+{
+ if (chr > 0x010000)
+ return 2;
+ else
+ return 1;
+}
+
+/* Convert index from UTF-32 to UTF-16 */
+static uint32_t
+_raqm_u32_to_u16_index (raqm_t *rq,
+ uint32_t index)
+{
+ size_t length = 0;
+
+ for (uint32_t i = 0; i < index; ++i)
+ length += _raqm_count_codepoint_utf16_short (rq->text[i]);
+
+ return length;
+}
+
+/* Convert index from UTF-16 to UTF-32 */
+static size_t
+_raqm_u16_to_u32_index (raqm_t *rq,
+ size_t index)
+{
+ const uint16_t *s = (const uint16_t *) rq->text_utf16;
+ const uint16_t *t = s;
+ size_t length = 0;
+
+ while (((size_t) (s - t) < index) && ('\0' != *s))
+ {
+ if (*s < 0xD800 || *s > 0xDBFF)
+ s += 1;
+ else
+ s += 2;
+
+ length++;
+ }
+
+ if ((size_t) (s-t) > index)
+ length--;
+
+ return length;
+}
+
+static inline size_t
+_raqm_encoding_to_u32_index (raqm_t *rq,
+ size_t index)
+{
+ if (rq->text_utf8)
+ return _raqm_u8_to_u32_index (rq, index);
+ else if (rq->text_utf16)
+ return _raqm_u16_to_u32_index (rq, index);
+ return index;
+}
static bool
_raqm_in_hangul_syllable (hb_codepoint_t ch);
@@ -2001,7 +2358,7 @@ _raqm_in_hangul_syllable (hb_codepoint_t ch);
* character is left-to-right, then the cursor will be at the right of it.
*
* Return value:
- * %true if the process was successful, %false otherwise.
+ * `true` if the process was successful, `false` otherwise.
*
* Since: 0.2
*/
@@ -2018,8 +2375,7 @@ raqm_index_to_position (raqm_t *rq,
if (rq == NULL)
return false;
- if (rq->text_utf8)
- *index = _raqm_u8_to_u32_index (rq, *index);
+ *index = _raqm_encoding_to_u32_index (rq, *index);
if (*index >= rq->text_len)
return false;
@@ -2077,6 +2433,8 @@ raqm_index_to_position (raqm_t *rq,
found:
if (rq->text_utf8)
*index = _raqm_u32_to_u8_index (rq, *index);
+ else if (rq->text_utf16)
+ *index = _raqm_u32_to_u16_index (rq, *index);
RAQM_TEST ("The position is %d at index %zu\n",*x ,*index);
return true;
}
@@ -2093,7 +2451,7 @@ found:
* @index.
*
* Return value:
- * %true if the process was successful, %false in case of error.
+ * `true` if the process was successful, `false` in case of error.
*
* Since: 0.2
*/
@@ -2371,8 +2729,8 @@ raqm_version_string (void)
* Checks if library version is less than or equal the specified version.
*
* Return value:
- * %true if library version is less than or equal the specfied version, %false
- * otherwise.
+ * `true` if library version is less than or equal the specified version,
+ * `false` otherwise.
*
* Since: 0.7
**/
@@ -2393,8 +2751,8 @@ raqm_version_atleast (unsigned int major,
* Checks if library version is less than or equal the specified version.
*
* Return value:
- * %true if library version is less than or equal the specfied version, %false
- * otherwise.
+ * `true` if library version is less than or equal the specified version,
+ * `false` otherwise.
*
* Since: 0.7
**/
diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h
index bdb5a50d8..2fd836c86 100644
--- a/src/thirdparty/raqm/raqm.h
+++ b/src/thirdparty/raqm/raqm.h
@@ -118,6 +118,10 @@ RAQM_API bool
raqm_set_text_utf8 (raqm_t *rq,
const char *text,
size_t len);
+RAQM_API bool
+raqm_set_text_utf16 (raqm_t *rq,
+ const uint16_t *text,
+ size_t len);
RAQM_API bool
raqm_set_par_direction (raqm_t *rq,
@@ -154,6 +158,17 @@ raqm_set_freetype_load_flags_range (raqm_t *rq,
size_t start,
size_t len);
+RAQM_API bool
+raqm_set_letter_spacing_range(raqm_t *rq,
+ int spacing,
+ size_t start,
+ size_t len);
+RAQM_API bool
+raqm_set_word_spacing_range(raqm_t *rq,
+ int spacing,
+ size_t start,
+ size_t len);
+
RAQM_API bool
raqm_set_invisible_glyph (raqm_t *rq,
int gid);
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index f5050946c..35980f19c 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -39,7 +39,7 @@ def cmd_rmdir(path):
def cmd_nmake(makefile=None, target="", params=None):
if params is None:
params = ""
- elif isinstance(params, list) or isinstance(params, tuple):
+ elif isinstance(params, (list, tuple)):
params = " ".join(params)
else:
params = str(params)
@@ -58,7 +58,7 @@ def cmd_nmake(makefile=None, target="", params=None):
def cmd_cmake(params=None, file="."):
if params is None:
params = ""
- elif isinstance(params, list) or isinstance(params, tuple):
+ elif isinstance(params, (list, tuple)):
params = " ".join(params)
else:
params = str(params)
@@ -109,9 +109,9 @@ header = [
deps = {
"libjpeg": {
"url": SF_PROJECTS
- + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download",
- "filename": "libjpeg-turbo-2.1.4.tar.gz",
- "dir": "libjpeg-turbo-2.1.4",
+ + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download",
+ "filename": "libjpeg-turbo-2.1.5.1.tar.gz",
+ "dir": "libjpeg-turbo-2.1.5.1",
"license": ["README.ijg", "LICENSE.md"],
"license_pattern": (
"(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n=========="
@@ -152,9 +152,9 @@ deps = {
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download",
- "filename": "xz-5.4.0.tar.gz",
- "dir": "xz-5.4.0",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download",
+ "filename": "xz-5.4.1.tar.gz",
+ "dir": "xz-5.4.1",
"license": "COPYING",
"patch": {
r"src\liblzma\api\lzma.h": {
@@ -177,9 +177,9 @@ deps = {
"libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"],
},
"libwebp": {
- "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz",
- "filename": "libwebp-1.2.4.tar.gz",
- "dir": "libwebp-1.2.4",
+ "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz",
+ "filename": "libwebp-1.3.0.tar.gz",
+ "dir": "libwebp-1.3.0",
"license": "COPYING",
"build": [
cmd_rmdir(r"output\release-static"), # clean
@@ -253,9 +253,9 @@ deps = {
"libs": ["*.lib"],
},
"freetype": {
- "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501
- "filename": "freetype-2.12.1.tar.gz",
- "dir": "freetype-2.12.1",
+ "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501
+ "filename": "freetype-2.13.0.tar.gz",
+ "dir": "freetype-2.13.0",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
"patch": {
r"builds\windows\vc2010\freetype.vcxproj": {
@@ -356,9 +356,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip",
- "filename": "harfbuzz-6.0.0.zip",
- "dir": "harfbuzz-6.0.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip",
+ "filename": "harfbuzz-7.0.1.zip",
+ "dir": "harfbuzz-7.0.1",
"license": "COPYING",
"build": [
cmd_set("CXXFLAGS", "-d2FH4-"),