diff --git a/.appveyor.yml b/.appveyor.yml
index 20908052b..b817cd9d8 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,7 +10,7 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python310
+ - PYTHON: C:/Python311
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python37-x64
diff --git a/.editorconfig b/.editorconfig
index d74549fe2..449530717 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,6 +13,10 @@ indent_style = space
trim_trailing_whitespace = true
+[*.rst]
+# Four-space indentation
+indent_size = 4
+
[*.yml]
# Two-space indentation
indent_size = 2
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6195f973b..49611e287 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.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
@@ -30,7 +30,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: "3.10"
+ python-version: "3.x"
cache: pip
cache-dependency-path: "setup.py"
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 65f2b81d5..dfd7d0553 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -2,7 +2,7 @@
set -e
-brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm
+brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
@@ -13,7 +13,6 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
python3 -m pip install numpy
# extra test images
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index ffac91cec..8c210bc90 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v6
+ uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 5b9ab0eda..7b8070d34 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.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
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-minor-version: [7, 8, 9]
+ python-minor-version: [8, 9]
timeout-minutes: 40
@@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v3
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v2
+ uses: cygwin/cygwin-install-action@v3
with:
platform: x86_64
packages: >
@@ -48,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
- uses: egor-tensin/cleanup-path@v2
+ uses: egor-tensin/cleanup-path@v3
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@@ -76,7 +76,7 @@ jobs:
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
- .ci/build.sh
+ SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
- name: Test
run: |
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index c68d43935..7331cf8ee 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.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
@@ -30,8 +30,8 @@ jobs:
centos-stream-9-amd64,
debian-10-buster-x86,
debian-11-bullseye-x86,
- fedora-35-amd64,
fedora-36-amd64,
+ fedora-37-amd64,
gentoo,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 6b7f62c23..487c3586f 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -19,9 +19,9 @@ jobs:
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
- - python-version: "pypy-3.7"
+ - python-version: "pypy3.8"
architecture: "x64"
- - python-version: "pypy-3.8"
+ - python-version: "pypy3.9"
architecture: "x64"
timeout-minutes: 30
@@ -141,7 +141,7 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_fribidi.cmd"
- # trim ~150MB x 9
+ # trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src
@@ -226,7 +226,7 @@ jobs:
path: dist\*.whl
- name: Upload fribidi.dll
- if: "github.event_name != 'pull_request' && matrix.python-version == 3.10"
+ if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
uses: actions/upload-artifact@v3
with:
name: fribidi
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 645384c02..11c7b77be 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.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
@@ -20,8 +20,8 @@ jobs:
"ubuntu-latest",
]
python-version: [
- "pypy-3.8",
- "pypy-3.7",
+ "pypy3.9",
+ "pypy3.8",
"3.11",
"3.10",
"3.9",
@@ -96,7 +96,7 @@ jobs:
path: Tests/errors
- name: Docs
- if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
+ if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11
run: |
make doccheck
diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml
deleted file mode 100644
index 69f9e5476..000000000
--- a/.github/workflows/tidelift.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Tidelift Align
-
-on:
- schedule:
- - cron: "30 2 * * *" # daily at 02:30 UTC
- push:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- pull_request:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- workflow_dispatch:
-
-permissions:
- contents: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- if: github.repository_owner == 'python-pillow'
- name: Run Tidelift to ensure approved open source packages are in use
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Scan
- uses: tidelift/alignment-action@main
- env:
- TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }}
- TIDELIFT_ORGANIZATION: team/aclark4life
- TIDELIFT_PROJECT: pillow
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f81bcb956..609352f22 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,18 +1,25 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.8.0
+ rev: 22.12.0
hooks:
- id: black
- args: ["--target-version", "py37"]
+ args: [--target-version=py37]
# Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$
types: []
- repo: https://github.com/PyCQA/isort
- rev: 5.10.1
+ rev: 5.11.1
hooks:
- id: isort
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.4
+ hooks:
+ - id: bandit
+ args: [--severity-level=high]
+ files: ^src/
+
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
@@ -25,7 +32,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8
- rev: 5.0.4
+ rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
@@ -37,16 +44,21 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.1
+ rev: v0.6.7
hooks:
- id: sphinx-lint
+ - repo: https://github.com/tox-dev/tox-ini-fmt
+ rev: 0.5.2
+ hooks:
+ - id: tox-ini-fmt
+
ci:
autoupdate_schedule: monthly
diff --git a/CHANGES.rst b/CHANGES.rst
index 6f2ba569e..76fc230a8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,78 @@
Changelog (Pillow)
==================
+9.4.0 (unreleased)
+------------------
+
+- Added DDS support for uncompressed L and LA images #6820
+ [radarhere, REDxEYE]
+
+- Added LightSource tag values to ExifTags #6749
+ [radarhere]
+
+- Fixed PyAccess after changing ICO size #6821
+ [radarhere]
+
+- Do not use EXIF from info when saving PNG images #6819
+ [radarhere]
+
+- Fixed saving EXIF data to MPO #6817
+ [radarhere]
+
+- Added Exif hide_offsets() #6762
+ [radarhere]
+
+- Only compare to previous frame when checking for duplicate GIF frames while saving #6787
+ [radarhere]
+
+- Always initialize all plugins in registered_extensions() #6811
+ [radarhere]
+
+- Ignore non-opaque WebP background when saving as GIF #6792
+ [radarhere]
+
+- Only set tile in ImageFile __setstate__ #6793
+ [radarhere]
+
+- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767
+ [radarhere]
+
+- Added IFD enum to ExifTags #6748
+ [radarhere]
+
+- Fixed bug combining GIF frame durations #6779
+ [radarhere]
+
+- Support saving JPEG comments #6774
+ [smason, radarhere]
+
+- Added getxmp() to WebPImagePlugin #6758
+ [radarhere]
+
+- Added "exact" option when saving WebP #6747
+ [ashafaei, radarhere]
+
+- Use fractional coordinates when drawing text #6722
+ [radarhere]
+
+- Fixed writing int as BYTE tag #6740
+ [radarhere]
+
+- Added MP Format Version when saving MPO #6735
+ [radarhere]
+
+- Added Interop to ExifTags #6724
+ [radarhere]
+
+- CVE-2007-4559 patch when building on Windows #6704
+ [TrellixVulnTeam, nulano, radarhere]
+
+- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714
+ [wiredfool]
+
+- Use verbose flag for pip install #6713
+ [wiredfool, radarhere]
+
9.3.0 (2022-10-29)
------------------
diff --git a/MANIFEST.in b/MANIFEST.in
index 08f6dfc08..f51551303 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,6 @@
include *.c
include *.h
include *.in
-include *.lock
include *.md
include *.py
include *.rst
@@ -10,7 +9,6 @@ include *.txt
include *.yaml
include LICENSE
include Makefile
-include Pipfile
include tox.ini
graft Tests
graft src
diff --git a/Makefile b/Makefile
index 8f2862948..a2545b54e 100644
--- a/Makefile
+++ b/Makefile
@@ -53,12 +53,12 @@ inplace: clean
.PHONY: install
install:
- python3 -m pip install .
+ python3 -m pip -v install .
python3 selftest.py
.PHONY: install-coverage
install-coverage:
- CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" .
+ CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" .
python3 selftest.py
.PHONY: debug
@@ -67,7 +67,7 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we
# see any build failures.
make clean > /dev/null
- CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
+ CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null
.PHONY: release-test
release-test:
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 1e611a63c..000000000
--- a/Pipfile
+++ /dev/null
@@ -1,22 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-black = "*"
-check-manifest = "*"
-coverage = "*"
-defusedxml = "*"
-packaging = "*"
-markdown2 = "*"
-olefile = "*"
-pyroma = "*"
-pytest = "*"
-pytest-cov = "*"
-pytest-timeout = "*"
-
-[dev-packages]
-
-[requires]
-python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 600b19050..000000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,324 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.9"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "attrs": {
- "hashes": [
- "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
- "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==21.2.0"
- },
- "black": {
- "hashes": [
- "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3",
- "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"
- ],
- "index": "pypi",
- "version": "==21.12b0"
- },
- "build": {
- "hashes": [
- "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f",
- "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==0.7.0"
- },
- "certifi": {
- "hashes": [
- "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
- "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
- ],
- "version": "==2021.10.8"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
- "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
- ],
- "markers": "python_version >= '3'",
- "version": "==2.0.9"
- },
- "check-manifest": {
- "hashes": [
- "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95",
- "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce"
- ],
- "index": "pypi",
- "version": "==0.47"
- },
- "click": {
- "hashes": [
- "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
- "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==8.0.3"
- },
- "coverage": {
- "hashes": [
- "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
- "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
- "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
- "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
- "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
- "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
- "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
- "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
- "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
- "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
- "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
- "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
- "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
- "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
- "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
- "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
- "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
- "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
- "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
- "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
- "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
- "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
- "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
- "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
- "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
- "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
- "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
- "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
- "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
- "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
- "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
- "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
- "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
- "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
- "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
- "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
- "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
- "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
- "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
- "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
- "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
- "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
- "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
- "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
- "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
- "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
- "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
- ],
- "index": "pypi",
- "version": "==6.2"
- },
- "defusedxml": {
- "hashes": [
- "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
- "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
- ],
- "index": "pypi",
- "version": "==0.7.1"
- },
- "docutils": {
- "hashes": [
- "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
- "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==0.18.1"
- },
- "idna": {
- "hashes": [
- "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
- "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
- ],
- "markers": "python_version >= '3'",
- "version": "==3.3"
- },
- "iniconfig": {
- "hashes": [
- "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
- "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
- ],
- "version": "==1.1.1"
- },
- "markdown2": {
- "hashes": [
- "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0",
- "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e"
- ],
- "index": "pypi",
- "version": "==2.4.2"
- },
- "mypy-extensions": {
- "hashes": [
- "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
- "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
- ],
- "version": "==0.4.3"
- },
- "olefile": {
- "hashes": [
- "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"
- ],
- "index": "pypi",
- "version": "==0.46"
- },
- "packaging": {
- "hashes": [
- "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
- "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
- ],
- "index": "pypi",
- "version": "==21.3"
- },
- "pathspec": {
- "hashes": [
- "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
- "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
- ],
- "version": "==0.9.0"
- },
- "pep517": {
- "hashes": [
- "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0",
- "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161"
- ],
- "version": "==0.12.0"
- },
- "platformdirs": {
- "hashes": [
- "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
- "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2.4.0"
- },
- "pluggy": {
- "hashes": [
- "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
- "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==1.0.0"
- },
- "py": {
- "hashes": [
- "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
- "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==1.11.0"
- },
- "pygments": {
- "hashes": [
- "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
- "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==2.10.0"
- },
- "pyparsing": {
- "hashes": [
- "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
- "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.6"
- },
- "pyroma": {
- "hashes": [
- "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65",
- "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a"
- ],
- "index": "pypi",
- "version": "==3.2"
- },
- "pytest": {
- "hashes": [
- "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
- "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
- ],
- "index": "pypi",
- "version": "==6.2.5"
- },
- "pytest-cov": {
- "hashes": [
- "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
- "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
- ],
- "index": "pypi",
- "version": "==3.0.0"
- },
- "pytest-timeout": {
- "hashes": [
- "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112",
- "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717"
- ],
- "index": "pypi",
- "version": "==2.0.2"
- },
- "requests": {
- "hashes": [
- "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
- "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==2.26.0"
- },
- "setuptools": {
- "hashes": [
- "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
- "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==60.0.0"
- },
- "toml": {
- "hashes": [
- "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
- "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
- ],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
- "version": "==0.10.2"
- },
- "tomli": {
- "hashes": [
- "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
- "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==1.2.3"
- },
- "typing-extensions": {
- "hashes": [
- "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
- "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==4.0.1"
- },
- "urllib3": {
- "hashes": [
- "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
- "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.26.7"
- }
- },
- "develop": {}
-}
diff --git a/README.md b/README.md
index e7c0ebc5a..8ee68f9b8 100644
--- a/README.md
+++ b/README.md
@@ -54,9 +54,9 @@ As of 2019, Pillow development is
-
+
diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif
new file mode 100644
index 000000000..ef0c894a5
Binary files /dev/null and b/Tests/images/duplicate_frame.gif differ
diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png
new file mode 100644
index 000000000..4a362535f
Binary files /dev/null and b/Tests/images/flower_thumbnail.png differ
diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png
index cf002b12c..7e98b8eac 100644
Binary files a/Tests/images/test_anchor_multiline_mm_right.png and b/Tests/images/test_anchor_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png
index 7b1e9c4e4..6a1513024 100644
Binary files a/Tests/images/test_combine_multiline_lm_center.png and b/Tests/images/test_combine_multiline_lm_center.png differ
diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png
index a26996c2d..8eb254fdf 100644
Binary files a/Tests/images/test_combine_multiline_lm_left.png and b/Tests/images/test_combine_multiline_lm_left.png differ
diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png
index 7caf5cb74..cb640a740 100644
Binary files a/Tests/images/test_combine_multiline_lm_right.png and b/Tests/images/test_combine_multiline_lm_right.png differ
diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png
index a859e9570..d1146b8b8 100644
Binary files a/Tests/images/test_combine_multiline_mm_center.png and b/Tests/images/test_combine_multiline_mm_center.png differ
diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png
index aadb5191f..f539a8e62 100644
Binary files a/Tests/images/test_combine_multiline_mm_left.png and b/Tests/images/test_combine_multiline_mm_left.png differ
diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png
index 8238d4ec8..02634163e 100644
Binary files a/Tests/images/test_combine_multiline_mm_right.png and b/Tests/images/test_combine_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png
index 7568dd63a..4cce8f6a0 100644
Binary files a/Tests/images/test_combine_multiline_rm_center.png and b/Tests/images/test_combine_multiline_rm_center.png differ
diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png
index b8c3b5b14..93d8162b3 100644
Binary files a/Tests/images/test_combine_multiline_rm_left.png and b/Tests/images/test_combine_multiline_rm_left.png differ
diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png
index 14c478a72..6c4634560 100644
Binary files a/Tests/images/test_combine_multiline_rm_right.png and b/Tests/images/test_combine_multiline_rm_right.png differ
diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png
index 49468698c..d2270826a 100644
Binary files a/Tests/images/text_float_coord.png and b/Tests/images/text_float_coord.png differ
diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png
index 50bdac3d8..2287071ff 100644
Binary files a/Tests/images/text_float_coord_1_alt.png and b/Tests/images/text_float_coord_1_alt.png differ
diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds
new file mode 100644
index 000000000..b82282587
Binary files /dev/null and b/Tests/images/uncompressed_l.dds differ
diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png
new file mode 100644
index 000000000..9d22a26a4
Binary files /dev/null and b/Tests/images/uncompressed_l.png differ
diff --git a/Tests/images/uncompressed_la.dds b/Tests/images/uncompressed_la.dds
new file mode 100644
index 000000000..30bf93576
Binary files /dev/null and b/Tests/images/uncompressed_la.dds differ
diff --git a/Tests/images/uncompressed_la.png b/Tests/images/uncompressed_la.png
new file mode 100644
index 000000000..0d4ea602f
Binary files /dev/null and b/Tests/images/uncompressed_la.png differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 09cc7bc16..7e9098f53 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -19,29 +19,17 @@ python3 setup.py build --build-base=/tmp/build install
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
- fuzzer_basename=$(basename -s .py $fuzzer)
- fuzzer_package=${fuzzer_basename}.pkg
- pyinstaller \
+ compile_python_fuzzer $fuzzer \
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
--add-binary /usr/local/lib/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \
--add-binary /usr/local/lib/libopenjp2.so.7:. \
--add-binary /usr/local/lib/libpng16.so.16:. \
- --add-binary /usr/local/lib/libtiff.so.5:. \
+ --add-binary /usr/local/lib/libtiff.so.6:. \
--add-binary /usr/local/lib/libwebp.so.7:. \
--add-binary /usr/local/lib/libwebpdemux.so.2:. \
--add-binary /usr/local/lib/libwebpmux.so.3:. \
- --add-binary /usr/local/lib/libxcb.so.1:. \
- --distpath $OUT --onefile --name $fuzzer_package $fuzzer
-
- # Create execution wrapper.
- echo "#!/bin/sh
-# LLVMFuzzerTestOneInput for fuzzer detection.
-this_dir=\$(dirname \"\$0\")
-LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
-ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
-\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
- chmod u+x $OUT/$fuzzer_basename
+ --add-binary /usr/local/lib/libxcb.so.1:.
done
find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index b17aad2ea..ed9aff9cc 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -35,6 +35,7 @@ def test_questionable():
"pal8os2v2.bmp",
"rgb24prof.bmp",
"pal1p1.bmp",
+ "pal4rletrns.bmp",
"pal8offs.bmp",
"rgb24lprof.bmp",
"rgb32fakealpha.bmp",
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 4b9f8949e..cac4108a8 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -22,6 +22,8 @@ TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds"
+TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
+TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@@ -194,26 +196,24 @@ def test_unimplemented_dxgi_format():
pass
-def test_uncompressed_rgb():
- """Check uncompressed RGB images can be opened"""
+@pytest.mark.parametrize(
+ ("mode", "size", "test_file"),
+ [
+ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
+ ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
+ ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
+ ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
+ ],
+)
+def test_uncompressed(mode, size, test_file):
+ """Check uncompressed images can be opened"""
- # convert -format dds -define dds:compression=none hopper.jpg hopper.dds
- with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
+ with Image.open(test_file) as im:
assert im.format == "DDS"
- assert im.mode == "RGB"
- assert im.size == (128, 128)
+ assert im.mode == mode
+ assert im.size == size
- assert_image_equal_tofile(im, "Tests/images/hopper.png")
-
- # Test image with alpha
- with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im:
- assert im.format == "DDS"
- assert im.mode == "RGBA"
- assert im.size == (800, 600)
-
- assert_image_equal_tofile(
- im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png")
- )
+ assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
def test__accept_true():
@@ -305,6 +305,8 @@ def test_save_unsupported_mode(tmp_path):
@pytest.mark.parametrize(
("mode", "test_file"),
[
+ ("L", "Tests/images/linear_gradient.png"),
+ ("LA", "Tests/images/uncompressed_la.png"),
("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"),
],
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 926f5c1ee..d48fc1442 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
+def test_dispose2_background_frame(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ im_list = [Image.new("RGBA", (1, 20))]
+
+ different_frame = Image.new("RGBA", (1, 20))
+ different_frame.putpixel((0, 10), (255, 0, 0, 255))
+ im_list.append(different_frame)
+
+ # Frame that matches the background
+ im_list.append(Image.new("RGBA", (1, 20)))
+
+ im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
+
+ with Image.open(out) as im:
+ assert im.n_frames == 3
+
+
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
@@ -791,6 +809,22 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list
+def test_roundtrip_info_duration_combined(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ with Image.open("Tests/images/duplicate_frame.gif") as im:
+ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
+ 1000,
+ 1000,
+ 1000,
+ ]
+ im.save(out, save_all=True)
+
+ with Image.open(out) as reloaded:
+ assert [
+ frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
+ ] == [1000, 2000]
+
+
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@@ -859,14 +893,23 @@ def test_background(tmp_path):
im.info["background"] = 1
im.save(out)
with Image.open(out) as reread:
-
assert reread.info["background"] == im.info["background"]
+
+def test_webp_background(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ # Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
with Image.open("Tests/images/hopper.webp") as im:
- assert isinstance(im.info["background"], tuple)
+ assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
+ # Test non-opaque WebP background
+ im = Image.new("L", (100, 100), "#000")
+ im.info["background"] = (0, 0, 0, 0)
+ im.save(out)
+
def test_comment(tmp_path):
with Image.open(TEST_GIF) as im:
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 3fcd5c61f..afb17b1af 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -71,6 +71,19 @@ def test_save_to_bytes():
)
+def test_getpixel(tmp_path):
+ temp_file = str(tmp_path / "temp.ico")
+
+ im = hopper()
+ im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
+
+ with Image.open(temp_file) as reloaded:
+ reloaded.load()
+ reloaded.size = (32, 32)
+
+ assert reloaded.getpixel((0, 0)) == (18, 20, 62)
+
+
def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index fa96e425b..eabc6bf75 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -86,6 +86,33 @@ class TestFileJpeg:
assert len(im.applist) == 2
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+ assert im.app["COM"] == im.info["comment"]
+
+ def test_comment_write(self):
+ with Image.open(TEST_FILE) as im:
+ assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+
+ # Test that existing comment is saved by default
+ out = BytesIO()
+ im.save(out, format="JPEG")
+ with Image.open(out) as reloaded:
+ assert im.info["comment"] == reloaded.info["comment"]
+
+ # Ensure that a blank comment causes any existing comment to be removed
+ for comment in ("", b"", None):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ assert "comment" not in reloaded.info
+
+ # Test that a comment argument overrides the default comment
+ for comment in ("Test comment text", b"Text comment text"):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ if not isinstance(comment, bytes):
+ comment = comment.encode()
+ assert reloaded.info["comment"] == comment
def test_cmyk(self):
# Test CMYK handling. Thanks to Tim and Charlie for test data,
@@ -415,6 +442,13 @@ class TestFileJpeg:
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"
+ def test_get_child_images(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ ims = im.get_child_images()
+
+ assert len(ims) == 1
+ assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png")
+
def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index d94bdaa96..3e5476222 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -80,7 +80,10 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files)
def test_exif(test_file):
- with Image.open(test_file) as im:
+ with Image.open(test_file) as im_original:
+ im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
+
+ for im in (im_original, im_reloaded):
info = im._getexif()
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
@@ -268,6 +271,7 @@ def test_save_all():
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
+ assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 37235fe6f..9481cd5dd 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -706,10 +706,18 @@ class TestFilePng:
assert exif[274] == 3
def test_exif_save(self, tmp_path):
+ # Test exif is not saved from info
+ test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/exif.png") as im:
- test_file = str(tmp_path / "temp.png")
im.save(test_file)
+ with Image.open(test_file) as reloaded:
+ assert reloaded._getexif() is None
+
+ # Test passing in exif
+ with Image.open("Tests/images/exif.png") as im:
+ im.save(test_file, exif=im.getexif())
+
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
assert exif[274] == 1
@@ -720,7 +728,7 @@ class TestFilePng:
def test_exif_from_jpg(self, tmp_path):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png")
- im.save(test_file)
+ im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index d38c1c523..b90dde3d9 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -201,6 +201,22 @@ def test_writing_bytes_to_ascii(tmp_path):
assert reloaded.tag_v2[271] == "test"
+def test_writing_int_to_bytes(tmp_path):
+ im = hopper()
+ info = TiffImagePlugin.ImageFileDirectory_v2()
+
+ tag = TiffTags.TAGS_V2[700]
+ assert tag.type == TiffTags.BYTE
+
+ info[700] = 1
+
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.tag_v2[700] == b"\x01"
+
+
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_alpha.py b/Tests/test_file_webp_alpha.py
index dc82fb742..5970fd2a3 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -97,6 +97,35 @@ def test_write_rgba(tmp_path):
assert_image_similar(image, pil_image, 1.0)
+def test_keep_rgb_values_when_transparent(tmp_path):
+ """
+ Saving transparent pixels should retain their original RGB values
+ when using the "exact" parameter.
+ """
+
+ image = hopper("RGB")
+
+ # create a copy of the image
+ # with the left half transparent
+ half_transparent_image = image.copy()
+ new_alpha = Image.new("L", (128, 128), 255)
+ new_alpha.paste(0, (0, 0, 64, 128))
+ half_transparent_image.putalpha(new_alpha)
+
+ # save with transparent area preserved
+ temp_file = str(tmp_path / "temp.webp")
+ half_transparent_image.save(temp_file, exact=True, lossless=True)
+
+ with Image.open(temp_file) as reloaded:
+ assert reloaded.mode == "RGBA"
+ assert reloaded.format == "WEBP"
+
+ # even though it is lossless, if we don't use exact=True
+ # in libwebp >= 0.5, the transparent area will be filled with black
+ # (or something more conducive to compression)
+ assert_image_equal(reloaded.convert("RGB"), image)
+
+
def test_write_unsupported_mode_PA(tmp_path):
"""
Saving a palette-based file with transparency to WebP format
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index f77a245c0..4f513d82b 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -11,6 +11,11 @@ pytestmark = [
skip_unless_feature("webp_mux"),
]
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ ElementTree = None
+
def test_read_exif_metadata():
@@ -110,6 +115,22 @@ def test_read_no_exif():
assert not webp_image._getexif()
+def test_getxmp():
+ with Image.open("Tests/images/flower.webp") as im:
+ assert "xmp" not in im.info
+ assert im.getxmp() == {}
+
+ with Image.open("Tests/images/flower2.webp") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ assert (
+ im.getxmp()["xmpmeta"]["xmptk"]
+ == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
+ )
+
+
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path):
iccp_data = b""
diff --git a/Tests/test_image.py b/Tests/test_image.py
index e57903490..a37c90296 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -7,7 +7,14 @@ import warnings
import pytest
-from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
+from PIL import (
+ ExifTags,
+ Image,
+ ImageDraw,
+ ImagePalette,
+ UnidentifiedImageError,
+ features,
+)
from .helper import (
assert_image_equal,
@@ -394,8 +401,6 @@ class TestImage:
def test_registered_extensions_uninitialized(self):
# Arrange
Image._initialized = 0
- extension = Image.EXTENSION
- Image.EXTENSION = {}
# Act
Image.registered_extensions()
@@ -403,10 +408,6 @@ class TestImage:
# Assert
assert Image._initialized == 2
- # Restore the original state and assert
- Image.EXTENSION = extension
- assert Image.EXTENSION
-
def test_registered_extensions(self):
# Arrange
# Open an image to trigger plugin registration
@@ -808,6 +809,18 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)
+ def test_exif_ifd1(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ exif = im.getexif()
+ assert exif.get_ifd(ExifTags.IFD.IFD1) == {
+ 513: 2036,
+ 514: 5448,
+ 259: 6,
+ 296: 2,
+ 282: 180.0,
+ 283: 180.0,
+ }
+
def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
@@ -838,6 +851,31 @@ class TestImage:
34665: 196,
}
+ def test_exif_hide_offsets(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ exif = im.getexif()
+
+ # Check offsets are present initially
+ assert 0x8769 in exif
+ for tag in (0xA005, 0x927C):
+ assert tag in exif.get_ifd(0x8769)
+ assert exif.get_ifd(0xA005)
+ loaded_exif = exif
+
+ with Image.open("Tests/images/flower.jpg") as im:
+ new_exif = im.getexif()
+
+ for exif in (loaded_exif, new_exif):
+ exif.hide_offsets()
+
+ # Assert they are hidden afterwards,
+ # but that the IFDs are still available
+ assert 0x8769 not in exif
+ assert exif.get_ifd(0x8769)
+ for tag in (0xA005, 0x927C):
+ assert tag not in exif.get_ifd(0x8769)
+ assert exif.get_ifd(0xA005)
+
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size):
im = Image.new("RGB", size)
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 902d8bf8f..0a7202a33 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -104,6 +104,13 @@ def test_rgba_p():
assert_image_similar(im, comparable, 20)
+def test_rgba():
+ with Image.open("Tests/images/transparent.png") as im:
+ assert im.mode == "RGBA"
+
+ assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
+
+
def test_trns_p(tmp_path):
im = hopper("P")
im.info["transparency"] = 0
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 3d60e52a2..0e6293349 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -55,10 +55,11 @@ def test_mode_with_L_with_float():
assert im.getpixel((0, 0)) == 2
-def test_mode_i():
+@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
+def test_mode_i(mode):
src = hopper("L")
data = list(src.getdata())
- im = Image.new("I", src.size, 0)
+ im = Image.new(mode, src.size, 0)
im.putdata(data, 2, 256)
target = [2 * elt + 256 for elt in data]
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 76b7c65cc..4c4c41b7b 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1238,6 +1238,27 @@ def test_stroke_descender():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
+@skip_unless_feature("freetype2")
+def test_split_word():
+ # Arrange
+ im = Image.new("RGB", (230, 55))
+ expected = im.copy()
+ expected_draw = ImageDraw.Draw(expected)
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48)
+ expected_draw.text((0, 0), "paradise", font=font)
+
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.text((0, 0), "par", font=font)
+
+ length = draw.textlength("par", font=font)
+ draw.text((length, 0), "adise", font=font)
+
+ # Assert
+ assert_image_equal(im, expected)
+
+
@skip_unless_feature("freetype2")
def test_stroke_multiline():
# Arrange
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index fa2291582..5e0eca28b 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,4 +1,5 @@
import os
+import shutil
import subprocess
import sys
@@ -33,7 +34,9 @@ class TestImageGrab:
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
def test_grab_no_xcb(self):
- if sys.platform not in ("win32", "darwin"):
+ if sys.platform not in ("win32", "darwin") and not shutil.which(
+ "gnome-screenshot"
+ ):
with pytest.raises(OSError) as e:
ImageGrab.grab()
assert str(e.value).startswith("Pillow was built without XCB support")
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 185e477ec..3de7ec30f 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -34,7 +34,7 @@ def test_numpy_to_image():
# Check supported 1-bit integer formats
assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE)
- assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE)
+ assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE)
# Check supported 8-bit integer formats
assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE)
@@ -193,7 +193,7 @@ def test_putdata():
"dtype",
(
bool,
- numpy.bool8,
+ numpy.bool_,
numpy.int8,
numpy.int16,
numpy.int32,
diff --git a/docs/Makefile b/docs/Makefile
index 458299aac..d32d25a3c 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -15,11 +15,12 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
+.PHONY: help
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
+ @echo " serve to start a local server for viewing docs"
+ @echo " livehtml to start a local server for viewing docs and auto-reload on change"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@@ -39,42 +40,49 @@ help:
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
+.PHONY: clean
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
- $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
+ $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph
+.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
+.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
+.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@@ -82,6 +90,7 @@ htmlhelp:
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
+.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@@ -92,6 +101,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
+.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@@ -102,12 +112,14 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
@echo "# devhelp"
+.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -116,6 +128,7 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
+.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -123,18 +136,21 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
+.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -143,6 +159,7 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
+.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -150,18 +167,21 @@ info:
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
+.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@@ -169,14 +189,17 @@ linkcheck:
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
+.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
+.PHONY: livehtml
livehtml: html
livereload $(BUILDDIR)/html -p 33233
+.PHONY: serve
serve:
cd $(BUILDDIR)/html; $(PYTHON) -m http.server
diff --git a/docs/conf.py b/docs/conf.py
index bc67d9368..04823e2d7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,12 +27,13 @@ needs_sphinx = "2.4"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- "sphinx_copybutton",
- "sphinx_issues",
- "sphinx_removed_in",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
+ "sphinx_copybutton",
+ "sphinx_inline_tabs",
+ "sphinx_issues",
+ "sphinx_removed_in",
"sphinxext.opengraph",
]
diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst
index a9b33e437..f3fa1f2b1 100644
--- a/docs/handbook/concepts.rst
+++ b/docs/handbook/concepts.rst
@@ -24,9 +24,10 @@ To get the number and names of bands in an image, use the
Modes
-----
-The ``mode`` of an image is a string which defines the type and depth of a pixel in the image.
-Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range
-of 0-1, an 8-bit pixel has a range of 0-255 and so on. The current release
+The ``mode`` of an image is a string which defines the type and depth of a pixel in the
+image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of
+0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of
+INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release
supports the following standard modes:
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
@@ -41,6 +42,9 @@ supports the following standard modes:
* ``LAB`` (3x8-bit pixels, the L*a*b color space)
* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space)
+
+ * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees
+
* ``I`` (32-bit signed integer pixels)
* ``F`` (32-bit floating point pixels)
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 93ae1b054..a41ef7cf8 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -474,6 +474,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0
+**comment**
+ A comment about the image.
+
+ .. versionadded:: 9.4.0
+
.. note::
@@ -1129,6 +1134,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
+**exact**
+ If true, preserve the transparent RGB values. Otherwise, discard
+ invisible RGB values for better compression. Defaults to false.
+ Requires libwebp 0.5.0 or later.
+
**icc_profile**
The ICC Profile to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
diff --git a/docs/index.rst b/docs/index.rst
index 45af4c571..5bcd5afa5 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -57,9 +57,9 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_ and packages
+ are tested by the ports team with all supported FreeBSD versions.
-macOS Installation
-^^^^^^^^^^^^^^^^^^
-
-We provide binaries for macOS for each of the supported Python
-versions in the wheel format. These include support for all optional
-libraries except libimagequant. Raqm support requires
-FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
-Linux Installation
-^^^^^^^^^^^^^^^^^^
-
-We provide binaries for Linux for each of the supported Python
-versions in the manylinux wheel format. These include support for all
-optional libraries except libimagequant. Raqm support requires
-FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
-Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
-also include Pillow in packages that previously contained PIL e.g.
-``python-imaging``. Debian splits it into two packages, ``python3-pil``
-and ``python3-pil.imagetk``.
-
-FreeBSD Installation
-^^^^^^^^^^^^^^^^^^^^
-
-Pillow can be installed on FreeBSD via the official Ports or Packages systems:
-
-**Ports**::
-
- cd /usr/ports/graphics/py-pillow && make install clean
-
-**Packages**::
-
- pkg install py38-pillow
-
-.. note::
-
- The `Pillow FreeBSD port
- `_ and packages
- are tested by the ports team with all supported FreeBSD versions.
-
+.. _Building on Linux:
+.. _Building on macOS:
+.. _Building on Windows:
+.. _Building on Windows using MSYS2/MinGW:
+.. _Building on FreeBSD:
+.. _Building on Android:
Building From Source
--------------------
-Download and extract the `compressed archive from PyPI`_.
-
-.. _compressed archive from PyPI: https://pypi.org/project/Pillow/
-
.. _external-libraries:
External Libraries
@@ -140,14 +143,14 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5**
* **libfreetype** provides type related services
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
- above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**.
+ above uses liblcms2. Tested with **1.19** and **2.7-2.14**.
* **libwebp** provides the WebP format.
@@ -191,7 +194,141 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support.
-Once you have installed the prerequisites, run::
+.. tab:: Linux
+
+ If you didn't build Python from source, make sure you have Python's
+ development libraries installed.
+
+ In Debian or Ubuntu::
+
+ sudo apt-get install python3-dev python3-setuptools
+
+ In Fedora, the command is::
+
+ sudo dnf install python3-devel redhat-rpm-config
+
+ In Alpine, the command is::
+
+ sudo apk add python3-dev py3-setuptools
+
+ .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
+
+ Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
+
+ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
+ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
+ libharfbuzz-dev libfribidi-dev libxcb1-dev
+
+ To install libraqm, ``sudo apt-get install meson`` and then see
+ ``depends/install_raqm.sh``.
+
+ Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
+
+ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
+ freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
+ harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
+
+ Note that the package manager may be yum or DNF, depending on the
+ exact distribution.
+
+ Prerequisites are installed for **Alpine** with::
+
+ sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
+ libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
+ libxcb-dev libpng-dev
+
+ See also the ``Dockerfile``\s in the Test Infrastructure repo
+ (https://github.com/python-pillow/docker-images) for a known working
+ install process for other tested distros.
+
+.. tab:: macOS
+
+ The Xcode command line tools are required to compile portions of
+ Pillow. The tools are installed by running ``xcode-select --install``
+ from the command line. The command line tools are required even if you
+ have the full Xcode package installed. It may be necessary to run
+ ``sudo xcodebuild -license`` to accept the license prior to using the
+ tools.
+
+ The easiest way to install external libraries is via `Homebrew
+ `_. After you install Homebrew, run::
+
+ brew install libjpeg libtiff little-cms2 openjpeg webp
+
+ To install libraqm on macOS use Homebrew to install its dependencies::
+
+ brew install freetype harfbuzz fribidi
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Windows
+
+ We recommend you use prebuilt wheels from PyPI.
+ If you wish to compile Pillow manually, you can use the build scripts
+ in the ``winbuild`` directory used for CI testing and development.
+ These scripts require Visual Studio 2017 or newer and NASM.
+
+ The scripts also install Pillow from the local copy of the source code, so the
+ `Installing`_ instructions will not be necessary afterwards.
+
+.. tab:: Windows using MSYS2/MinGW
+
+ To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
+ **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
+
+ The following instructions target the 64-bit build, for 32-bit
+ replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
+
+ Make sure you have Python and GCC installed::
+
+ pacman -S \
+ mingw-w64-x86_64-gcc \
+ mingw-w64-x86_64-python3 \
+ mingw-w64-x86_64-python3-pip \
+ mingw-w64-x86_64-python3-setuptools
+
+ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
+
+ pacman -S \
+ mingw-w64-x86_64-libjpeg-turbo \
+ mingw-w64-x86_64-zlib \
+ mingw-w64-x86_64-libtiff \
+ mingw-w64-x86_64-freetype \
+ mingw-w64-x86_64-lcms2 \
+ mingw-w64-x86_64-libwebp \
+ mingw-w64-x86_64-openjpeg2 \
+ mingw-w64-x86_64-libimagequant \
+ mingw-w64-x86_64-libraqm
+
+.. tab:: FreeBSD
+
+ .. Note:: Only FreeBSD 10 and 11 tested
+
+ Make sure you have Python's development libraries installed::
+
+ sudo pkg install python3
+
+ Prerequisites are installed on **FreeBSD 10 or 11** with::
+
+ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Android
+
+ Basic Android support has been added for compilation within the Termux
+ environment. The dependencies can be installed by::
+
+ pkg install -y python ndk-sysroot clang make \
+ libjpeg-turbo
+
+ This has been tested within the Termux app on ChromeOS, on x86.
+
+Installing
+^^^^^^^^^^
+
+Once you have installed the prerequisites, to install Pillow from the source
+code on PyPI, run::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
@@ -211,9 +348,19 @@ prerequisites, it may be necessary to manually clear the pip cache or
build without cache using the ``--no-cache-dir`` option to force a
build with newly installed external libraries.
+If you would like to install from a local copy of the source code instead, you
+can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
+or download and extract the `compressed archive from PyPI`_.
+
+After navigating to the Pillow directory, run::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install .
+
+.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files
Build Options
-^^^^^^^^^^^^^
+"""""""""""""
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
@@ -256,157 +403,6 @@ Sample usage::
python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
-
-Building on macOS
-^^^^^^^^^^^^^^^^^
-
-The Xcode command line tools are required to compile portions of
-Pillow. The tools are installed by running ``xcode-select --install``
-from the command line. The command line tools are required even if you
-have the full Xcode package installed. It may be necessary to run
-``sudo xcodebuild -license`` to accept the license prior to using the
-tools.
-
-The easiest way to install external libraries is via `Homebrew
-`_. After you install Homebrew, run::
-
- brew install libjpeg libtiff little-cms2 openjpeg webp
-
-To install libraqm on macOS use Homebrew to install its dependencies::
-
- brew install freetype harfbuzz fribidi
-
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-Now install Pillow with::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
-
-or from within the uncompressed source directory::
-
- python3 -m pip install .
-
-Building on Windows
-^^^^^^^^^^^^^^^^^^^
-
-We recommend you use prebuilt wheels from PyPI.
-If you wish to compile Pillow manually, you can use the build scripts
-in the ``winbuild`` directory used for CI testing and development.
-These scripts require Visual Studio 2017 or newer and NASM.
-
-Building on Windows using MSYS2/MinGW
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
-**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
-
-The following instructions target the 64-bit build, for 32-bit
-replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
-
-Make sure you have Python and GCC installed::
-
- pacman -S \
- mingw-w64-x86_64-gcc \
- mingw-w64-x86_64-python3 \
- mingw-w64-x86_64-python3-pip \
- mingw-w64-x86_64-python3-setuptools
-
-Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
-
- pacman -S \
- mingw-w64-x86_64-libjpeg-turbo \
- mingw-w64-x86_64-zlib \
- mingw-w64-x86_64-libtiff \
- mingw-w64-x86_64-freetype \
- mingw-w64-x86_64-lcms2 \
- mingw-w64-x86_64-libwebp \
- mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-libimagequant \
- mingw-w64-x86_64-libraqm
-
-Now install Pillow with::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
-
-
-Building on FreeBSD
-^^^^^^^^^^^^^^^^^^^
-
-.. Note:: Only FreeBSD 10 and 11 tested
-
-Make sure you have Python's development libraries installed::
-
- sudo pkg install python3
-
-Prerequisites are installed on **FreeBSD 10 or 11** with::
-
- sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
-
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-
-Building on Linux
-^^^^^^^^^^^^^^^^^
-
-If you didn't build Python from source, make sure you have Python's
-development libraries installed.
-
-In Debian or Ubuntu::
-
- sudo apt-get install python3-dev python3-setuptools
-
-In Fedora, the command is::
-
- sudo dnf install python3-devel redhat-rpm-config
-
-In Alpine, the command is::
-
- sudo apk add python3-dev py3-setuptools
-
-.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
-
-Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
-
- sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
- libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
- libharfbuzz-dev libfribidi-dev libxcb1-dev
-
-To install libraqm, ``sudo apt-get install meson`` and then see
-``depends/install_raqm.sh``.
-
-Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
-
- sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
- freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
- harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
-
-Note that the package manager may be yum or DNF, depending on the
-exact distribution.
-
-Prerequisites are installed for **Alpine** with::
-
- sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
- libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
- libxcb-dev libpng-dev
-
-See also the ``Dockerfile``\s in the Test Infrastructure repo
-(https://github.com/python-pillow/docker-images) for a known working
-install process for other tested distros.
-
-Building on Android
-^^^^^^^^^^^^^^^^^^^
-
-Basic Android support has been added for compilation within the Termux
-environment. The dependencies can be installed by::
-
- pkg install -y python ndk-sysroot clang make \
- libjpeg-turbo
-
-This has been tested within the Termux app on ChromeOS, on x86.
-
-
Platform Support
----------------
@@ -440,10 +436,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 35 | 3.10 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| Fedora 37 | 3.11 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
@@ -464,7 +460,7 @@ These platforms are built and tested for every change.
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+
-| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
+| | 3.8, 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@@ -482,11 +478,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
-| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm |
+| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.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 | 9.2.0 |x86-64 |
+| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 |
| +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index ff5788524..464ab77ea 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -4,8 +4,50 @@
:py:mod:`~PIL.ExifTags` Module
==============================
-The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which
-provide constants and clear-text names for various well-known EXIF tags.
+The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
+which provide constants and clear-text names for various well-known EXIF tags.
+
+.. py:data:: Base
+
+ >>> from PIL.ExifTags import Base
+ >>> Base.ImageDescription.value
+ 270
+ >>> Base(270).name
+ 'ImageDescription'
+
+.. py:data:: GPS
+
+ >>> from PIL.ExifTags import GPS
+ >>> GPS.GPSDestLatitude.value
+ 20
+ >>> GPS(20).name
+ 'GPSDestLatitude'
+
+.. py:data:: Interop
+
+ >>> from PIL.ExifTags import Interop
+ >>> Interop.RelatedImageFileFormat.value
+ 4096
+ >>> Interop(4096).name
+ 'RelatedImageFileFormat'
+
+.. py:data:: IFD
+
+ >>> from PIL.ExifTags import IFD
+ >>> IFD.Exif.value
+ 34665
+ >>> IFD(34665).name
+ 'Exif
+
+.. py:data:: LightSource
+
+ >>> from PIL.ExifTags import LightSource
+ >>> LightSource.Unknown.value
+ 0
+ >>> LightSource(0).name
+ 'Unknown'
+
+Two of these values are also exposed as dictionaries.
.. py:data:: TAGS
:type: dict
@@ -26,22 +68,3 @@ provide constants and clear-text names for various well-known EXIF tags.
>>> from PIL.ExifTags import GPSTAGS
>>> GPSTAGS[20]
'GPSDestLatitude'
-
-
-These values are also exposed as ``enum.IntEnum`` classes.
-
-.. py:data:: Base
-
- >>> from PIL.ExifTags import Base
- >>> Base.ImageDescription.value
- 270
- >>> Base(270).name
- 'ImageDescription'
-
-.. py:data:: GPS
-
- >>> from PIL.ExifTags import GPS
- >>> GPS.GPSDestLatitude.value
- 20
- >>> GPS(20).name
- 'GPSDestLatitude'
diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst
new file mode 100644
index 000000000..aae3e2b64
--- /dev/null
+++ b/docs/releasenotes/9.4.0.rst
@@ -0,0 +1,105 @@
+9.4.0
+-----
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+TODO
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Added start position for getmask and getmask2
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Text may render differently when starting at fractional coordinates, so
+:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now
+support a ``start`` argument. This tuple of horizontal and vertical offset
+will be used internally by :py:meth:`.ImageDraw.text` to more accurately place
+text at the ``xy`` coordinates.
+
+Added the ``exact`` encoding option for WebP
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``exact`` encoding option for WebP is now supported. The WebP encoder
+removes the hidden RGB values for better compression by default in libwebp 0.5
+or later. By setting this option to ``True``, the encoder will keep the hidden
+RGB values.
+
+Added IFD, Interop and LightSource ExifTags enums
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with
+:py:meth:`~PIL.Image.Exif.get_ifd`::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ print(im.getexif().get_ifd(ExifTags.IFD.Exif))
+
+``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should
+not be used in other contexts, as the enum value is only internally meaningful.
+
+:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop)
+ print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98
+
+:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource
+tag::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/iptc.jpg")
+ exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif)
+ print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown
+
+getxmp()
+^^^^^^^^
+
+`XMP data `_ can now be
+decoded for WEBP images through ``getxmp()``.
+
+Writing JPEG comments
+^^^^^^^^^^^^^^^^^^^^^
+
+When saving a JPEG image, a comment can now be written from
+:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving::
+
+ im.save(out, comment="Test comment")
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+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.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 8c436be3b..a2b588696 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 9.4.0
9.3.0
9.2.0
9.1.1
diff --git a/setup.cfg b/setup.cfg
index 44feb25ff..b562e2934 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -46,6 +46,7 @@ docs =
olefile
sphinx>=2.4
sphinx-copybutton
+ sphinx-inline-tabs
sphinx-issues>=3.0.1
sphinx-removed-in
sphinxext-opengraph
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 533997737..45987ec03 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -373,6 +373,9 @@ class BLP1Decoder(_BLPBaseDecoder):
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
+ if image.mode == "CMYK":
+ decoder_name, extents, offset, args = image.tile[0]
+ image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index eea6e3153..f78c8b17c 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -135,11 +135,19 @@ class DdsImageFile(ImageFile.ImageFile):
fourcc = header.read(4)
(bitcount,) = struct.unpack("> 24) & 0xFF)
- x, y = (int(c) for c in coord)
+ x, y = coord
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
self.draw.draw_bitmap(coord, mask, ink)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index f281b9e14..dbdc0cb38 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -137,6 +137,10 @@ class ImageFile(Image.Image):
if self.format is not None:
return Image.MIME.get(self.format.upper())
+ def __setstate__(self, state):
+ self.tile = []
+ super().__setstate__(state)
+
def verify(self):
"""Check file integrity"""
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 457e906c8..3b1a2a23a 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -26,6 +26,7 @@
#
import base64
+import math
import os
import sys
import warnings
@@ -588,6 +589,7 @@ class FreeTypeFont:
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
):
"""
Create a bitmap for the text.
@@ -647,6 +649,11 @@ class FreeTypeFont:
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
@@ -659,6 +666,7 @@ class FreeTypeFont:
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
+ start=start,
)[0]
def getmask2(
@@ -672,6 +680,7 @@ class FreeTypeFont:
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
*args,
**kwargs,
):
@@ -739,6 +748,11 @@ class FreeTypeFont:
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
@@ -750,12 +764,23 @@ class FreeTypeFont:
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
- size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
+ if start is None:
+ start = (0, 0)
+ size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2))
offset = offset[0] - stroke_width, offset[1] - stroke_width
Image._decompression_bomb_check(size)
im = fill("RGBA" if mode == "RGBA" else "L", size, 0)
self.font.render(
- text, im.id, mode, direction, features, language, stroke_width, ink
+ text,
+ im.id,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ ink,
+ start[0],
+ start[1],
)
return im, offset
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 76f42a307..9d5224588 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -125,7 +125,7 @@ class Viewer:
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
- os.system(self.get_command(path, **options))
+ os.system(self.get_command(path, **options)) # nosec
return 1
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index a6ed223bc..eb0db5bb3 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
+from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets
@@ -89,6 +90,7 @@ def APP(self, marker):
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
+ self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@@ -724,7 +726,7 @@ def _save(im, fp, filename):
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
i = 1
for marker in markers:
- size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
+ size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
extra += (
b"\xFF\xE2"
+ size
@@ -735,6 +737,8 @@ def _save(im, fp, filename):
)
i += 1
+ comment = info.get("comment", im.info.get("comment"))
+
# "progressive" is the official name, but older documentation
# says "progression"
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
@@ -757,6 +761,7 @@ def _save(im, fp, filename):
dpi[1],
subsampling,
qtables,
+ comment,
extra,
exif,
)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 5bfd8efc1..095cfe7ee 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -22,7 +22,14 @@ import itertools
import os
import struct
-from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
+from . import (
+ ExifTags,
+ Image,
+ ImageFile,
+ ImageSequence,
+ JpegImagePlugin,
+ TiffImagePlugin,
+)
from ._binary import i16be as i16
from ._binary import o32le
@@ -45,14 +52,22 @@ def _save_all(im, fp, filename):
_save(im, fp, filename)
return
+ mpf_offset = 28
offsets = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
# APP2 marker
- im.encoderinfo["extra"] = (
- b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
+ im_frame.encoderinfo["extra"] = (
+ b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
)
+ exif = im_frame.encoderinfo.get("exif")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ im_frame.encoderinfo["exif"] = exif
+ if exif:
+ mpf_offset += 4 + len(exif)
+
JpegImagePlugin._save(im_frame, fp, filename)
offsets.append(fp.tell())
else:
@@ -60,6 +75,7 @@ def _save_all(im, fp, filename):
offsets.append(fp.tell() - offsets[-1])
ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[0xB000] = b"0100"
ifd[0xB001] = len(offsets)
mpentries = b""
@@ -71,11 +87,11 @@ def _save_all(im, fp, filename):
mptype = 0x000000 # Undefined
mpentries += struct.pack(" max_len: # prevent buildup of half_token
raise ValueError(
- f"Token too long found in data: {half_token[:max_len + 1]}"
+ b"Token too long found in data: %s" % half_token[: max_len + 1]
)
for token in tokens:
if len(token) > max_len:
raise ValueError(
- f"Token too long found in data: {token[:max_len + 1]}"
+ b"Token too long found in data: %s" % token[: max_len + 1]
)
value = int(token)
if value > maxval:
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 9a2ec48fc..039f5ceea 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -13,8 +13,7 @@
# Notes:
#
-# * Implements the pixel access object following Access.
-# * Does not implement the line functions, as they don't appear to be used
+# * Implements the pixel access object following Access.c
# * Taking only the tuple form, which is used from python.
# * Fill.c uses the integer form, but it's still going to use the old
# Access.c implementation.
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 1dfd5275f..aa2a782c2 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -719,6 +719,8 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(1) # Basic type, except for the legacy API.
def write_byte(self, data):
+ if isinstance(data, int):
+ data = bytes((data,))
return data
@_register_loader(2, 1)
@@ -1151,39 +1153,6 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
- def get_child_images(self):
- if SUBIFD not in self.tag_v2:
- return []
- child_images = []
- exif = self.getexif()
- offset = None
- for im_offset in self.tag_v2[SUBIFD]:
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- current_offset = self._fp.tell()
- if offset is None:
- offset = current_offset
-
- fp = self._fp
- ifd = exif._get_ifd_dict(im_offset)
- jpegInterchangeFormat = ifd.get(513)
- if jpegInterchangeFormat is not None:
- fp.seek(jpegInterchangeFormat)
- jpeg_data = fp.read(ifd.get(514))
-
- fp = io.BytesIO(jpeg_data)
-
- with Image.open(fp) as im:
- if jpegInterchangeFormat is None:
- im._frame_pos = [im_offset]
- im._seek(0)
- im.load()
- child_images.append(im)
-
- if offset is not None:
- self._fp.seek(offset)
- return child_images
-
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 5eaeb10cc..81ed550d9 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -98,6 +98,15 @@ class WebPImageFile(ImageFile.ImageFile):
return None
return self.getexif()._get_merged_dict()
+ def getxmp(self):
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+
+ :returns: XMP tags in a dictionary.
+ """
+ return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
+
def seek(self, frame):
if not self._seek_check(frame):
return
@@ -318,6 +327,7 @@ def _save(im, fp, filename):
exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4)
+ exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
alpha = (
@@ -336,6 +346,7 @@ def _save(im, fp, filename):
im.mode,
icc_profile,
method,
+ exact,
exif,
xmp,
)
diff --git a/src/_imaging.c b/src/_imaging.c
index 0888188fb..05e1370f6 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1531,25 +1531,21 @@ if (PySequence_Check(op)) { \
PyErr_SetString(PyExc_TypeError, must_be_sequence);
return NULL;
}
+ int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0;
double value;
- if (scale == 1.0 && offset == 0.0) {
- /* Clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
- image->image8[y][x] = (UINT8)CLIP8(value);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ for (i = x = y = 0; i < n; i++) {
+ set_value_to_item(seq, i);
+ if (scale != 1.0 || offset != 0.0) {
+ value = value * scale + offset;
}
-
- } else {
- /* Scaled and clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
- image->image8[y][x] = CLIP8(value * scale + offset);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ if (endian == 0) {
+ image->image8[y][x] = (UINT8)CLIP8(value);
+ } else {
+ image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256);
+ image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8);
+ }
+ if (++x >= (int)image->xsize) {
+ x = 0, y++;
}
}
PyErr_Clear(); /* Avoid weird exceptions */
@@ -1829,7 +1825,7 @@ _resize(ImagingObject *self, PyObject *args) {
box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) {
imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]);
} else if (filter == IMAGING_TRANSFORM_NEAREST) {
- double a[6];
+ double a[8];
memset(a, 0, sizeof a);
a[0] = (double)(box[2] - box[0]) / xsize;
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 7cd6dfb1d..b52d6353e 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -777,13 +777,15 @@ font_render(FontObject *self, PyObject *args) {
const char *lang = NULL;
PyObject *features = Py_None;
PyObject *string;
+ float x_start = 0;
+ float y_start = 0;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
if (!PyArg_ParseTuple(
args,
- "On|zzOziL:render",
+ "On|zzOziLff:render",
&string,
&id,
&mode,
@@ -791,7 +793,9 @@ font_render(FontObject *self, PyObject *args) {
&features,
&lang,
&stroke_width,
- &foreground_ink_long)) {
+ &foreground_ink_long,
+ &x_start,
+ &y_start)) {
return NULL;
}
@@ -876,8 +880,8 @@ font_render(FontObject *self, PyObject *args) {
}
/* set pen position to text origin */
- x = (-x_min + stroke_width) << 6;
- y = (-y_max + (-stroke_width)) << 6;
+ x = (-x_min + stroke_width + x_start) * 64;
+ y = (-y_max + (-stroke_width) - y_start) * 64;
if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER;
@@ -956,7 +960,7 @@ font_render(FontObject *self, PyObject *args) {
/* we didn't ask for color, fall through to default */
#endif
default:
- PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
@@ -1023,7 +1027,7 @@ font_render(FontObject *self, PyObject *args) {
}
}
} else {
- PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
}
diff --git a/src/_webp.c b/src/_webp.c
index fd99116cb..493e0709c 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_encoder_dealloc(PyObject *self) {
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
- Py_RETURN_NONE;
}
PyObject *
@@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_decoder_dealloc(PyObject *self) {
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
- Py_RETURN_NONE;
}
PyObject *
@@ -576,6 +574,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
int lossless;
float quality_factor;
int method;
+ int exact;
uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
@@ -597,7 +596,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "y#iiifss#is#s#",
+ "y#iiifss#iis#s#",
(char **)&rgb,
&size,
&width,
@@ -608,6 +607,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
&icc_bytes,
&icc_size,
&method,
+ &exact,
&exif_bytes,
&exif_size,
&xmp_bytes,
@@ -633,6 +633,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
+#if WEBP_ENCODER_ABI_VERSION >= 0x0209
+ // the "exact" flag is only available in libwebp 0.5.0 and later
+ config.exact = exact;
+#endif
// Validate the config
if (!WebPValidateConfig(&config)) {
diff --git a/src/encode.c b/src/encode.c
index aa47fe671..21c42d915 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
+ char *comment = NULL;
+ Py_ssize_t comment_size;
char *extra = NULL;
Py_ssize_t extra_size;
char *rawExif = NULL;
@@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnOy#y#",
+ "ss|nnnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
@@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&ydpi,
&subsampling,
&qtables,
+ &comment,
+ &comment_size,
&extra,
&extra_size,
&rawExif,
@@ -1090,13 +1094,28 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}
- // Freed in JpegEncode, Case 5
+ // Freed in JpegEncode, Case 6
qarrays = get_qtables_arrays(qtables, &qtablesLen);
+ if (comment && comment_size > 0) {
+ /* malloc check ok, length is from python parsearg */
+ char *p = malloc(comment_size); // Freed in JpegEncode, Case 6
+ if (!p) {
+ return ImagingError_MemoryError();
+ }
+ memcpy(p, comment, comment_size);
+ comment = p;
+ } else {
+ comment = NULL;
+ }
+
if (extra && extra_size > 0) {
/* malloc check ok, length is from python parsearg */
- char *p = malloc(extra_size); // Freed in JpegEncode, Case 5
+ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6
if (!p) {
+ if (comment) {
+ free(comment);
+ }
return ImagingError_MemoryError();
}
memcpy(p, extra, extra_size);
@@ -1107,8 +1126,11 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (rawExif && rawExifLen > 0) {
/* malloc check ok, length is from python parsearg */
- char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5
+ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6
if (!pp) {
+ if (comment) {
+ free(comment);
+ }
if (extra) {
free(extra);
}
@@ -1134,6 +1156,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size;
((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif;
diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c
index 514fb2929..83860c38a 100644
--- a/src/libImaging/Access.c
+++ b/src/libImaging/Access.c
@@ -43,23 +43,6 @@ add_item(const char *mode) {
return &access_table[i];
}
-/* fetch pointer to pixel line */
-
-static void *
-line_8(Imaging im, int x, int y) {
- return &im->image8[y][x];
-}
-
-static void *
-line_16(Imaging im, int x, int y) {
- return &im->image8[y][x + x];
-}
-
-static void *
-line_32(Imaging im, int x, int y) {
- return &im->image32[y][x];
-}
-
/* fetch individual pixel */
static void
@@ -187,36 +170,35 @@ put_pixel_32(Imaging im, int x, int y, const void *color) {
void
ImagingAccessInit() {
-#define ADD(mode_, line_, get_pixel_, put_pixel_) \
+#define ADD(mode_, get_pixel_, put_pixel_) \
{ \
ImagingAccess access = add_item(mode_); \
- access->line = line_; \
access->get_pixel = get_pixel_; \
access->put_pixel = put_pixel_; \
}
/* populate access table */
- ADD("1", line_8, get_pixel_8, put_pixel_8);
- ADD("L", line_8, get_pixel_8, put_pixel_8);
- ADD("LA", line_32, get_pixel, put_pixel);
- ADD("La", line_32, get_pixel, put_pixel);
- ADD("I", line_32, get_pixel_32, put_pixel_32);
- ADD("I;16", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B);
- ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L);
- ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B);
- ADD("F", line_32, get_pixel_32, put_pixel_32);
- ADD("P", line_8, get_pixel_8, put_pixel_8);
- ADD("PA", line_32, get_pixel, put_pixel);
- ADD("RGB", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBA", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBa", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBX", line_32, get_pixel_32, put_pixel_32);
- ADD("CMYK", line_32, get_pixel_32, put_pixel_32);
- ADD("YCbCr", line_32, get_pixel_32, put_pixel_32);
- ADD("LAB", line_32, get_pixel_32, put_pixel_32);
- ADD("HSV", line_32, get_pixel_32, put_pixel_32);
+ ADD("1", get_pixel_8, put_pixel_8);
+ ADD("L", get_pixel_8, put_pixel_8);
+ ADD("LA", get_pixel, put_pixel);
+ ADD("La", get_pixel, put_pixel);
+ ADD("I", get_pixel_32, put_pixel_32);
+ ADD("I;16", get_pixel_16L, put_pixel_16L);
+ ADD("I;16L", get_pixel_16L, put_pixel_16L);
+ ADD("I;16B", get_pixel_16B, put_pixel_16B);
+ ADD("I;32L", get_pixel_32L, put_pixel_32L);
+ ADD("I;32B", get_pixel_32B, put_pixel_32B);
+ ADD("F", get_pixel_32, put_pixel_32);
+ ADD("P", get_pixel_8, put_pixel_8);
+ ADD("PA", get_pixel, put_pixel);
+ ADD("RGB", get_pixel_32, put_pixel_32);
+ ADD("RGBA", get_pixel_32, put_pixel_32);
+ ADD("RGBa", get_pixel_32, put_pixel_32);
+ ADD("RGBX", get_pixel_32, put_pixel_32);
+ ADD("CMYK", get_pixel_32, put_pixel_32);
+ ADD("YCbCr", get_pixel_32, put_pixel_32);
+ ADD("LAB", get_pixel_32, put_pixel_32);
+ ADD("HSV", get_pixel_32, put_pixel_32);
}
ImagingAccess
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 2b45d0cc4..b03bd02af 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -479,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) {
}
}
+static void
+rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) {
+ int x;
+ unsigned int alpha;
+ for (x = 0; x < xsize; x++, in += 4) {
+ alpha = in[3];
+ if (alpha == 255 || alpha == 0) {
+ *out++ = in[0];
+ *out++ = in[1];
+ *out++ = in[2];
+ } else {
+ *out++ = CLIP8((255 * in[0]) / alpha);
+ *out++ = CLIP8((255 * in[1]) / alpha);
+ *out++ = CLIP8((255 * in[2]) / alpha);
+ }
+ *out++ = 255;
+ }
+}
+
/*
* Conversion of RGB + single transparent color to RGBA,
* where any pixel that matches the color will have the
@@ -934,6 +953,7 @@ static struct {
{"RGBA", "HSV", rgb2hsv},
{"RGBa", "RGBA", rgba2rgbA},
+ {"RGBa", "RGB", rgba2rgb_},
{"RGBX", "1", rgb2bit},
{"RGBX", "L", rgb2l},
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index b65f8eadd..d9ded1852 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -124,7 +124,6 @@ struct ImagingMemoryInstance {
struct ImagingAccessInstance {
const char *mode;
- void *(*line)(Imaging im, int x, int y);
void (*get_pixel)(Imaging im, int x, int y, void *pixel);
void (*put_pixel)(Imaging im, int x, int y, const void *pixel);
};
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index a876d3bb6..1d7550818 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -92,6 +92,10 @@ typedef struct {
/* in factors of DCTSIZE2 */
int qtablesLen;
+ /* Comment */
+ char *comment;
+ size_t comment_size;
+
/* Extra data (to be injected after header) */
char *extra;
int extra_size;
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index a44debcaf..2a24eff39 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
case 4:
+
+ if (context->comment) {
+ jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size);
+ }
+ state->state++;
+
+ case 5:
if (1024 > context->destination.pub.free_in_buffer) {
break;
}
@@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
state->state++;
/* fall through */
- case 5:
+ case 6:
/* Finish compression */
if (context->destination.pub.free_in_buffer < 100) {
@@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
jpeg_finish_compress(&context->cinfo);
/* Clean up */
+ if (context->comment) {
+ free(context->comment);
+ context->comment = NULL;
+ }
if (context->extra) {
free(context->extra);
context->extra = NULL;
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index dfa6d842d..783852c24 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1717,7 +1717,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
withAlpha = !strcmp(im->mode, "RGBA");
int transparency = 0;
- unsigned char r, g, b;
+ unsigned char r = 0, g = 0, b = 0;
for (i = y = 0; y < im->ysize; y++) {
for (x = 0; x < im->xsize; x++, i++) {
p[i].v = im->image32[y][x];
diff --git a/tox.ini b/tox.ini
index 21b5d4b50..9a41ca96b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,15 +1,13 @@
-# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it,
-# "python3 -m pip install tox" and then run "tox" from this directory.
-
[tox]
envlist =
lint
- py{37,38,39,310,311,py3}
+ py{py3, 311, 310, 39, 38, 37}
minversion = 1.9
[testenv]
+deps =
+ cffi
+ numpy
extras =
tests
commands =
@@ -17,16 +15,15 @@ commands =
{envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
-deps =
- cffi
- numpy
+allowlist_externals = make
[testenv:lint]
+passenv =
+ PRE_COMMIT_COLOR
+skip_install = true
+deps =
+ check-manifest
+ pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
check-manifest
-deps =
- pre-commit
- check-manifest
-skip_install = true
-passenv = PRE_COMMIT_COLOR
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index b4b15cc1e..6ded944da 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -152,9 +152,9 @@ deps = {
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download",
- "filename": "xz-5.2.7.tar.gz",
- "dir": "xz-5.2.7",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download",
+ "filename": "xz-5.4.0.tar.gz",
+ "dir": "xz-5.4.0",
"license": "COPYING",
"patch": {
r"src\liblzma\api\lzma.h": {
@@ -200,15 +200,11 @@ deps = {
"libs": [r"output\release-static\{architecture}\lib\*.lib"],
},
"libtiff": {
- "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz",
- "filename": "tiff-4.4.0.tar.gz",
- "dir": "tiff-4.4.0",
- "license": "COPYRIGHT",
+ "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz",
+ "filename": "tiff-4.5.0.tar.gz",
+ "dir": "tiff-4.5.0",
+ "license": "LICENSE.md",
"patch": {
- r"cmake\LZMACodec.cmake": {
- # fix typo
- "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}",
- },
r"libtiff\tif_lzma.c": {
# link against liblzma.lib
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
@@ -228,9 +224,9 @@ deps = {
# "bins": [r"libtiff\*.dll"],
},
"libpng": {
- "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download",
- "filename": "lpng1638.zip",
- "dir": "lpng1638",
+ "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
+ "filename": "lpng1639.zip",
+ "dir": "lpng1639",
"license": "LICENSE",
"build": [
# lint: do not inline
@@ -293,9 +289,9 @@ deps = {
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
},
"lcms2": {
- "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download",
- "filename": "lcms2-2.13.1.tar.gz",
- "dir": "lcms2-2.13.1",
+ "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.14.tar.gz/download",
+ "filename": "lcms2-2.14.tar.gz",
+ "dir": "lcms2-2.14",
"license": "COPYING",
"patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
@@ -323,6 +319,11 @@ deps = {
"filename": "openjpeg-2.5.0.tar.gz",
"dir": "openjpeg-2.5.0",
"license": "LICENSE",
+ "patch": {
+ r"src\lib\openjp2\ht_dec.c": {
+ "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
+ }
+ },
"build": [
cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")),
cmd_nmake(target="clean"),
@@ -355,11 +356,12 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip",
- "filename": "harfbuzz-5.3.1.zip",
- "dir": "harfbuzz-5.3.1",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip",
+ "filename": "harfbuzz-6.0.0.zip",
+ "dir": "harfbuzz-6.0.0",
"license": "COPYING",
"build": [
+ cmd_set("CXXFLAGS", "-d2FH4-"),
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),
cmd_nmake(target="harfbuzz"),
@@ -469,11 +471,22 @@ def extract_dep(url, filename):
raise RuntimeError(ex)
print("Extracting " + filename)
+ sources_dir_abs = os.path.abspath(sources_dir)
if filename.endswith(".zip"):
with zipfile.ZipFile(file) as zf:
+ for member in zf.namelist():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ raise RuntimeError("Attempted Path Traversal in Zip File")
zf.extractall(sources_dir)
elif filename.endswith(".tar.gz") or filename.endswith(".tgz"):
with tarfile.open(file, "r:gz") as tgz:
+ for member in tgz.getnames():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ raise RuntimeError("Attempted Path Traversal in Tar File")
tgz.extractall(sources_dir)
else:
raise RuntimeError("Unknown archive type: " + filename)