From 76b99756e425099b650bd383e204c259b5bdd3f1 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 29 Oct 2022 18:24:44 +0100 Subject: [PATCH 001/137] disable __CxxFrameHandler4 when compiling harfbuzz --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..bc19c5fa1 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -360,6 +360,7 @@ deps = { "dir": "harfbuzz-5.3.1", "license": "COPYING", "build": [ + cmd_set("CXXFLAGS", "-d2FH4-"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), cmd_nmake(target="harfbuzz"), From e50a3a213ed5f5296deeaefe9ec0699010545157 Mon Sep 17 00:00:00 2001 From: TrellixVulnTeam Date: Sun, 30 Oct 2022 23:44:48 +0000 Subject: [PATCH 002/137] Adding tarfile member sanitization to extractall() --- winbuild/build_prepare.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..0e585f796 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,26 @@ def extract_dep(url, filename): zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - tgz.extractall(sources_dir) + def is_within_directory(directory, target): + + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + + return prefix == abs_directory + + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): + + for member in tar.getmembers(): + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted Path Traversal in Tar File") + + tar.extractall(path, members, numeric_owner=numeric_owner) + + + safe_extract(tgz, sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) From 74c60b47a89502adec658316b06cd0043046d36b Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 23:45:49 +0000 Subject: [PATCH 003/137] simplify patch, also check zipfile --- winbuild/build_prepare.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0e585f796..3cd841484 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -469,31 +469,23 @@ 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.commonprefix([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: - def is_within_directory(directory, target): - - abs_directory = os.path.abspath(directory) - abs_target = os.path.abspath(target) - - prefix = os.path.commonprefix([abs_directory, abs_target]) - - return prefix == abs_directory - - def safe_extract(tar, path=".", members=None, *, numeric_owner=False): - - for member in tar.getmembers(): - member_path = os.path.join(path, member.name) - if not is_within_directory(path, member_path): - raise Exception("Attempted Path Traversal in Tar File") - - tar.extractall(path, members, numeric_owner=numeric_owner) - - - safe_extract(tgz, sources_dir) + for member in tgz.getmembers(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + member_prefix = os.path.commonprefix([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) From 7528b673fa4517604f4af91a6d061802843f3246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Oct 2022 19:36:14 +1100 Subject: [PATCH 004/137] Removed Fedora 35 --- .github/workflows/test-docker.yml | 3 +-- docs/installation.rst | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c68d43935..1e36b3382 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,7 +30,6 @@ jobs: centos-stream-9-amd64, debian-10-buster-x86, debian-11-bullseye-x86, - fedora-35-amd64, fedora-36-amd64, gentoo, ubuntu-18.04-bionic-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index eb69d5805..4812b27cf 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -440,8 +440,6 @@ 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 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | From 5b4703d6153689bf008ffe37f43c489c4f9211a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Nov 2022 08:39:02 +1100 Subject: [PATCH 005/137] Added conversion from RGBa to RGB --- Tests/test_image_convert.py | 7 +++++++ src/libImaging/Convert.c | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) 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/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}, From 6fd772e6694c53aeb1c9d5ff48d931a4db63fb2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 00:08:29 +1100 Subject: [PATCH 006/137] Updated lcms2 to 2.14 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4812b27cf..c65095640 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -147,7 +147,7 @@ Many of Pillow's features require external libraries: * **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. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1e..455481e3d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -293,9 +293,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": { From 6b286ed62f6cb2be447a5bb3a7f09f5edad5f3d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 07:47:05 +1100 Subject: [PATCH 007/137] XCB will not be used on Linux if gnome-screenshot is present --- Tests/test_imagegrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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") From c10c6bf8940c2a1bbbc1d01575489cb85e612a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 3 Nov 2022 20:23:59 +0100 Subject: [PATCH 008/137] use os.path.commonpath instead of os.path.commonprefix Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3cd841484..14f8d7ba0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,7 @@ def extract_dep(url, filename): 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.commonprefix([sources_dir_abs, member_abspath]) + 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) @@ -482,7 +482,7 @@ def extract_dep(url, filename): with tarfile.open(file, "r:gz") as tgz: for member in tgz.getmembers(): member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) - member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + 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) From d93b9919e338f8fe253e76fe0bdc7f0994267385 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 21:40:08 +0100 Subject: [PATCH 009/137] Use verbose flag for pip install * Ensures when developing that compilation warnings are visible * Provides feedback that compilation has occured. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8f2862948..7783bd96b 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 From 41987cffade673b46b96054f8462f3c45d410de0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 22:57:39 +0100 Subject: [PATCH 010/137] Fix compiler error: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/_imaging.c:1842:17: warning: ‘ImagingTransform’ accessing 64 bytes in a region of size 48 [-Wstringop-overflow=] 1842 | imOut = ImagingTransform( | ^~~~~~~~~~~~~~~~~ 1843 | imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/_imaging.c:1842:17: note: referencing argument 8 of type ‘double *’ --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 0888188fb..940b5fbb3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1829,7 +1829,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; From f9a2f991db4fb91a78ba1bb30bb45b9d2b235348 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 11:48:18 +1100 Subject: [PATCH 011/137] Replaced IOError with OSError --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7cd6dfb1d..bd4099176 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -956,7 +956,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 +1023,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; } } From 8947cbf4d113b112660b15db23fe84f94e128788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Fri, 4 Nov 2022 07:31:00 +0100 Subject: [PATCH 012/137] simplify code Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 14f8d7ba0..872e74a20 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -480,8 +480,8 @@ def extract_dep(url, filename): 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.getmembers(): - member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + 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") From bbe9cc6ae5f19207b0c3bf80f591016ec03019b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 20:26:39 +1100 Subject: [PATCH 013/137] Use verbose flag for pip install for debug target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7783bd96b..a2545b54e 100644 --- a/Makefile +++ b/Makefile @@ -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: From ef1eb2f3d6e7e19c251beb6eda5e9ac06e902ca8 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 4 Nov 2022 11:16:22 +0100 Subject: [PATCH 014/137] Add oss-fuzz badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e7c0ebc5a..7a81e0c40 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ As of 2019, Pillow development is Tidelift Align + Fuzzing Status From 13a4feafb75b1c0cdf4821dd6db88f0c44d9ce4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 16:38:50 +1100 Subject: [PATCH 015/137] Patch OpenJPEG to include uclouvain/openjpeg#1423 --- winbuild/build_prepare.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5277b84f8..9f1e74e53 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -323,6 +323,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"), From b8be24850bc07c04a21635dbd8835c9e8408cf93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 21:54:23 +1100 Subject: [PATCH 016/137] Added file to questionable list --- Tests/test_bmp_reference.py | 1 + 1 file changed, 1 insertion(+) 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", From 4001a9fab471dbeaaa6f530d98208e25ef6bf912 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Nov 2022 22:41:06 +1100 Subject: [PATCH 017/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6f2ba569e..fc8d8362a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,15 @@ Changelog (Pillow) ================== +9.4.0 (unreleased) +------------------ + +- 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) ------------------ From 9448532f913665006b0373ae3cee61c550b34339 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:03:51 +0000 Subject: [PATCH 018/137] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/sphinx-contrib/sphinx-lint: v0.6.1 → v0.6.7](https://github.com/sphinx-contrib/sphinx-lint/compare/v0.6.1...v0.6.7) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f81bcb956..2c13fb3b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: ["--target-version", "py37"] @@ -44,7 +44,7 @@ repos: - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.6.1 + rev: v0.6.7 hooks: - id: sphinx-lint From e31ca06b7cde667aa973cfcb35166d17a5408925 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 10:58:33 +1100 Subject: [PATCH 019/137] Updated AppVeyor to Python 3.11 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5471dc2b265d2486a9fe0f37d2d7cbfe6f4f4cd6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 11:49:39 +1100 Subject: [PATCH 020/137] Use fractional coordinates when drawing text --- .../images/test_anchor_multiline_mm_right.png | Bin 9514 -> 9525 bytes .../test_combine_multiline_lm_center.png | Bin 4160 -> 4147 bytes .../images/test_combine_multiline_lm_left.png | Bin 4223 -> 4197 bytes .../test_combine_multiline_lm_right.png | Bin 4170 -> 4154 bytes .../test_combine_multiline_mm_center.png | Bin 4243 -> 4204 bytes .../images/test_combine_multiline_mm_left.png | Bin 4216 -> 4189 bytes .../test_combine_multiline_mm_right.png | Bin 4247 -> 4215 bytes .../test_combine_multiline_rm_center.png | Bin 4160 -> 4149 bytes .../images/test_combine_multiline_rm_left.png | Bin 4149 -> 4144 bytes .../test_combine_multiline_rm_right.png | Bin 4215 -> 4186 bytes Tests/images/text_float_coord.png | Bin 2877 -> 2875 bytes Tests/images/text_float_coord_1_alt.png | Bin 807 -> 809 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 10 +++++++-- src/PIL/ImageFont.py | 19 ++++++++++++++-- src/_imagingft.c | 12 ++++++---- 16 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png index cf002b12cd0a89eec1f97cd1c6530b7d1007fc9e..7e98b8eac8d780876d377315b2158f203c30757c 100644 GIT binary patch literal 9525 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 TfD8N|D1`D|b%jrICcgg%hStVx diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png index 7b1e9c4e42f69874b4c508d16bc85527d5d902b7..6a15130248ad8bed5e13a937d3667eb98624b5bf 100644 GIT binary patch literal 4147 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 literal 4160 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 CJA=Fc diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png index a26996c2dbe70748f834607fc49ae8dd079c4e09..8eb254fdf26323e778c1ff5e73dab8c8a0b4b638 100644 GIT binary patch literal 4197 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 diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png index 7caf5cb742a27f001106cc3ade926f2f177e71fb..cb640a7409fccce7b08ec23bdf728f11a78a1533 100644 GIT binary patch literal 4154 zcmeI0X*8SL9>(>wJ=Lmet3?MKT1u6e=iw+hisH0X5TuTxYDgU=h)8EeOG`~PwG@Y# zm2eVC(VB-=DrTxk8j_%r7((3Ld+)mIe!HLUw|;on&f0JCzI*@wzvp@OO5E@ER-*fk z?-LRd61Bcyd09wk$J6hZ@K0byY~odxkkG*$)|Tc@5!tK>|3IfO^4sN1iPz`kI~0|~ zgr(FmJDy!#ItwjGZuq&N?RuQzE`>vOmg>`gzwQu!ud4B};r&tQjEKdr%Ir%K9Qk~u ztaxU};j-hFK|2^6QU<+GT3PyKPW4vr%KCjt`o(^O6|y8yK{Fr@q5y z{o9;^fQuR;be`8GDJaMFqMcoDreAKOPj{N7 z{Kr=ov3q2rm#A>0suYUHFjGt7EB2G^w*^oTCZ*4<=(NYXH!cnDop9wp^Em|85M)pXL2~a zF(hh%s+!cQW|LT@^jO}o#RlFK#hpt>i6+NMg6>NefD;|cfHO!@7y(IO1t%c7h&Qo;AHAYSn+sroF z%>)`ZKfeW)XoOXIN*jeUKfJQ=_h*nWe6XYN#mOe`_C)T&(BfzvZaIF)6V1}-V`(Gaojo#ACQ%$ju0L z@%#N!-##M#uRJ#bYSFX$kN1CvE%2Blp%+!c7pUb#V)WY5bQ5|tc$1Qk9e3+EB=KNb zHuQB> zZW2#au9=8~Rw*?cRdHsHH`dl!J=25M8&wNSJP7;z_6{gn{lBM{wSSd6eVDw(8#|R` z{O7mMGqSxaRYgUL?QPUbgjoQ!Ohm2WC4wB|=%X16sy>~Yl_eGixxG4=Z}&?>3s_o1hv)G z7DV`n4;&wC!kiy+54H2Hw$CvvN03cdt00ZD$vxjdM<&5VM$xm7-)fAk6kQA=Zz(;( zN^d0)7_hZ3?-S76`7mu9iZ#l;CuVw`_y7^JuE5Xu%B=0LsH{ZZ=z$QI&hvC7&$%pZ z6ILp{$K;yN9-Mk%VZQnFV&6L5j6wm<;a(r;8WjZqfO&*nnVBoL%c53$w=?0UTO3Nr zOxqxp>KlB_IASS7!+Qhp_~Anp*ueV)Nk42zw5#h#vV!ezPmYImq?v6lwTeK9>MO?B z*=JTo;4u+GfNCWOWScvLpwy-)+I0!a=3`}JGPL~cZNur;*Qwd#ZUz}gaAn0*AY{#c zd{Ch~3ng@>nN(k_+Hoy1t;NYOnFIbTS>j>IDwp9DZ+OW8)dKK6i1{xy&V`TNQsX z&(x&$t%aD0cHnT8Z(_pZ;7EyP)W|w|4;Ri16hB zTkj+uJE<4!&a2$Mp0z8X-Un1$b-YEe#Sh)&Y%EkFqiLVw&g6oCfGDPPN=)iJP;_~t z3dD~V!GE~#`cW^EdXl|K2YntdShw9659UaFXG@OZ?# zIV51TN8z`Hg@u(rP3dp^DWq)ypTk*hmo@Y2%kJ@Ta(4ENoJ(aeoX&=Aafj&oTVx2Hgrv*@`>8`Y##}AliR!2ldObxKga}9#nkTRU+SA%& zePHwbrhI0V$k5Ob1YXbVGg@SVAO`h6fjrBf*G~70VtDCkg61*_Xuy8faky4ET`Izw zHhZ}9NE-se1uirUa_pKf;TA(PW^?Vkg;1#i4#feSzA#J8d-{zQiA>RG@MC%|I+%vs z8n8Fvegf*`Fet?U6Tlrm*91*1hvTET^F3J&p!fY58mG{y%QNi|m#SY(CcuPF0989I ztbDJyB5`9L$6$!t>wC?0rSANBzdkd$cQ~hD?r>*gUATtwWh~|f@OctAKp5o4f=G** z2`Y1~_3d~n*UpJHofTeqx_B^p=84{)``6PEF`a^)IUrVxNY(j~git5q>CkV-B(Sk^ z77v|ZFxdbMpaoX$223FAOWYZmkm=;)y+Y<(2cadF&y4=e2hF93)qzT`q z*f#B4CrRDEp2j+$N>`P85Ym9x<*zPcf#yL6VHNJ{AmP)VKL?xl1CsS_d)&yS!b4Bi ze#Z<3Bd<=P5!fwmnkt;fp2)H(Gz7eXLl%hr1JZ_X^7Fs6h(LM-!#ENtrYA!qlVa&$ zF!y{A2b1kZiA+Y!Jjo2KLr%po1`1DEB^rauOy;-~6?&|zl=i3J@YjWO7Dd`#FdsJx z_050POr~uX_}|@qU(V%MsUO3S68w{bWpYM}{x$0Fh8*ypfRHuR-m>ES)w}-yZ<(H; literal 4170 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 T-aH8YY>?KX<8lAr0RHTE diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png index aadb5191f0e7df6ae1838ca6b04745268de17728..f539a8e62e66d1769e1bd692594d5938db7d1d46 100644 GIT binary patch literal 4189 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 AyZ`_I literal 4216 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 Ha8LLb^xtTU literal 4247 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 sq)Epb{z}gH3sL0nTmDag9FofH^0IWV!Z diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png index 7568dd63a33181ae5ec5c8c99ac72c18859109b6..4cce8f6a00ead59f2f061bde6a6b374e9fecbfe3 100644 GIT binary patch literal 4149 zcmeHLX*85?8<(hrrd3){sZ@w)tl?kURFtx&v4m8UWEoSo5t`nTEX8Dc%J*5=RD`R?(4dK%XL3bF4>$>T)Ta( zl$4aB)!CDmrKDCQEMIG+!4n80J00AGRws|!-Ore0Jh-zNA*r8XDCKT8{$;oMczq;G zZN1#**r@muL))HhkKXr6`Q^!@$A82$smo#ysP5greT($d1IiNJkI_*+n^m_=|3$Eq znURUMk9L$lS52|t@#)NS*_XPrMIE$>l2q?to++JSSkcb9Q%feB%RVyj{QT_e*RP|h zvPIUmfe@(ycR8i?X!jK!YyK>MO7IU9oCbIy6Pmtnf7@Z+=$+}zyK;U1USHYv$p<{PE|{#<-a^Yp_n`hQ8Y zJ^l-24)AtTD1Tn!T<&ER2wA<>sx(qpMt+Uu|9kn#<#MTJ=&l2qHljwXQ=61r>0{ZL z-A*$^iCA#z)G1a_ZB5O>2*%B3Xm@|u9JR`qp4ZOG$;m-7!wsK2j`~v1{$Iri?8*(J&&<; z#&|mItygt8YZfx^1j>_dlX)4}!C}9^ zyHoVsjMwemlA?0GXxBDgX_Eg*o=#VZ8>)F9#*1H3zBJDYWWB|>-XDC_h*q5FDx$BT zU4p?#A>46C*9Rk?wz>&N)BOUfA*#m~7RRt^M{j=5hP0|G$*nK++KH>4h_?_M-TIje zf*T6+H(fkd=0=HVQbUEstD5yz`!S1Qbwfi#u)5&Rt}ae@nJ2x9ay@NQqZcS~213u=j#Zy{b723fqr<2ALlC%FCnOr=(>xxAhlit=KqFxoVd}gY<{>TK{_~RL{=K%L^CqSV1Ae zg&C?@iu6tIbjw{)h`mH!FT5^z@;1kI^94=%PU#JW>K@HW`>19yCvN}!N^;H8(h}&s z0sp|-yky^XWosOUx(b~uz}H4qPc>AQQ+F#hoa@4(#KuTY-+vxug(M}M17P&4xIr}m z$w5Z*K}Q*1z8d)ThtXijOl1E9K(fOJ4;qiZdjlE`4fyqi?nen{)FK3Rv=5Ku=hTL> zAFG-SysP%5r%0%{t!`od9BdlV*!Ak(diql1KRCqzegN8?Z*+V zhA+;J08|^jszZ)0H?!Bpp==$0}_^%MKp`fbPB@s1@T?Xb_=e6ArG2trTzDtvCP*WiaoXH*b&!8{VbzYZwn zoEitHLo3xL-C**WeydT&jAeif{!^+2D&j8W=C@Zz^05&i!!2X0Ct7qU*C-L;jo#>W zDj@)bBRBFdV#ywjX!iWaC!3am!=5koXgs8;3GtauIM8t0e^zpN>ZxY2(ZH-FTA|q@ zDcN)n8ROzddQT)CGZl?tQLf?K&QKmZ)fCCmEFmV5%0RE>w;sMjubRw;sBGNtq}>8C zHy2Jf_OWsKSt0dxx$P{W`n*fkou0AlK;$o)aqSNK!n+CygXkT12_q?e(tIXL0yG!m z-*@li8iy935-CFUA)t)s)neMr1zJcu#1 zY!KY7KAG0p8K-7p&c{`R@;L-l?MRBtYd7mu|$MI*2oY>&jb>x2UgJhnimrKLT6`qV6x z=jGGV(&AUk%CkF!=<{zH!KQLSfwbJn#2I@iBJg#jjHdshqxr&6tj@X`A%X_O)P1}O z2zR`7EW-pb4givs2`pzgN>)AqVry&b-{Xz(&eu=<79+Q5yD|4@=REbH^v1%EipsjX z3`xP0z1HalP_nRD1Dhd<`)Y4*Z^#I`vj2tgz)Iln?VH9{sj^qkLR1Ddvv_B#XA)B zj(tEN5P)Fh$tpTf27Le`H2y2agnDW#tg_ubGmHa)c`hQinFh1vb%+U>oPLr3t+<1S zL=?>oH|bohJYdQo1dhFj(|D6~B=)>&$Ea|^?Qr6SK`8Qw3yxw@1}vQe+*IA-5zRHU zXwbyqPPZla8*sHM9 zs1$$|$33X2=~8_c=I!lGD~3U8?%CS@AdF?DQ_^{05b;87c?FFSF7!2U!%aqCdCryu z)n5PfR4D&q)U=ZNuyIV?Lr*MD)P?gojAd%u+jsW9hob;Z!QAEwCK|B&kN?_W*ni(s zU7!W326|iqbPsfs2xkKI z#4}T!+w^bL7#))xQ~q;3Efvp@E^c3<~>U)S&Y{=VPu@4D{Cm#odV zZBgAKCMLG+!uhjyVq)UYH-4KXK#v0REkR6dyUc~NXBa@Zl`r8-^CrQ^X}R$p?*YS-ooLZ!xJy?Hj1{p+@HcP?T{MPuu#lR_f}L_ z+K%56^2=9od{p|GF6BSbINZ#_z)*|r!I{p;X*Q41HrqCXEgvu_YX~$zqEKo0-w8)O zEj&Fv?PKvTvvP7}#4tHha+^(y@BZUw^Amy}S@0|1%TSlQ_v0^985+Y;4mtrJ*g_i} zN0(0}`|c(xKhV0;d_Fr~7z7=Er_E@@s}k3M4} zW1a%;8dOrVc8Buzhi=kkclg}&bkL1-ajABz!)vOKtS=Awb>}%6r|9YEtgej4#)oV+ zJc(Co`&xIe>FJ($i_D`*ZIhqh!35tM>e0ByKa}7uw;NNT;dZ%QJO&Ckp5&z*yoS+y z-C%Jp&I^;5pWk6P`gD#eI+b8SLNmkq-`}Vh^tZJA{6aWjk3xy+<@5&w=~g5XDc2zn zClXq|JUrF;w&5ze-RSVnubUyMddRA_G#C|04$gJBxjf6PV1=d!qmBUvsDOv%a>B&1CE>&^rwa52iav}Yz=T5Q3N))alvx!7TVJG8 zATjFD)g-S}BOyRE5QG1F#Cb;0=XA4pEi0$0z^NT*ua)n^3a3`SKHqP;<)}{G0okeM zc4rcchLf}O%>XABIHC#3Ufzu8mdG z)lheqnb;>P`ofVX`aMLHh*L2n6fAV$1oKTAIDsu34tH|uFTXVaFyPHH$$mo=Ng4Tl zCw#9U5Qqb#n7`K?iQJ%y=eEz*Xn3&D^yE^nyNnudoJLrno($^B zv8^Pl)|j|DY1ZcJ>_BDNJzMSnCMoR$0s#|PXVbS8nB45j(TH5RO3g=wjwMt!T3A{# z3Y{6>i}kA}(+9bLtRVAj9Co5re{YQmNOyJK&iOP`(by?Wv9hjHSEgm6DWV2EHdnLE zqByUNC8D^okK2ve1yxm5lF#`3iL{QP@#@e8Ar32~e^A?bc;|?7z|N9GDXIK9oLBZl zM;g_)uXJYduDGNgRlto%Of$TF4dBl8<4?YeJ@<6Ct-XCbInyTXl#-GXlZ+)2i9@BH zt?6b@xYTS)Mux}yNaH;j1(r|i9A$0x(T-pJVPod%p&lb2AFs)~EsWE~#>S|^RiEIc zRW`8$1}EL1XC)o;I)TPoSVs8BKVW>U^UdX^_7c!lhn9U~_Z%C))KgScG|Q?9M;8J& zxa{c2Azjpp59N;~4v>5joeQvYEFkhVG^fYekEUyHXUFY^qd4TY2P&?woU=fn)b*u- zsK0`9$fKEN5(53J0f8ORTjz zFBuoW!_c^SQ-7)azN91W=Vmv1e|)sV^x@VUA$);b1dtvu9TqkzN>YXeNLbkiluSgF zX_-LLgC9~%u-CI?wy8QG5TTm)qR+}+F0*%ZOdwE3{(#6=2|!A@9Uf!tkLJeOP38)6 zaz0s$03R4G0W<(CKuWkY;OFJ%OV6Us^p!c2ZU(KGM&Fmcyif`juLQaL?Ya%{oO?xa z*4YakokSEhlKKRYfR=n5l31PqfwS%`(6EEI&CyZO>R~y+a-)sVhB{LDp$KYZgmBON znR}Zu73zSMr#O2y#i+^jhaeDrqp`45LIf~(f4@!UPj|l%4dFUZf;qY1hWfB_@EnU- zTLVW$@F;NW6imJBvFmN#D`8>Y@?cy(C@HZRIi3vJWnn5iO;1;sMjacD)@aAs%hGy| zSV6^v*NKn}9S9`WDC;)tow($te^%FmG`_@%N9ZIZ1G^1kAQ?I4QCj=2&@2baZ!LIf zUwnlbd91CBS=)QU_Y?IyG#?a!X6>}<`M>tct(<-?1a>ptAzY-}NDqb}kxWN8 zG9jGN2fShsxdDJMDcn01#5cttqJ%TrX?N^upTWM@N`ey9Y+Zq7uHI8P^=ZA)=GHCF zt0V40AOwk1`xIZh9oOd!RKPgz>HcMv`Npw>8|2gvriz4Xt*x!uHfgL9_of+LVAR#i ztZ+FV=DAdzg^Lie@Q`TqN!7^T1HJmpfYtlq_UMb)w^>=&XL?J*mu8yV<@cVLDIY*v zX4(P0c2JM90Yt9vy=@x^H)m!~i_2af;llWC4G*`;AR`4U^JZqE^~$d9Zl!N7D<2>4 zqVmTHGmF67VFBZ9gQib`B1>EKTCKf~Cgl?;#>=@^*1CI{Q4%`~KJupM+6L=@QbZ)V ze0a7@9YO6{%xpdxYJ<3JbfwhZZ^1fNs5cBn=uCuBuxvDCRn7^mpXyP^SyIJfo z@HX&=1(FHG_1ZXS+I1N{P_8SY-{4Z=2ZlhL$s!C0->9=!J_uX={`}q8^Va8#Q$*r* z4bcjy>X6&$r4OSq2)M*itYG5CLy&7^)`0 zGp~&TO6efcC{QRih&A+^!VRpDr?2ti;_&gHBhEQgSP7`d?5k*KeS_FefJm>_f6&`&*cqHR@IjeyKfT3F@<> zW4Fm%^#bs}2mlL^SDVMt%*;$qKM4YXuo=Xm;;RjMAHL!Rbl0GuAmAO9t!HhFr!u

Gc&a5H1zmVybWfk{$63_yW#%@t&MX| diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png index b8c3b5b143d2b7b67116daaef4f7b3f119777494..93d8162b3bf9196b44bc38721cbc254de2854cb4 100644 GIT binary patch literal 4144 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 literal 4149 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 diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png index 49468698cd4e749ad6a666bb253a45695a3e6839..d2270826a5bd84efa4a8dbde8a987d5db0a2dc94 100644 GIT binary patch delta 2732 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 diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png index 50bdac3d8f39aa492bf41ba3e49c787031d07628..2287071ffab678232370853706c56c3c843d4fbb 100644 GIT binary patch delta 684 zcmV;d0#p5`2B`*+Bs=g)L_t(|obB6Na_b-vMNuV{DP{~FQ^w#yDj!NwB!W&O9tY&T z>VEm!h_-ZqAo*?I_YHu~ySf9wtCQ~nB9VO!f9yluffk;R#EYL_vUV`iu(K8)sI>j( z!}tg@A<)^8-sE7hZClw)_@|X*vOVua~gKmjstaQpP3L4 zE13yDzkY@RV~mVPi&ciwTk@ZX^Gw2t8;h9`5F>8du4`%9CAz1ya4BB4 z@Ux?<#4qL?U*=SFXaDkhT79Y^{}Q^jOCby^gxo(*3-?nxoDW!UCl3*N=(iE2&skcZ(Ml1*+mF8f{l?|oue1q93h7MfAAVLjug*tCR6k-c9tUy$ zTzi|fv>MJ?q2@UU^BNX~Fg~NkQ?s3)+3UhCEq-yKq7FAM?U1_~Yp%oO!?C};?fYKb z)*;!U(w~8NMi--n(!nX$RI7IUK@Cd&6#Tb*Pp|4rbAH>c+G*9`dBVK#b)`O{{ym|w z5?FLJJ~(lOnCqK z8wN6CWH?%^GL&AD{X`sR5>DK3%!Gg#Aq2$MzeRykes_VD=N!}_yzt$*5N1yK>RUTAmsYvS-n+D8`@{B%Ql<6pfGC9d=MVh-%@uQ)Db4@a zB&C&hqg84C4Pk@PMe?vKA+tn(K6($GijA4D;Amm~_2urE=SvG;6qc6y+Vi@Wmdb!@ zR+zxli*|;Om(PSv)-#sE+?~luCVveJhY1$>ar8Ivjp0@C_qpQR( zWE@}SRCH(m^m|%;sv-Llx`Xl%`V~UzpQVNCDILxSthb|w@IAD5pm%BVZ7O32y7g1B zF;jZwEwmy9%FVqTMML?qZ@E3N3sKC3Ol$Zk?=E=hEeZ^FyaBLrI`mkMHh)rjK@Z-S zuisvuwzPW2(z=aS!$Wxa(bvA;xSZ>iHpfVzn+fg1&noBD_{fOrM+}+AL5x4w-exVW zhH+M?dCuY8HLM6>ct$f%&Gvt0uf2R};fo6u> 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/ImageFont.py b/src/PIL/ImageFont.py index 457e906c8..c8de65be2 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. @@ -659,6 +661,7 @@ class FreeTypeFont: stroke_width=stroke_width, anchor=anchor, ink=ink, + start=start, )[0] def getmask2( @@ -672,6 +675,7 @@ class FreeTypeFont: stroke_width=0, anchor=None, ink=0, + start=None, *args, **kwargs, ): @@ -750,12 +754,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/_imagingft.c b/src/_imagingft.c index bd4099176..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; From 97a6f651d4ddf07cbd13b2cd38b19dde381050c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 12:01:15 +1100 Subject: [PATCH 021/137] Added Interop tags --- docs/reference/ExifTags.rst | 50 +++++++++++++++++++++---------------- src/PIL/ExifTags.py | 8 ++++++ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index ff5788524..d362334a5 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,35 @@ :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' + + +Two of these values are also exposed as dictionaries. .. py:data:: TAGS :type: dict @@ -26,22 +53,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/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index f3a73bf1a..c00730ba9 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -338,3 +338,11 @@ class GPS(IntEnum): """Maps EXIF GPS tags to tag names.""" GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 1 + InteropVersion = 2 + RelatedImageFileFormat = 4096 + RelatedImageWidth = 4097 + RleatedImageHeight = 4098 From ebde03eae829ca8396dbb446157daf5a5b04e67c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 13:10:08 +1100 Subject: [PATCH 022/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fc8d8362a..574fbdbd3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- 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] From 73bec9622413cf52b71f23723aad6ce8b01c4445 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 21:50:06 +1100 Subject: [PATCH 023/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 574fbdbd3..34c00c3d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added Interop to ExifTags #6724 + [radarhere] + - CVE-2007-4559 patch when building on Windows #6704 [TrellixVulnTeam, nulano, radarhere] From 62fd8336b93e97c15ed1e9afee31ccc1d9b15362 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 18:42:46 +1100 Subject: [PATCH 024/137] Update to Python 3.11 in GitHub Actions --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6195f973b..8a14dad92 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.11" cache: pip cache-dependency-path: "setup.py" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b7f62c23..5cabb6622 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -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..831e33c13 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 @@ -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 From b0ab324f829f8016f89141b332998ba67e3867c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 9 Nov 2022 20:03:16 +1100 Subject: [PATCH 025/137] Use the latest Python version Co-authored-by: Hugo van Kemenade --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a14dad92..49611e287 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" cache: pip cache-dependency-path: "setup.py" From 1c032ff5db5854895314045ee25950a06bea2ae8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Nov 2022 10:37:22 +1100 Subject: [PATCH 026/137] Revert "Install NumPy with OpenBLAS" This reverts commit c82483e35a37919df9700485aa752e8c5a38f28c. --- .github/workflows/macos-install.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 99a11297b108c1427cef683d9dcd196f32794043 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Nov 2022 16:08:42 +1100 Subject: [PATCH 027/137] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c65095640..f4e959fbe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -480,11 +480,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 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 | | +----------------------------------+---------------------------+------------------+--------------+ From 9fbfd3f00efbb6719bb35043315e457614617072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 21:32:40 +1100 Subject: [PATCH 028/137] Added oss-fuzz badge --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 45af4c571..1efbe74c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,6 +61,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Sun, 13 Nov 2022 08:00:20 +1100 Subject: [PATCH 029/137] Added MP Format Version when saving --- Tests/test_file_mpo.py | 1 + src/PIL/MpoImagePlugin.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d94bdaa96..dba1ec1b1 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -268,6 +268,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/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 5bfd8efc1..92d288f2f 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -51,7 +51,7 @@ def _save_all(im, fp, filename): if not offsets: # APP2 marker im.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) @@ -60,6 +60,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"" From 20f17cc6a79881ba441adfb734e8dbe6901a9749 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:14:37 -0600 Subject: [PATCH 030/137] remove unused ImagingAccess->line() method defs --- src/libImaging/Access.c | 62 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 40 deletions(-) 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 From 16994ccc9b40f97113cb1c8f56abe9a37a2744fa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:15:50 -0600 Subject: [PATCH 031/137] remove unused ImagingAccess->line() method def --- src/libImaging/Imaging.h | 1 - 1 file changed, 1 deletion(-) 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); }; From 55abf18f1020b456cd782ebfe94b5847148559f6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:16:50 -0600 Subject: [PATCH 032/137] remove comment about Access.c line methods --- src/PIL/PyAccess.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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. From 21f202a22a712e0392c4ee148946967bd05eb936 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 06:06:08 +1100 Subject: [PATCH 033/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34c00c3d4..bf9a236cc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added MP Format Version when saving MPO #6735 + [radarhere] + - Added Interop to ExifTags #6724 [radarhere] From 84458c3988ad22d82afaa33a4d66080a2a089908 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 08:18:31 +1100 Subject: [PATCH 034/137] Updated xz to 5.2.8 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1fcec66b3..10e2000ae 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.2.8.tar.gz/download", + "filename": "xz-5.2.8.tar.gz", + "dir": "xz-5.2.8", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 8a3ba659450f2b46da7c0e4df92fa10dfc0a21d4 Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Mon, 14 Nov 2022 10:57:30 -0500 Subject: [PATCH 035/137] Remove Tidelift alignment action and badge Not sure if we still care about this? cf. #5762 #5763 --- .github/workflows/tidelift.yml | 36 ---------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/tidelift.yml 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 From 70cc8a57415f6a9744cf4d4f0407a4415319dda2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:06:41 +1100 Subject: [PATCH 036/137] Fixed writing int as BYTE tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) 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/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1dfd5275f..ab9ac5ea2 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) From ddc215ce3c6772a9b3aa6995f38c97ea922ecf48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:51:20 +1100 Subject: [PATCH 037/137] Revert "Added Tidelift Align badge to docs" This reverts commit 06ab0324a3bb66965c7c1505dbdd0aa640ba308b. --- docs/index.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1efbe74c4..5bcd5afa5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,10 +57,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 15 Nov 2022 11:35:14 +1100 Subject: [PATCH 038/137] Revert "Add tidelift alignment badge" This reverts commit c8822a6cac65bbe0a7d831ef2b8431435f1feb1d. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7a81e0c40..8ee68f9b8 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,6 @@ As of 2019, Pillow development is Code coverage - Tidelift Align Fuzzing Status From d4c7bd7e19e926d5d47c70d04eaba86cf20a67a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:04:02 +1100 Subject: [PATCH 039/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf9a236cc..87ff33f1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed writing int as BYTE tag #6740 + [radarhere] + - Added MP Format Version when saving MPO #6735 [radarhere] From 70c8e342a514815ae698a3fb22e2ab52f3c09d7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:15:56 +1100 Subject: [PATCH 040/137] Added "start" argument to docstring --- src/PIL/ImageFont.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8de65be2..3b1a2a23a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -649,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. """ @@ -743,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 From 62db04478733e9baddd41d365f96f9a8dbeb388d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:27:33 +1100 Subject: [PATCH 041/137] Added release notes --- docs/releasenotes/9.4.0.rst | 54 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/9.4.0.rst diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst new file mode 100644 index 000000000..46c7e2f22 --- /dev/null +++ b/docs/releasenotes/9.4.0.rst @@ -0,0 +1,54 @@ +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. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO 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 From cb40f46ec13f2413163baa00e6e44958dd3f67f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 14:58:21 +1100 Subject: [PATCH 042/137] Added Fedora 37 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 1e36b3382..7331cf8ee 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -31,6 +31,7 @@ jobs: debian-10-buster-x86, debian-11-bullseye-x86, fedora-36-amd64, + fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index f4e959fbe..cf6b9ca8f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,6 +442,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | 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 | From df8e87291254dc22415a0c6e371950e79311dbf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Nov 2022 08:26:47 +1100 Subject: [PATCH 043/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 87ff33f1f..cd1b07be4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Use fractional coordinates when drawing text #6722 + [radarhere] + - Fixed writing int as BYTE tag #6740 [radarhere] From 1f6df76c42dab44e00b128b40fd6657185925aca Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Thu, 17 Nov 2022 13:58:07 -0800 Subject: [PATCH 044/137] updated webp with exact parameter. --- Tests/test_file_webp_alpha.py | 34 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 4 ++++ src/PIL/WebPImagePlugin.py | 2 ++ src/_webp.c | 5 +++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index dc82fb742..07df7a068 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,40 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) +def test_write_rgba_keep_transparent(tmp_path): + """ + Can we write a RGBA mode file to WebP while preserving + the transparent RGB without error. + Does it have the bits we expect? + """ + + temp_output_file = str(tmp_path / "temp.webp") + + input_image = hopper("RGB") + # make a copy of the image + output_image = input_image.copy() + # make a single channel image with the same size as input_image + new_alpha = Image.new("L", input_image.size, 255) + # make the left half transparent + new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + # putalpha on output_image + output_image.putalpha(new_alpha) + + # now save with transparent area preserved. + output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) + # even though it is lossless, if we don't put exact=True, the transparent + # area will be filled with black (or something more conducive to compression) + + with Image.open(temp_output_file) as image: + image.load() + + assert image.mode == "RGBA" + assert image.format == "WEBP" + image.load() + image = image.convert("RGB") + assert_image_similar(image, input_image, 1.0) + + def test_write_unsupported_mode_PA(tmp_path): """ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e79db68b..ffc949148 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1124,6 +1124,10 @@ 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. + **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/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 5eaeb10cc..c88f730a2 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,6 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) + exact = im.encoderinfo.get("exact", False) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -336,6 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, + 1 if exact else 0, exif, xmp, ) diff --git a/src/_webp.c b/src/_webp.c index fd99116cb..ec9425d36 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -576,6 +576,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 +598,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 +609,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &icc_bytes, &icc_size, &method, + &exact, &exif_bytes, &exif_size, &xmp_bytes, @@ -633,6 +635,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; + config.exact = exact; // Validate the config if (!WebPValidateConfig(&config)) { From 770560d8e4972d69193a816713be2bda5ff3ed94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:06:14 +0000 Subject: [PATCH 045/137] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp_alpha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 07df7a068..5a57d591a 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,7 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) + def test_write_rgba_keep_transparent(tmp_path): """ Can we write a RGBA mode file to WebP while preserving @@ -111,7 +112,7 @@ def test_write_rgba_keep_transparent(tmp_path): # make a single channel image with the same size as input_image new_alpha = Image.new("L", input_image.size, 255) # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) # putalpha on output_image output_image.putalpha(new_alpha) @@ -130,7 +131,6 @@ def test_write_rgba_keep_transparent(tmp_path): assert_image_similar(image, input_image, 1.0) - def test_write_unsupported_mode_PA(tmp_path): """ Saving a palette-based file with transparency to WebP format From 3587f27780a5be7d02d0c781b39e13919c88d8d6 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:15:24 -0800 Subject: [PATCH 046/137] Added version check for WebP --- src/_webp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index ec9425d36..9231150aa 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -635,7 +635,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)) { From fdf074b050f272e60e37bbd7f6ff52f3fc95a299 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:22:33 -0800 Subject: [PATCH 047/137] added a note to the docs for webp --- docs/handbook/image-file-formats.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ffc949148..9c2319b44 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,6 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **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 From 509dcbf073b3cf8c4fc3d051ebc884a6672e070b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 15:35:06 +1100 Subject: [PATCH 048/137] Added LightSource tag values --- src/PIL/ExifTags.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba9..3df1dbf72 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,27 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class LightSource(IntEnum): + Unknown = 0 + Daylight = 1 + Fluorescent = 2 + Tungsten = 3 + Flash = 4 + Fine = 9 + Cloudy = 10 + Shade = 11 + DaylightFluorescent = 12 + DayWhiteFluorescent = 13 + CoolWhiteFluorescent = 14 + WhiteFluorescent = 15 + StandardLightA = 17 + StandardLightB = 18 + StandardLightC = 19 + D55 = 20 + D65 = 21 + D75 = 22 + D50 = 23 + ISO = 24 + Other = 255 From 96a4d98abc265dabc1442a3e7b0cfdd5043f90b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:07:43 +1100 Subject: [PATCH 049/137] Simplified code --- src/PIL/WebPImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c88f730a2..e3c19db3d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,7 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) - exact = im.encoderinfo.get("exact", False) + exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -337,7 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, - 1 if exact else 0, + exact, exif, xmp, ) From 7e5e843d5cd9f4a17361853138816773da28d8c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:12:51 +1100 Subject: [PATCH 050/137] Note that the fill behaviour only affects libwebp >= 0.5 --- Tests/test_file_webp_alpha.py | 45 ++++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 5a57d591a..df6cffb17 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -97,38 +97,33 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_write_rgba_keep_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path): """ - Can we write a RGBA mode file to WebP while preserving - the transparent RGB without error. - Does it have the bits we expect? + Saving transparent pixels should retain their original RGB values + when using the "exact" parameter. """ - temp_output_file = str(tmp_path / "temp.webp") + image = hopper("RGB") - input_image = hopper("RGB") - # make a copy of the image - output_image = input_image.copy() - # make a single channel image with the same size as input_image - new_alpha = Image.new("L", input_image.size, 255) - # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) - # putalpha on output_image - output_image.putalpha(new_alpha) + # 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) - # now save with transparent area preserved. - output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) - # even though it is lossless, if we don't put exact=True, the transparent - # area will be filled with black (or something more conducive to compression) + # 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_output_file) as image: - image.load() + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGBA" + assert reloaded.format == "WEBP" - assert image.mode == "RGBA" - assert image.format == "WEBP" - image.load() - image = image.convert("RGB") - assert_image_similar(image, input_image, 1.0) + # 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_similar(reloaded.convert("RGB"), image, 1) def test_write_unsupported_mode_PA(tmp_path): From 3c7aa133eb62d75c0f96360ba54c7c4ed19d5c6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:18:27 +1100 Subject: [PATCH 051/137] Assert that image is equal --- Tests/test_file_webp_alpha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index df6cffb17..5970fd2a3 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -123,7 +123,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): # 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_similar(reloaded.convert("RGB"), image, 1) + assert_image_equal(reloaded.convert("RGB"), image) def test_write_unsupported_mode_PA(tmp_path): From 690446050a1963599dc926b06e0360aea0da3f67 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:26:08 -0800 Subject: [PATCH 052/137] minor fix in the comments --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 9231150aa..c2532a496 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -636,7 +636,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { 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 + // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; #endif From d6f10d4876e4f3e1126d3a1d6eac1f75b9d675c2 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:51:06 -0800 Subject: [PATCH 053/137] doc update for libwebp --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9c2319b44..ac39625a2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,7 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **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. + Requires libwebp 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. Only supported if From 55a75b9a696e968ab56a19a207959211cd33adf9 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:14:59 -0800 Subject: [PATCH 054/137] added RN for the new exact option. --- docs/releasenotes/9.4.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 46c7e2f22..3a9c3977f 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -37,6 +37,13 @@ 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. By setting +this option to ``True``, the encoder will keep the hidden RGB values. + Security ======== From 9c5b00ef7e03b921fd55cd6c432bac3a00562d46 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:19:08 -0800 Subject: [PATCH 055/137] RN trailing space fix --- docs/releasenotes/9.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 3a9c3977f..ad79022fe 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,7 +41,7 @@ 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. By setting +removes the hidden RGB values for better compression by default. By setting this option to ``True``, the encoder will keep the hidden RGB values. Security From 8f73a895ec29a8f824466812ef6dae9c90ad0397 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sun, 20 Nov 2022 16:00:24 -0800 Subject: [PATCH 056/137] Update docs/releasenotes/9.4.0.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/9.4.0.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ad79022fe..0f47f5ad6 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,8 +41,9 @@ 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. By setting -this option to ``True``, the encoder will keep the hidden RGB values. +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. Security ======== From be7d350e3f5425fe9eaa2bd3fe8e0751d48b1013 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 11:56:30 +1100 Subject: [PATCH 057/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd1b07be4..461f34e54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added "exact" option when saving WebP #6747 + [ashafaei, radarhere] + - Use fractional coordinates when drawing text #6722 [radarhere] From 100ed363ce1407481331d864ae444b22391bb397 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 15:42:44 +1100 Subject: [PATCH 058/137] Updated libpng to 1.6.39 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 10e2000ae..e4bf275a1 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -228,9 +228,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 From 2c513c6448d24e393de63be85ec30e980b99d8fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 22:05:57 +1100 Subject: [PATCH 059/137] Use stdlib for setuptools on Cygwin --- .github/workflows/test-cygwin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b9ab0eda..bbf0ee736 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 @@ -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: | From 851e7b03ec2a1ccbc98c2d4fcb765e932187c985 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Nov 2022 22:57:10 +1100 Subject: [PATCH 060/137] Document how to install Pillow from a directory --- docs/installation.rst | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index cf6b9ca8f..c50a6cc3c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -103,10 +103,6 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: Building From Source -------------------- -Download and extract the `compressed archive from PyPI`_. - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - .. _external-libraries: External Libraries @@ -191,7 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, run:: +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,6 +208,16 @@ 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 download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +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/ Build Options ^^^^^^^^^^^^^ From 58cbcbf10826039376b563521b24b84e3fddbb8f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 08:47:40 +1100 Subject: [PATCH 061/137] Added getxmp() to WebPImagePlugin --- Tests/test_file_webp_metadata.py | 21 +++++++++++++++++++++ src/PIL/WebPImagePlugin.py | 3 +++ 2 files changed, 24 insertions(+) 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/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e3c19db3d..e9a7aac77 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -98,6 +98,9 @@ class WebPImageFile(ImageFile.ImageFile): return None return self.getexif()._get_merged_dict() + def getxmp(self): + return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} + def seek(self, frame): if not self._seek_check(frame): return From 3473eb8e7f225e6d2fc3709b0565289d64ff9cec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 15:44:03 +1100 Subject: [PATCH 062/137] Added Exif hide_offsets() --- Tests/test_image.py | 25 +++++++++++++++++++++++++ src/PIL/Image.py | 19 ++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e57903490..45fedbe4d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -838,6 +838,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/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..10ca3a65e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3503,6 +3503,7 @@ class Exif(MutableMapping): def __init__(self): self._data = {} + self._hidden_data = {} self._ifds = {} self._info = None self._loaded_exif = None @@ -3556,6 +3557,7 @@ class Exif(MutableMapping): return self._loaded_exif = data self._data.clear() + self._hidden_data.clear() self._ifds.clear() if data and data.startswith(b"Exif\x00\x00"): data = data[6:] @@ -3576,6 +3578,7 @@ class Exif(MutableMapping): def load_from_fp(self, fp, offset=None): self._loaded_exif = None self._data.clear() + self._hidden_data.clear() self._ifds.clear() # process dictionary @@ -3631,8 +3634,9 @@ class Exif(MutableMapping): if tag not in self._ifds: if tag in [0x8769, 0x8825]: # exif, gpsinfo - if tag in self: - self._ifds[tag] = self._get_ifd_dict(self[tag]) + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + self._ifds[tag] = self._get_ifd_dict(offset) elif tag in [0xA005, 0x927C]: # interop, makernote if 0x8769 not in self._ifds: @@ -3717,7 +3721,16 @@ class Exif(MutableMapping): else: # interop self._ifds[tag] = self._get_ifd_dict(tag_data) - return self._ifds.get(tag, {}) + ifd = self._ifds.get(tag, {}) + if tag == 0x8769 and self._hidden_data: + ifd = {k: v for (k, v) in ifd.items() if k not in (0xA005, 0x927C)} + return ifd + + def hide_offsets(self): + for tag in (0x8769, 0x8825): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] def __str__(self): if self._info is not None: From 710927a311c5699b69d78bcd13ea6ddabc0b0563 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:08:49 +1100 Subject: [PATCH 063/137] Added docstring --- src/PIL/WebPImagePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e9a7aac77..81ed550d9 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -99,6 +99,12 @@ class WebPImageFile(ImageFile.ImageFile): 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): From 3f9410334cd9efe4d9ebca5eb59d42b7c1c8778c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:11:51 +1100 Subject: [PATCH 064/137] Added getxmp() to release notes --- docs/releasenotes/9.4.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 0f47f5ad6..f2b50fa5b 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,12 @@ 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. +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +decoded for WEBP images through ``getxmp()``. + Security ======== From 72372ad23f612a320c470c442afb4adab39d988b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 20:42:04 +1100 Subject: [PATCH 065/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 461f34e54..7fac5201c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added getxmp() to WebPImagePlugin #6758 + [radarhere] + - Added "exact" option when saving WebP #6747 [ashafaei, radarhere] From 24a5405a9f7ea22f28f9c98b3e407292ea5ee1d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 08:39:56 +1100 Subject: [PATCH 066/137] Added IFD enum --- docs/reference/ExifTags.rst | 7 ++++++ src/PIL/ExifTags.py | 7 ++++++ src/PIL/Image.py | 45 ++++++++++++++++++++++++------------- src/PIL/MpoImagePlugin.py | 11 +++++++-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index d362334a5..650bb4f95 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -31,6 +31,13 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> Interop(4096).name 'RelatedImageFileFormat' +.. py:data:: IFD + + >>> from PIL.ExifTags import IFD + >>> IFD.Exif.value + 34665 + >>> IFD(34665).name + 'Exif' Two of these values are also exposed as dictionaries. diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba9..97a21335f 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,10 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class IFD(IntEnum): + Exif = 34665 + GPSInfo = 34853 + Makernote = 37500 + Interop = 40965 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..3fcc86931 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -47,7 +47,14 @@ except ImportError: # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) from ._binary import i32le, o32be, o32le from ._deprecate import deprecate from ._util import DeferredError, is_path @@ -3598,14 +3605,16 @@ class Exif(MutableMapping): merged_dict = dict(self) # get EXIF extension - if 0x8769 in self: - ifd = self._get_ifd_dict(self[0x8769]) + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif]) if ifd: merged_dict.update(ifd) # GPS - if 0x8825 in self: - merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo] + ) return merged_dict @@ -3615,30 +3624,34 @@ class Exif(MutableMapping): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): - if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): + if tag in [ + ExifTags.IFD.Exif, + 0x8225, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): value = self.get_ifd(tag) if ( - tag == 0x8769 - and 0xA005 in value - and not isinstance(value[0xA005], dict) + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) ): value = value.copy() - value[0xA005] = self.get_ifd(0xA005) + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): if tag not in self._ifds: - if tag in [0x8769, 0x8825]: + if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: # exif, gpsinfo if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) - elif tag in [0xA005, 0x927C]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: # interop, makernote - if 0x8769 not in self._ifds: - self.get_ifd(0x8769) - tag_data = self._ifds[0x8769][tag] - if tag == 0x927C: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.Makernote: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 92d288f2f..3ae4d4abf 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 @@ -137,7 +144,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] if mptype.startswith("Large Thumbnail"): - exif = self.getexif().get_ifd(0x8769) + exif = self.getexif().get_ifd(ExifTags.IFD.Exif) if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: From a0326245a288801b7ea4753eef32d94398c5b9af Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 21:19:13 +1100 Subject: [PATCH 067/137] Removed typo --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3fcc86931..d07fc716c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3626,7 +3626,6 @@ class Exif(MutableMapping): for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, - 0x8225, ExifTags.IFD.GPSInfo, ] and not isinstance(value, dict): value = self.get_ifd(tag) From 50cdf39f505158e37af2cbe39458f3ab27e7377e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 09:18:47 +1100 Subject: [PATCH 068/137] List dependency instructions first --- docs/installation.rst | 186 ++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 99 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c50a6cc3c..af1d3399c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -187,85 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -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: - -If the prerequisites are installed in the standard library locations -for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no -additional configuration should be required. If they are installed in -a non-standard location, you may need to configure setuptools to use -those locations by editing :file:`setup.py` or -:file:`setup.cfg`, or by adding environment variables on the command -line:: - - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: - -If Pillow has been previously built without the required -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 download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. - -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/ - -Build Options -^^^^^^^^^^^^^ - -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. - -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, - ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``, ``--disable-xcb``. - Disable building the corresponding feature even if the development - libraries are present on the building machine. - -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, - ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``, ``--enable-xcb``. - Require that the corresponding feature is built. The build will raise - an exception if the libraries are not found. Webpmux (WebP metadata) - relies on WebP support. Tcl and Tk also must be used together. - -* Build flags: ``--vendor-raqm --vendor-fribidi`` - These flags are used to compile a modified version of libraqm and - a shim that dynamically loads libfribidi at runtime. These are - used to compile the standard Pillow wheels. Compiling libraqm requires - a C99-compliant compiler. - -* Build flag: ``--disable-platform-guessing``. Skips all of the - platform dependent guessing of include and library directories for - automated build systems that configure the proper paths in the - environment variables (e.g. Buildroot). - -* Build flag: ``--debug``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. - - -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`` @@ -285,25 +208,19 @@ To install libraqm on macOS use Homebrew to install its dependencies:: 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. +The scripts also install Pillow from the local copy of the source code, so the +`Installing`_ instructions will not be necessary afterwards. + 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. @@ -332,14 +249,8 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: 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 @@ -353,9 +264,8 @@ Prerequisites are installed on **FreeBSD 10 or 11** with:: 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. @@ -403,7 +313,7 @@ See also the ``Dockerfile``\s in the Test Infrastructure repo 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:: @@ -413,6 +323,84 @@ environment. The dependencies can be installed by:: 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: + +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`setup.cfg`, or by adding environment variables on the command +line:: + + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: + +If Pillow has been previously built without the required +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 download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +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/ + +Build Options +""""""""""""" + +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. + +* Build flags: ``--disable-zlib``, ``--disable-jpeg``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, + ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-imagequant``, ``--disable-xcb``. + Disable building the corresponding feature even if the development + libraries are present on the building machine. + +* Build flags: ``--enable-zlib``, ``--enable-jpeg``, + ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, + ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-imagequant``, ``--enable-xcb``. + Require that the corresponding feature is built. The build will raise + an exception if the libraries are not found. Webpmux (WebP metadata) + relies on WebP support. Tcl and Tk also must be used together. + +* Build flags: ``--vendor-raqm --vendor-fribidi`` + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + +* Build flag: ``--disable-platform-guessing``. Skips all of the + platform dependent guessing of include and library directories for + automated build systems that configure the proper paths in the + environment variables (e.g. Buildroot). + +* Build flag: ``--debug``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to + stdout. + + +Sample usage:: + + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Platform Support ---------------- From 556b672eb2f982a90e271a580d2a1f00b78ca131 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 27 Nov 2022 17:48:12 -0600 Subject: [PATCH 069/137] Fix webp dealloc method definitions --- src/_webp.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index c2532a496..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 * From 91fe817911cd8f4a4fa30632aeff621e495cfa7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Nov 2022 18:03:08 +1100 Subject: [PATCH 070/137] Updated instructions to download source code Co-authored-by: Hugo van Kemenade --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index af1d3399c..6d67a2536 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -348,15 +348,15 @@ 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 download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. +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/ +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files Build Options """"""""""""" From e3a46fcfd0111d7f080da0efe5846430771afeeb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 11:08:39 +0200 Subject: [PATCH 071/137] Use sphinx-inline-tabs to organise installation per OS --- .editorconfig | 4 + docs/Makefile | 2 +- docs/conf.py | 7 +- docs/installation.rst | 293 +++++++++++++++++++++--------------------- setup.cfg | 1 + 5 files changed, 157 insertions(+), 150 deletions(-) diff --git a/.editorconfig b/.editorconfig index d74549fe2..7f5eab056 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,10 @@ indent_style = space trim_trailing_whitespace = true +[*.rst] +# Three-space indentation +indent_size = 3 + [*.yml] # Two-space indentation indent_size = 2 diff --git a/docs/Makefile b/docs/Makefile index 458299aac..0a663ce2b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,7 +43,7 @@ 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 html: $(MAKE) install-sphinx 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/installation.rst b/docs/installation.rst index 6d67a2536..3c86f09cc 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,6 +23,11 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 +.. _Windows Installation: +.. _macOS Installation: +.. _Linux Installation: +.. _FreeBSD Installation: + Basic Installation ------------------ @@ -38,67 +43,69 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -Windows Installation -^^^^^^^^^^^^^^^^^^^^ +.. tab:: Windows -We provide Pillow binaries for Windows compiled for the matrix of -supported Pythons in both 32 and 64-bit versions in the wheel format. -These binaries include support for all optional libraries except -libimagequant and libxcb. Raqm support requires -FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow -To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + +.. tab:: macOS + + 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 + +.. tab:: Linux + + 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``. + +.. tab:: FreeBSD + + 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. -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 macOS: +.. _Building on Windows: +.. _Building on Windows using MSYS2/MinGW: +.. _Building on FreeBSD: +.. _Building on Linux: +.. _Building on Android: Building From Source -------------------- @@ -187,141 +194,135 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Building on macOS -""""""""""""""""" +.. 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 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:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp -To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Windows -""""""""""""""""""" +.. 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. + 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. + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. -Building on Windows using MSYS2/MinGW -""""""""""""""""""""""""""""""""""""" +.. 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. + 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-``. + 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:: + 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 + 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:: + 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 + 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 -Building on FreeBSD -""""""""""""""""""" +.. tab:: FreeBSD -.. Note:: Only FreeBSD 10 and 11 tested + .. Note:: Only FreeBSD 10 and 11 tested -Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 -Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Linux -""""""""""""""""" +.. tab:: Linux -If you didn't build Python from source, make sure you have Python's -development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. -In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools -In Fedora, the command is:: + In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config -In Alpine, the command is:: + In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools -.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + .. 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:: + 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 + 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``. + 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:: + 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 + 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. + Note that the package manager may be yum or DNF, depending on the + exact distribution. -Prerequisites are installed for **Alpine** with:: + 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 + 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. + 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 -""""""""""""""""""" +.. tab:: Android -Basic Android support has been added for compilation within the Termux -environment. The dependencies can be installed by:: + 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 + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo -This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ 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 From e6e5a0018e27779827678552ea2b17a7c1034e7e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:34:43 +0200 Subject: [PATCH 072/137] Add missing 'make help' for serve and livehtml --- docs/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 0a663ce2b..a65e2d3f5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,6 +20,8 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 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" @@ -38,6 +40,8 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @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" clean: -rm -rf $(BUILDDIR)/* From d12c119ec41e62642157a2add640fc0211d46066 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:36:04 +0200 Subject: [PATCH 073/137] Inline PHONY targets to help avoid omissions (texinfo, info, livehtml, serve were missing) --- docs/Makefile | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index a65e2d3f5..d5242f935 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,8 +15,7 @@ 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" @@ -40,45 +39,50 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @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" +.PHONY: clean clean: -rm -rf $(BUILDDIR)/* install-sphinx: $(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 @@ -86,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 @@ -96,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 @@ -106,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 @@ -120,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 @@ -127,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 @@ -147,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 @@ -154,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 @@ -173,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 From 5e42b1779e29ecbd34adcdd911b44cbe87ad439b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:50:55 +0200 Subject: [PATCH 074/137] Reorder tabs: big three OS first --- docs/installation.rst | 146 +++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 3c86f09cc..00924eab9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,9 +23,9 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 -.. _Windows Installation: -.. _macOS Installation: .. _Linux Installation: +.. _macOS Installation: +.. _Windows Installation: .. _FreeBSD Installation: Basic Installation @@ -43,29 +43,6 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -.. tab:: Windows - - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. - -.. tab:: macOS - - 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 - .. tab:: Linux We provide binaries for Linux for each of the supported Python @@ -81,6 +58,29 @@ Install Pillow with :command:`pip`:: ``python-imaging``. Debian splits it into two packages, ``python3-pil`` and ``python3-pil.imagetk``. +.. tab:: macOS + + 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 + +.. tab:: Windows + + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + .. tab:: FreeBSD Pillow can be installed on FreeBSD via the official Ports or Packages systems: @@ -100,11 +100,11 @@ Install Pillow with :command:`pip`:: 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 Linux: .. _Building on Android: Building From Source @@ -194,6 +194,53 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +.. 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 @@ -267,53 +314,6 @@ Many of Pillow's features require external libraries: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -.. 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:: Android Basic Android support has been added for compilation within the Termux From 50ccb27a4d57500b9b5e474e276df03158b6c870 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 23:23:46 +0200 Subject: [PATCH 075/137] Remove extra space Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index d5242f935..d32d25a3c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -90,7 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." -.PHONY: qthelp +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp From 3ec8fa614705ae273426d60f994e3b01bb57a69a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Nov 2022 13:49:07 +1100 Subject: [PATCH 076/137] Do not trust JPEG decoder to determine image is CMYK --- src/PIL/BlpImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) 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()) From aab7983146729c81a5105b6511858f41d76b53f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Dec 2022 07:57:26 +1100 Subject: [PATCH 077/137] Updated xz to 5.2.9 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e4bf275a1..66e352c73 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.8.tar.gz/download", - "filename": "xz-5.2.8.tar.gz", - "dir": "xz-5.2.8", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.9.tar.gz/download", + "filename": "xz-5.2.9.tar.gz", + "dir": "xz-5.2.9", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 96b316880e284f4221b2d8400ef1bec5c81bcb83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 2 Dec 2022 11:40:06 +0200 Subject: [PATCH 078/137] Use 4-space indents for RST --- .editorconfig | 2 +- docs/installation.rst | 218 +++++++++++++++++++++--------------------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7f5eab056..07f02c236 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ trim_trailing_whitespace = true [*.rst] # Three-space indentation -indent_size = 3 +indent_size = 4 [*.yml] # Two-space indentation diff --git a/docs/installation.rst b/docs/installation.rst index 00924eab9..89b2e558f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -45,59 +45,59 @@ Install Pillow with :command:`pip`:: .. tab:: Linux - 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:: + 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 + 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``. + 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``. .. tab:: macOS - 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:: + 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 + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow .. tab:: Windows - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. .. tab:: FreeBSD - Pillow can be installed on FreeBSD via the official Ports or Packages systems: + Pillow can be installed on FreeBSD via the official Ports or Packages systems: - **Ports**:: + **Ports**:: - cd /usr/ports/graphics/py-pillow && make install clean + cd /usr/ports/graphics/py-pillow && make install clean - **Packages**:: + **Packages**:: - pkg install py38-pillow + pkg install py38-pillow - .. note:: + .. note:: - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. .. _Building on Linux: @@ -196,133 +196,133 @@ Many of Pillow's features require external libraries: .. tab:: Linux - If you didn't build Python from source, make sure you have Python's - development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. - In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools - In Fedora, the command is:: + In Fedora, the command is:: sudo dnf install python3-devel redhat-rpm-config - In Alpine, the command is:: + 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. + .. 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:: + 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 + 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``. + 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:: + 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 + 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. + Note that the package manager may be yum or DNF, depending on the + exact distribution. - Prerequisites are installed for **Alpine** with:: + 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 + 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. + 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 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:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp - To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + 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. + 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. + 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. + 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-``. + 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:: + 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 + 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:: + 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 + 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 + .. Note:: Only FreeBSD 10 and 11 tested - Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 - Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + 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:: + 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 + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo - This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ From c120649632391c1f287cae6b7ff7ce7c28ddb20b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Dec 2022 21:29:27 +1100 Subject: [PATCH 079/137] Remove specific number of jobs from comment --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5cabb6622..e2a9de65c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -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 From d822d85af6d9b1c108650832d56b75cd86b2f5a9 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Fri, 2 Dec 2022 17:57:19 +0000 Subject: [PATCH 080/137] support round-tripping JPEG comments --- Tests/test_file_jpeg.py | 12 ++++++++++++ src/PIL/JpegImagePlugin.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b..94ef59565 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -87,6 +87,18 @@ class TestFileJpeg: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + def test_com_write(self): + dummy_text = "this is a test comment" + with Image.open(TEST_FILE) as im: + with BytesIO() as buf: + im.save(buf, format="JPEG") + with Image.open(buf) as im2: + assert im.app['COM'] == im2.app['COM'] + with BytesIO() as buf: + im.save(buf, format="JPEG", comment=dummy_text) + with Image.open(buf) as im2: + assert im2.app['COM'].decode() == dummy_text + def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc..a6abe8b9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,6 +44,7 @@ import warnings from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._binary import o16be as o16 from ._binary import o8 from ._deprecate import deprecate from .JpegPresets import presets @@ -713,6 +714,15 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + comment = info.get("comment") + if comment is None and isinstance(im, JpegImageFile): + comment = im.app.get('COM') + if comment: + if isinstance(comment, str): + comment = comment.encode() + size = o16(2 + len(comment)) + extra += b'\xFF\xFE%s%s' % (size, comment) + icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 From e9f485849157100ddf75f289db6fcb509927706c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:07:07 +0000 Subject: [PATCH 081/137] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 4 ++-- src/PIL/JpegImagePlugin.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 94ef59565..ffaf2caba 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -93,11 +93,11 @@ class TestFileJpeg: with BytesIO() as buf: im.save(buf, format="JPEG") with Image.open(buf) as im2: - assert im.app['COM'] == im2.app['COM'] + assert im.app["COM"] == im2.app["COM"] with BytesIO() as buf: im.save(buf, format="JPEG", comment=dummy_text) with Image.open(buf) as im2: - assert im2.app['COM'].decode() == dummy_text + assert im2.app["COM"].decode() == dummy_text def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6abe8b9f..cb8a4e57f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,8 +44,8 @@ import warnings from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._binary import o16be as o16 from ._binary import o8 +from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets @@ -716,12 +716,12 @@ def _save(im, fp, filename): comment = info.get("comment") if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get('COM') + comment = im.app.get("COM") if comment: if isinstance(comment, str): comment = comment.encode() size = o16(2 + len(comment)) - extra += b'\xFF\xFE%s%s' % (size, comment) + extra += b"\xFF\xFE%s%s" % (size, comment) icc_profile = info.get("icc_profile") if icc_profile: From 976ad5746a0155135337efa75648c550705215c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:29:02 +1100 Subject: [PATCH 082/137] Save comments from any image format by default --- src/PIL/JpegImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index cb8a4e57f..c9de714d8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,9 +714,7 @@ def _save(im, fp, filename): extra = info.get("extra", b"") - comment = info.get("comment") - if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get("COM") + comment = info.get("comment", im.info.get("comment")) if comment: if isinstance(comment, str): comment = comment.encode() From c1d0a00943ee6fcc993f47047b798e2b6f9bac6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:31:05 +1100 Subject: [PATCH 083/137] Use _binary instead of struct --- src/PIL/JpegImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index c9de714d8..92dbb3193 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -732,7 +732,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 From 525c01143a8a4e0133908826577ccb54ed829a1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:59:22 +1100 Subject: [PATCH 084/137] Test that comment is reread --- Tests/test_file_jpeg.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ffaf2caba..bb4ebb686 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -86,18 +86,26 @@ 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_com_write(self): - dummy_text = "this is a test comment" + def test_comment_write(self): with Image.open(TEST_FILE) as im: - with BytesIO() as buf: - im.save(buf, format="JPEG") - with Image.open(buf) as im2: - assert im.app["COM"] == im2.app["COM"] - with BytesIO() as buf: - im.save(buf, format="JPEG", comment=dummy_text) - with Image.open(buf) as im2: - assert im2.app["COM"].decode() == dummy_text + 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"] + + # 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, From 61cbcaee64a852bc9902d60ab0732f676d6d0e72 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:35:01 +1100 Subject: [PATCH 085/137] Changed indentation to be consistent --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 89b2e558f..b559c824d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -205,11 +205,11 @@ Many of Pillow's features require external libraries: In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. From eafff0e1396a1b55522c561b7e355c4e7ebaa5b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:54:04 +1100 Subject: [PATCH 086/137] Use compile_python_fuzzer --- Tests/oss-fuzz/build.sh | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 09cc7bc16..b459ee47a 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -19,9 +19,7 @@ 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:. \ @@ -31,17 +29,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --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 -@ From 8e70787cf20399f2a88976ad8ad4d3983f81c741 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 01:44:21 +0000 Subject: [PATCH 087/137] Update cygwin/cygwin-install-action action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index bbf0ee736..37dc694c6 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -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: > From 61f27211c2d50a20ac544ff8ab16c58439c4cbe8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Dec 2022 07:43:58 +0200 Subject: [PATCH 088/137] Fix comment Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 07f02c236..449530717 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_style = space trim_trailing_whitespace = true [*.rst] -# Three-space indentation +# Four-space indentation indent_size = 4 [*.yml] From 1ed1a3a971e127b55353bde39cb7ea6bedb45c04 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Sat, 3 Dec 2022 15:07:37 +0000 Subject: [PATCH 089/137] make sure passing a blank comment removes existing comment --- Tests/test_file_jpeg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bb4ebb686..7a958c7da 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -98,6 +98,13 @@ class TestFileJpeg: 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() From 8ada23ed04ee18730d44d14dd82b0aabc12a0917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 09:09:00 +1100 Subject: [PATCH 090/137] Added IFD1 reading --- Tests/test_image.py | 21 ++++++++++++++++++++- src/PIL/ExifTags.py | 1 + src/PIL/Image.py | 10 +++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e57903490..b4e81e466 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, @@ -808,6 +815,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() diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 97a21335f..ffab7e554 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -353,3 +353,4 @@ class IFD(IntEnum): GPSInfo = 34853 Makernote = 37500 Interop = 40965 + IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d07fc716c..1f3d4b74f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3641,17 +3641,17 @@ class Exif(MutableMapping): def get_ifd(self, tag): if tag not in self._ifds: - if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - # exif, gpsinfo + if tag == ExifTags.IFD.IFD1: + if self._info is not None: + self._ifds[tag] = self._get_ifd_dict(self._info.next) + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: - # interop, makernote if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] if tag == ExifTags.IFD.Makernote: - # makernote from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -3727,7 +3727,7 @@ class Exif(MutableMapping): makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self._ifds[tag] = makernote else: - # interop + # Interop self._ifds[tag] = self._get_ifd_dict(tag_data) return self._ifds.get(tag, {}) From e50ae85ea406d86073ca88ffdec469e1e18d7527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 13:57:26 +1100 Subject: [PATCH 091/137] Use jpeg_write_marker to write comment --- src/PIL/JpegImagePlugin.py | 12 +++++------- src/encode.c | 26 ++++++++++++++++++++++---- src/libImaging/Jpeg.h | 4 ++++ src/libImaging/JpegEncode.c | 13 ++++++++++++- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 92dbb3193..7b5b32be0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,13 +714,6 @@ def _save(im, fp, filename): extra = info.get("extra", b"") - comment = info.get("comment", im.info.get("comment")) - if comment: - if isinstance(comment, str): - comment = comment.encode() - size = o16(2 + len(comment)) - extra += b"\xFF\xFE%s%s" % (size, comment) - icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 @@ -743,6 +736,10 @@ def _save(im, fp, filename): ) i += 1 + comment = info.get("comment", im.info.get("comment")) or b"" + if isinstance(comment, str): + comment = comment.encode() + # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) @@ -765,6 +762,7 @@ def _save(im, fp, filename): dpi[1], subsampling, qtables, + comment, extra, exif, ) diff --git a/src/encode.c b/src/encode.c index 72c7f64d0..a2eae81fd 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|nnnnnnnnOy#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,12 +1094,24 @@ 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) { return ImagingError_MemoryError(); } @@ -1107,7 +1123,7 @@ 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 (extra) { free(extra); @@ -1134,6 +1150,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/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..b6e3acc95 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_size > 0) { + 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; From 72ac7d1ce9e15803e4adb759dbd318b75d652724 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 18:53:28 +1100 Subject: [PATCH 092/137] Corrected default combined frame duration --- Tests/images/duplicate_frame.gif | Bin 0 -> 138 bytes Tests/test_file_gif.py | 16 ++++++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Tests/images/duplicate_frame.gif diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef0c894a540b0ca3074938666fd22a7d93d1fd0d GIT binary patch literal 138 zcmZ?wbhEHb Date: Mon, 5 Dec 2022 17:46:54 +0000 Subject: [PATCH 093/137] switch to #z for comment parameter * means `comment=None` can be passed directly * no need to conditionally run `str.encode()` * clean up checking of whether a comment is passed --- src/PIL/JpegImagePlugin.py | 4 +--- src/encode.c | 2 +- src/libImaging/JpegEncode.c | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7b5b32be0..ef0be6699 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -736,9 +736,7 @@ def _save(im, fp, filename): ) i += 1 - comment = info.get("comment", im.info.get("comment")) or b"" - if isinstance(comment, str): - comment = comment.encode() + comment = info.get("comment", im.info.get("comment")) # "progressive" is the official name, but older documentation # says "progression" diff --git a/src/encode.c b/src/encode.c index a2eae81fd..d37cbfbcf 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1057,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#y#", + "ss|nnnnnnnnOz#y#y#", &mode, &rawmode, &quality, diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index b6e3acc95..2a24eff39 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -278,7 +278,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { case 4: - if (context->comment_size > 0) { + if (context->comment) { jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); } state->state++; From b786ff819a9799974e55db3d639b0de0b49d9b89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:25:01 +0000 Subject: [PATCH 094/137] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c13fb3b1..d44874bf7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,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,7 +37,7 @@ 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 From c2a42655e10c7b3888f3c50b49717886296e1720 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 19:30:53 +1100 Subject: [PATCH 095/137] Allow get_child_images to access JPEG thumbnails --- Tests/images/flower_thumbnail.png | Bin 0 -> 35617 bytes Tests/test_file_jpeg.py | 7 +++++ src/PIL/Image.py | 43 ++++++++++++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 1 + src/PIL/TiffImagePlugin.py | 33 ----------------------- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 Tests/images/flower_thumbnail.png diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..4a362535f25fcdf10c49c368f4252f48f8d45bd0 GIT binary patch literal 35617 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 RR_6c!002ovPDHLkV1fuV02lxO literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b..987187556 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -415,6 +415,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/src/PIL/Image.py b/src/PIL/Image.py index 1f3d4b74f..e568e6afa 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1454,6 +1454,49 @@ class Image: self._exif._loaded = False self.getexif() + def get_child_images(self): + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + thumbnailOffset = ifd.get(513) + if thumbnailOffset is not None: + try: + thumbnailOffset += self._exif_offset + except AttributeError: + pass + self.fp.seek(thumbnailOffset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) + + with open(fp) as im: + if thumbnailOffset is None: + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def getim(self): """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc..f2d8c4846 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -89,6 +89,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 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab9ac5ea2..aa2a782c2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1153,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. From 1d780081a620b00007a8fe93db469e5759c86868 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 20:22:12 +1100 Subject: [PATCH 096/137] Free comment when returning early --- src/encode.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encode.c b/src/encode.c index d37cbfbcf..e6352cbfe 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1113,6 +1113,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { + if (comment) { + free(comment); + } return ImagingError_MemoryError(); } memcpy(p, extra, extra_size); @@ -1125,6 +1128,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { + if (comment) { + free(comment); + } if (extra) { free(extra); } From 674ec6ec4dd1083b4666e283459beeba0e422fb4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 6 Dec 2022 20:55:34 +0200 Subject: [PATCH 097/137] Add support for PyPy3.9, drop PyPy3.7 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e2a9de65c..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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 831e33c13..11c7b77be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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", From 4704cab1a1b4dfc34b0bc0c06bdcdc56b365b69f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:16:14 +1100 Subject: [PATCH 098/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7fac5201c..f3ad8c797 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Support saving JPEG comments #6774 + [smason, radarhere] + - Added getxmp() to WebPImagePlugin #6758 [radarhere] From bef128b04bcc220aa6b57afa58b796f7b289ddf7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:30:15 +1100 Subject: [PATCH 099/137] Added support for saving JPEG comments --- docs/handbook/image-file-formats.rst | 5 +++++ docs/releasenotes/9.4.0.rst | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ac39625a2..c9e32835a 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:: diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index f2b50fa5b..ccbe62a6b 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -51,6 +51,14 @@ 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 ======== From 4ab837ae23103b841d2e5fa7ac91a8ff92279627 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Dec 2022 11:35:48 +1100 Subject: [PATCH 100/137] Only compare to previous when checking for duplicate frames while saving --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 32 +++++++++++++++++--------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1ee..2cbaf2805 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: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e..367958048 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -565,6 +565,16 @@ def _write_single_frame(im, fp, palette): fp.write(b"\0") # end of image data +def _getbbox(base_im, im_frame): + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) + else: + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) + return delta.getbbox() + + def _write_multiple_frames(im, fp, palette): duration = im.encoderinfo.get("duration") @@ -598,6 +608,12 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] + bbox = _getbbox(previous["im"], im_frame) + if not bbox: + # This frame is identical to the previous frame + if duration: + previous["encoderinfo"]["duration"] += encoderinfo["duration"] + continue if encoderinfo.get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( @@ -606,21 +622,7 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - base_im = background_im - else: - base_im = previous["im"] - if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): - delta = ImageChops.subtract_modulo(im_frame, base_im) - else: - delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") - ) - bbox = delta.getbbox() - if not bbox: - # This frame is identical to the previous frame - if duration: - previous["encoderinfo"]["duration"] += encoderinfo["duration"] - continue + bbox = _getbbox(background_im, im_frame) else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) From 7436ae0933ea4897111d2aebfb16b59a5c960a35 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Dec 2022 16:58:04 +0200 Subject: [PATCH 101/137] Remove unnecessary Pipfile --- MANIFEST.in | 2 - Pipfile | 22 ---- Pipfile.lock | 324 --------------------------------------------------- 3 files changed, 348 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock 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/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": {} -} From 66f5ad0eae90b6f4b07df1a3154f996c6fe00069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 10:45:09 +1100 Subject: [PATCH 102/137] Ignore non-opaque WebP background when saving as GIF --- Tests/test_file_gif.py | 13 +++++++++++-- src/PIL/GifImagePlugin.py | 15 +++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1ee..a196c1612 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -859,14 +859,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/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e..01518b378 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -886,20 +886,23 @@ def _get_palette_bytes(im): def _get_background(im, info_background): background = 0 if info_background: - background = info_background - if isinstance(background, tuple): + if isinstance(info_background, tuple): # WebPImagePlugin stores an RGBA value in info["background"] # So it must be converted to the same format as GifImagePlugin's # info["background"] - a global color table index try: - background = im.palette.getcolor(background, im) + background = im.palette.getcolor(info_background, im) except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": + if str(e) not in ( # If all 256 colors are in use, # then there is no need for the background color - return 0 - else: + "cannot allocate more than 256 colors", + # Ignore non-opaque WebP background + "cannot add non-opaque RGBA color to RGB palette", + ): raise + else: + background = info_background return background From 4f0b83cc54230728bbd3593a3116cac046c5ee4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 12:29:27 +1100 Subject: [PATCH 103/137] Only set tile in ImageFile __setstate__ --- src/PIL/Image.py | 1 - src/PIL/ImageFile.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..bf93917ed 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -704,7 +704,6 @@ class Image: def __setstate__(self, state): Image.__init__(self) - self.tile = [] info, mode, size, palette, data = state self.info = info self.mode = mode 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""" From ae3f43de64afbd59fdc424f37f18964dd25765e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Dec 2022 19:48:07 +1100 Subject: [PATCH 104/137] Document Hue range --- docs/handbook/concepts.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index a9b33e437..083351eec 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -41,6 +41,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) From f6f622dceee19fef36e6746a7943f2e806d8cabd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:36:27 +1100 Subject: [PATCH 105/137] Clarify apply_transparency() docstring --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..155a546c2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1482,7 +1482,8 @@ class Image: def apply_transparency(self): """ If a P mode image has a "transparency" key in the info dictionary, - remove the key and apply the transparency to the palette instead. + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. """ if self.mode != "P" or "transparency" not in self.info: return From 164311a7568c7fed3c7a1dd60570cc182d3d5a0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:55:10 +1100 Subject: [PATCH 106/137] Specify "I" and "F" ranges --- docs/handbook/concepts.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 083351eec..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) From 1f9754cdc0c03405bbe1e3aa73b5dbb6750aa608 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:04:30 +0200 Subject: [PATCH 107/137] Format tox.ini with tox-ini-fmt --- .pre-commit-config.yaml | 9 +++++++-- tox.ini | 24 ++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d44874bf7..8d133b18d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: ["--target-version", "py37"] @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.11.1 hooks: - id: isort @@ -48,5 +48,10 @@ repos: 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/tox.ini b/tox.ini index 21b5d4b50..195522ffa 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,14 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -deps = - cffi - numpy [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 From bfa1f3290c8ae830e0240dbfad2626fa6b49bb1b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:06:58 +0200 Subject: [PATCH 108/137] Add allowlist_externals=make to fix tox 4 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 195522ffa..9a41ca96b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} +allowlist_externals = make [testenv:lint] passenv = From 56964da7487c7fff897cd3b41f11d62922f84046 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:45:57 +1100 Subject: [PATCH 109/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f3ad8c797..1bcb9d2e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed bug combining GIF frame durations #6779 + [radarhere] + - Support saving JPEG comments #6774 [smason, radarhere] From 5301b86f1cd255fc55a464b38af176f37f91c396 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:48:36 +1100 Subject: [PATCH 110/137] Use snake case --- src/PIL/Image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e568e6afa..c2216e27a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1476,18 +1476,18 @@ class Image: offset = current_offset fp = self.fp - thumbnailOffset = ifd.get(513) - if thumbnailOffset is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: try: - thumbnailOffset += self._exif_offset + thumbnail_offset += self._exif_offset except AttributeError: pass - self.fp.seek(thumbnailOffset) + self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) with open(fp) as im: - if thumbnailOffset is None: + if thumbnail_offset is None: im._frame_pos = [ifd_offset] im._seek(0) im.load() From b564f3e6bf82bb705ae410b44422aefbc56198e2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 07:41:39 +1100 Subject: [PATCH 111/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1bcb9d2e9..0372b5b37 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added IFD enum to ExifTags #6748 + [radarhere] + - Fixed bug combining GIF frame durations #6779 [radarhere] From e25d6031891cd53917379bb489d7b22614fe06fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 09:48:46 +1100 Subject: [PATCH 112/137] Updated xz to 5.4.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 66e352c73..0c3152b06 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.9.tar.gz/download", - "filename": "xz-5.2.9.tar.gz", - "dir": "xz-5.2.9", + "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": { From d1cb81976cba7fbd3b13525a26b163cc42f029a7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:32:55 +0200 Subject: [PATCH 113/137] Run Bandit on CI via pre-commit --- .pre-commit-config.yaml | 9 ++++++++- src/PIL/ImageShow.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d133b18d..609352f22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: 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: [] @@ -13,6 +13,13 @@ repos: 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: 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 From 1a051f2e079253c74918ac89cbe899f4c6136bc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Dec 2022 07:50:40 +0000 Subject: [PATCH 114/137] Update egor-tensin/cleanup-path action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 37dc694c6..f297eb1b5 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -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' From 7f6fe3c28728f0e68dba58b6ea9843de0b00ca3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Dec 2022 08:15:32 +1100 Subject: [PATCH 115/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0372b5b37..1e5f71b86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 + [radarhere] + - Added IFD enum to ExifTags #6748 [radarhere] From 5eaca52efd86e41dc068802fd2683d433a45003e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 07:04:05 +1100 Subject: [PATCH 116/137] Updated harfbuzz to 6.0.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0c3152b06..a1908e35e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -355,9 +355,9 @@ 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-"), From 2a86d7353f1a1435d564d0ac882268c80fc9486d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 08:19:15 +1100 Subject: [PATCH 117/137] Always initialize all plugins in registered_extensions() --- Tests/test_image.py | 6 ------ src/PIL/Image.py | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b4e81e466..69a66b85a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -401,8 +401,6 @@ class TestImage: def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 - extension = Image.EXTENSION - Image.EXTENSION = {} # Act Image.registered_extensions() @@ -410,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 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c2216e27a..6288f46ef 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3418,8 +3418,7 @@ def registered_extensions(): Returns a dictionary containing all file extensions belonging to registered plugins """ - if not EXTENSION: - init() + init() return EXTENSION From 88e127d1b27f6dfbc6ddfe4ffba13748e89f4d11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 22:16:07 +0000 Subject: [PATCH 118/137] Update actions/stale action to v7 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From a065e0252b563b4d7a490ddd1a1eb7c8089662c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 09:29:25 +1100 Subject: [PATCH 119/137] Updated deprecated NumPy alias --- Tests/test_numpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From d6e79045280be42cf2273716b18d82661cf7f779 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 12:47:46 +1100 Subject: [PATCH 120/137] Removed Python 3.7 on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index f297eb1b5..7b8070d34 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-minor-version: [7, 8, 9] + python-minor-version: [8, 9] timeout-minutes: 40 From 967034356a72d02e4cddad5ac4b6c75299d08394 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 14:20:47 +1100 Subject: [PATCH 121/137] Fixed BytesWarning --- src/PIL/PpmImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 392771d3e..1670d9d64 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -208,7 +208,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError(f"Invalid token for this mode: {bytes([token])}") + raise ValueError( + b"Invalid token for this mode: %s" % bytes([token]) + ) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -242,13 +244,13 @@ class PpmPlainDecoder(ImageFile.PyDecoder): half_token = tokens.pop() # save half token for later if len(half_token) > 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: From 1df7e75205247ae3ef021a623659d532bd5a4f15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 06:52:06 +1100 Subject: [PATCH 122/137] Python 3.7 on Cygwin is no longer part of CI --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index b559c824d..b188020b9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -460,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 | +----------------------------------+----------------------------+---------------------+ From a4ac40354916401063028fc9af402e830eaf8606 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 07:14:02 +1100 Subject: [PATCH 123/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1e5f71b86..04b3fc4c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- 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] From 9898613c4d276f8065d5e3bbb5fda7f4715e90d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 15:31:36 +1100 Subject: [PATCH 124/137] Fixed saving EXIF data to MPO --- Tests/test_file_mpo.py | 5 ++++- src/PIL/MpoImagePlugin.py | 14 +++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index dba1ec1b1..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 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 3ae4d4abf..095cfe7ee 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -52,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"] = ( + 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: @@ -79,11 +87,11 @@ def _save_all(im, fp, filename): mptype = 0x000000 # Undefined mpentries += struct.pack(" Date: Thu, 22 Dec 2022 17:16:52 +1100 Subject: [PATCH 125/137] Initialize unsigned char variables --- src/libImaging/Quant.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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]; From 88f15eb9f07b0434a6b2831b02d402dd4efdee6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 12:10:36 +1100 Subject: [PATCH 126/137] Do not save EXIF from info --- Tests/test_file_png.py | 12 ++++++++++-- src/PIL/PngImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) 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/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2c53be109..b6a3c4cb6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1383,7 +1383,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif", im.info.get("exif")) + exif = im.encoderinfo.get("exif") if exif: if isinstance(exif, Image.Exif): exif = exif.tobytes(8) From 9e6a7d974084a4d7b6be9d68b732558194d20e51 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 17:43:18 +1100 Subject: [PATCH 127/137] Added support for uncompressed L images --- Tests/images/uncompressed_l.dds | Bin 0 -> 16512 bytes Tests/images/uncompressed_l.png | Bin 0 -> 861 bytes Tests/test_file_dds.py | 12 +++++++++-- src/PIL/DdsImagePlugin.py | 35 ++++++++++++++++++++++++-------- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 Tests/images/uncompressed_l.dds create mode 100644 Tests/images/uncompressed_l.png diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds new file mode 100644 index 0000000000000000000000000000000000000000..b82282587ec30665fceafced278f1c59b3402ed1 GIT binary patch literal 16512 zcmeH}-EHGA5QLRH-P>I{2-3o};0}Uxa}TK}#ia!we>Mbmxid@IFa*O(Adcw~@eMx; zf=;LR*MHl#{rikdie4wCtD@J5$rZg$Odd=pyeTl@O@Rr&tAImS3LLsp;L!Id0QjK*;D-W$ zUsiB1AL2y-+`b5a+g}qv@T~yBw*myeRN!Df#TNl|`$YiV{(^u9=Lg$A2l~GQ{ow&5 zpBqU3+`zrxe;YskeE#v{zyA5p51_m@(gG!?cVO_^Sz~$wl>F9wR-n}<1zJu7v^@NP z24p2HAUP}$lKTZm^U(>6`arZRRKVo%R5fre zR}Gw8HE{BUpFaA4SDyNS`QJWZP6L6%3Ic}}1b$%!MXwXnRnhCjXRluPu1rA*)aOis!0Q^t@@IwK>FDp2h5Ah-ZZeIkz?XL+S_*Q`6TLFS!DsV8L z;)?*f{UU&Fe?h>5^Mmbw1o}^b{_p^j&kdx1Zs6YUzl|S%KL7afU;q5)2T)!cX@Qc{ zJ1}_dtTDX-N`7lTE70nb0xc&3S{{Bt1F{kpkQ^2W$^C+(`RD{jeIQ`uK)}f3FOCAW z$Z4Q*r-91D$yC6smsv0<#s|HT48aVmFPj7v|D_{D6`BNV-r-8s>1%bl~ z0>7|=qSuM(s_1oMaz(EblLylYZwgF!Q((gHD&WwS0*9^?IP^UV0DdR{_@MycmlYh$ zhjsv0<#s|HT4 t8aVmFPf`J1DHUKY6=0r08b||aAPuB}G>`_;KpIE`X&?=xfi!T4f&Vw%wVD6` literal 0 HcmV?d00001 diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png new file mode 100644 index 0000000000000000000000000000000000000000..9d22a26a446d3dbdfd8f9c931ea466f6c6424e90 GIT binary patch literal 861 zcmeAS@N?(olHy`uVBq!ia0vp^4Is<`Bp9BB+KDqTFspdFIEGZrc^h?ls*<5Vur)JB zqlg-7P(pKyh(}`Z41qR*8_gT%*K1u(y=E67zCX-RIr5$RbYFRmk9~(3o90{0iTo?q zZokIPVc}P`n#F#$cTRI}Z`b*cHdxb-Qkj7%xN9XC&Z5~v}sUc zQF_LyDDi8i`mdSklfSkId~vT6XS8VuNn~8YHGhS2Q^L#RQrG)UyL@3X&}x&GB>;Tib{;+mb; z9E?oS*}aKX$Uxa5;eK2CjmcMXGaH^hIH47DKd5laQEeT@eua-ZO-D9&R()gjOuir% zvW=}bGBx``)z5EfnVi1_vX=2LOsg_|ey?EeT|36sB|Uh!dQU<13AVi4r5g)!U-!1I-}Y|HguFw3`xCAHw!QqqenxjbTuMSUDiOXag;?62vFzD;OU-N3|GNKs$H|s>F&hgWVD=Cdb6BTa*mH8 zSvtelvqpRLzjllcxGA-Tb?VxK$@jOLdwX91dKc*H#c{mLBht%bs(#FW&Mb`zE`Q|r X-moxli&n>DP`>eW^>bP0l+XkK*RXjv literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 4b9f8949e..f579cd1c2 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -22,6 +22,7 @@ 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_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -194,8 +195,14 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed_rgb(): - """Check uncompressed RGB images can be opened""" +def test_uncompressed(): + """Check uncompressed images can be opened""" + with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: @@ -305,6 +312,7 @@ def test_save_unsupported_mode(tmp_path): @pytest.mark.parametrize( ("mode", "test_file"), [ + ("L", "Tests/images/linear_gradient.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eea6e3153..b78cc649f 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -135,7 +135,12 @@ class DdsImageFile(ImageFile.ImageFile): fourcc = header.read(4) (bitcount,) = struct.unpack(" Date: Mon, 8 Aug 2022 02:24:55 +0300 Subject: [PATCH 128/137] Add missing LA test textures --- Tests/images/la.dds | Bin 0 -> 32896 bytes Tests/images/la.png | Bin 0 -> 1060 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/images/la.dds create mode 100644 Tests/images/la.png diff --git a/Tests/images/la.dds b/Tests/images/la.dds new file mode 100644 index 0000000000000000000000000000000000000000..30bf93576fd17f80397a1a016c3ee2306e4bf28a GIT binary patch literal 32896 zcmeI4;f>T#420j&0MG%V1zNZc5Z&;DdMJ(-j@^xtFtTUtJV%OdaU|H-`0+4X(6pEBpi3mQLUN zhAVA5p%B0!$UPjuA;|dw9Djv6iQ(a(_u$>I1Ojy6clnLvvWhAUCVm}G%is+ZHu896>c61SC68$csOrpOQNl+^8 zJtZg=cfSOs;_k2AQNU1G`clA9So{<)6l&b7JH(^~&%jUNQoik;evw#x!ROi zq3`pbTr({T)*H&H70aUD|CJNy-#_6Imjze<{;7TgzW@DKCa}DDa=}tu{POEfinTX? zb<;2RP!+ZuVEt!g0Fef~lfiTo-0z~AB=^@93cU%shYJiz?KiL{q5Ws5 z8>+dGpL4Ya?ey1*vMd$_D29ehEs&-Cw(-fT6JTrGTNZ_$gp0 z)VNo7h)D~cfuF*qgfYS;%Yre&CCh?!hx4B)Oj^|Za=oE=_5SH%lR^j>6v)G!66a6; z-I>IsWdU9&rx0aP^H(N77n>Bq1qTIkwJEVe-{(KMW?B}kH^rdg<&vqm`|Ee% zs!aj*l*il^(N7_%fc|P^@WKRkbP;6|{S=c5=&wg1FcjE+64r>;PeEP5`p?J!A`N&a zgXt!?-$gY^?yoBpdJ}XH7Z{S-Z(vPA`_E7}RCAMiI^aT(^Bd$Xk@KHZ^%bt;&-@J6 zasLZk$NlpT0EZy=OaO-<=Lc{Ia{fvc;JSf+I^ep2_5-dPXn#$0flZS4aDh#d`(0p@ z<>kY-L_fHp_6hgqDpmg{j=J}Ih@nI|F6h(W=Tw17T|?)^H(RZZgTdMt~Mp~@1O99%Yv(a|5Segy#M`ICa}D7a=}tu z{PW)cv+q3mQZKq&^c?)NCt#nVTrw4R|J(`K{q;L{y)ubCDI^upe|!SyuSOEp6|p15 zqyqY{=0Exl;J^MJQBYT~{$mrc{xdS}qD_)_a#2l^`*Snk{<>mdO+xoDuqL7X@dUL0 z4AmfSiQLl-@|MW?`3!RYbE-Dz$IbC)ZqSdL<9?jNaeq5{0zHAAKu@42&=cqh^aOeW tJ%OG;PoO8z6X*%_1bPBJfu2B5peN81=n3=$dICLxow)~&Z0 zH5EtM_}-^3Hu{@?P>lS=P97Ps&2Y`uaavk;f+X z|NQ$jeEDRv|F65_YwNN;r`k*j^g*GU zZrQreW}1D!B^c{>PdvO!Q}DcywYc$d-YXYC%pI{Ec)v#0Q1dpzO) z>}bXbHv4f(YB%FVzfuC*mB;y3?5j z1Y_6l`*Zr}^!K%%U%v1;Fm4i`z{@pZZ$nN1!{0UQ-Rxhl{Qn3%@>4BHFG zvO)N_WU1bT6BB&dj~$)VdmKdUP~ty6OIP9JK^?b8&5=DWQEzT4__ZJBudVs8`g7#( zEAQX`@v{F_RsA^p{{34ulKk8O-p@~*f)PiiFVsE|QIi7_fuGYeL8Um$eIo#hy0rW-j^kyJGGiySUreHRK;G{qW88_J1>XAU=NS zyafLm7qg!>=auF^k6h|9g(3RDz4}GE*KdJ{%kKNxA2hsSP1#&w#xU{!KZg8$-&HSO zwq{@j1*3+;H>ORFj9)hIlX{<mc)r>dSOl$mp?&q7^EYrKsANhAsLdGHG zJ20rK7CedkEnz6J*YW@;s5BYRZ}jM#$(_tSC4Y-t#^bFZ{I|zSJfrVe%H_twjVX^e z0tw}HCk}5+m~ShZQl<%b`g*RTKc>bd*xEBgAN@LH9B?8Cq8^B=a` z{7TfFtE}w|A`btsoASe`WPg^sa;d+23pTn#+UJ zi>uD)Er=RDdV4Fvzpy&-gjnM+dDD|ps Date: Fri, 23 Dec 2022 19:07:45 +1100 Subject: [PATCH 129/137] Added support for uncompressed LA images --- Tests/images/{la.dds => uncompressed_la.dds} | Bin Tests/images/{la.png => uncompressed_la.png} | Bin Tests/test_file_dds.py | 40 ++++++++----------- src/PIL/DdsImagePlugin.py | 31 +++++++------- 4 files changed, 34 insertions(+), 37 deletions(-) rename Tests/images/{la.dds => uncompressed_la.dds} (100%) rename Tests/images/{la.png => uncompressed_la.png} (100%) diff --git a/Tests/images/la.dds b/Tests/images/uncompressed_la.dds similarity index 100% rename from Tests/images/la.dds rename to Tests/images/uncompressed_la.dds diff --git a/Tests/images/la.png b/Tests/images/uncompressed_la.png similarity index 100% rename from Tests/images/la.png rename to Tests/images/uncompressed_la.png diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index f579cd1c2..cac4108a8 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -23,6 +23,7 @@ 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" @@ -195,32 +196,24 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed(): +@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""" - with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: + + with Image.open(test_file) as im: assert im.format == "DDS" - assert im.mode == "L" - assert im.size == (128, 128) + assert im.mode == mode + assert im.size == size - assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") - - # convert -format dds -define dds:compression=none hopper.jpg hopper.dds - with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (128, 128) - - 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(): @@ -313,6 +306,7 @@ def test_save_unsupported_mode(tmp_path): ("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/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b78cc649f..f78c8b17c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -136,15 +136,18 @@ class DdsImageFile(ImageFile.ImageFile): (bitcount,) = struct.unpack(" Date: Fri, 23 Dec 2022 23:20:06 +1100 Subject: [PATCH 130/137] Clear pyaccess after re-assigning im --- Tests/test_file_ico.py | 13 +++++++++++++ src/PIL/IcoImagePlugin.py | 1 + 2 files changed, 14 insertions(+) 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/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 17b9855a0..93b9dfdea 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -327,6 +327,7 @@ class IcoImageFile(ImageFile.ImageFile): # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im + self.pyaccess = None self.mode = im.mode if im.size != self.size: warnings.warn("Image was not the expected size") From 8bd5fbf450f9f5a03a32c7efcfb488bfc2a40d1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 07:32:09 +1100 Subject: [PATCH 131/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 04b3fc4c6..3e409fe64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- 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] From 5c482e20af8efe0210cd7b0cfe2dec7367d03042 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:32:58 +1100 Subject: [PATCH 132/137] Document new ExifTags enums --- docs/reference/ExifTags.rst | 10 +++++++++- docs/releasenotes/9.4.0.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 650bb4f95..464ab77ea 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -37,7 +37,15 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> IFD.Exif.value 34665 >>> IFD(34665).name - 'Exif' + '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. diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6b..7da0e61f3 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,34 @@ 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() ^^^^^^^^ From 941a2d60b28c32f1193ef2f9627fc80f9279802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:41:57 +1100 Subject: [PATCH 133/137] Added release notes --- docs/releasenotes/9.4.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6b..0068f2816 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -70,7 +70,8 @@ TODO Other Changes ============= -TODO -^^^^ +Added support for DDS L and LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Support has been added to read and write L and LA DDS images in the uncompressed +format, known as "luminance" textures. From 426ac9c1fe085d78d501c7143039b83a23eeac3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 14:19:32 +1100 Subject: [PATCH 134/137] Updated libtiff to 4.5.0 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index b188020b9..42fe8c254 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -143,7 +143,7 @@ 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 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a1908e35e..0b0c782a0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -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 From d2590437c4f90b1f6837f951223e18923c4d3467 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 16:21:45 +1100 Subject: [PATCH 135/137] Updated libtiff shared library name --- Tests/oss-fuzz/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index b459ee47a..7e9098f53 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -25,7 +25,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --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:. \ From 08816f43ae621830cd4cf9dc1fecfbae63e5cc60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 15:46:14 +1100 Subject: [PATCH 136/137] Added support for I;16 modes in putdata() --- Tests/test_image_putdata.py | 5 +++-- src/_imaging.c | 30 +++++++++++++----------------- 2 files changed, 16 insertions(+), 19 deletions(-) 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/src/_imaging.c b/src/_imaging.c index 940b5fbb3..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 */ From 2755e0ffaadc8b29c3e67e223c333c50e197a733 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 19:24:41 +1100 Subject: [PATCH 137/137] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e409fe64..76fc230a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ 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]