diff --git a/.editorconfig b/.editorconfig
index 449530717..d74549fe2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,10 +13,6 @@ indent_style = space
trim_trailing_whitespace = true
-[*.rst]
-# Four-space indentation
-indent_size = 4
-
[*.yml]
# Two-space indentation
indent_size = 2
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index ba2b7d8ed..d03fcf0d9 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
+- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
## Reporting Issues
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 9a1e46705..e7ab6466e 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -104,7 +104,7 @@ jobs:
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
- SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
+ .ci/build.sh
- name: Test
run: |
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 4f01abe44..3bcb8cfbc 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -39,6 +39,7 @@ jobs:
centos-stream-8-amd64,
centos-stream-9-amd64,
debian-11-bullseye-x86,
+ debian-12-bookworm-x86,
fedora-37-amd64,
fedora-38-amd64,
gentoo,
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index a109ec0d8..5a737a1ee 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -80,7 +80,7 @@ jobs:
pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow
- run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
+ run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install .
- name: Test Pillow
run: |
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index fbfec8c13..3bfd54ee3 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -28,10 +28,10 @@ jobs:
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
- - python-version: "pypy3.8"
- architecture: "x64"
- python-version: "pypy3.9"
architecture: "x64"
+ - python-version: "pypy3.10"
+ architecture: "x64"
timeout-minutes: 30
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index afb8fb56c..893c0d12c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -29,8 +29,8 @@ jobs:
"ubuntu-latest",
]
python-version: [
+ "pypy3.10",
"pypy3.9",
- "pypy3.8",
"3.12-dev",
"3.11",
"3.10",
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f4b695883..79966503f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,9 +4,6 @@ repos:
hooks:
- id: black
args: [--target-version=py38]
- # Only .py files, until https://github.com/psf/black/issues/402 resolved
- files: \.py$
- types: []
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
diff --git a/CHANGES.rst b/CHANGES.rst
index 190751ad2..c05268772 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,42 @@ Changelog (Pillow)
10.0.0 (unreleased)
-------------------
+- Fixed finding dependencies on Cygwin #7175
+ [radarhere]
+
+- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219
+ [abey79, radarhere]
+
+- Added in_place argument to ImageOps.exif_transpose() #7092
+ [radarhere]
+
+- Fixed calling putpalette() on L and LA images before load() #7187
+ [radarhere]
+
+- Fixed saving TIFF multiframe images with LONG8 tag types #7078
+ [radarhere]
+
+- Fixed combining single duration across duplicate APNG frames #7146
+ [radarhere]
+
+- Remove temporary file when error is raised #7148
+ [radarhere]
+
+- Do not use temporary file when grabbing clipboard on Linux #7200
+ [radarhere]
+
+- If the clipboard fails to open on Windows, wait and try again #7141
+ [radarhere]
+
+- Fixed saving multiple 1 mode frames to GIF #7181
+ [radarhere]
+
+- Replaced absolute PIL import with relative import #7173
+ [radarhere]
+
+- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192
+ [radarhere]
+
- Improved wl-paste mimetype handling in ImageGrab #7094
[rrcgat, radarhere]
diff --git a/MANIFEST.in b/MANIFEST.in
index f51551303..606e7e074 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -15,6 +15,7 @@ graft src
graft depends
graft winbuild
graft docs
+graft _custom_build
# build/src control detritus
exclude .appveyor.yml
diff --git a/Makefile b/Makefile
index e41f36411..57d756b47 100644
--- a/Makefile
+++ b/Makefile
@@ -46,7 +46,6 @@ help:
@echo " docserve run an HTTP server on the docs directory"
@echo " html make HTML docs"
@echo " htmlview open the index page built by the html target in your browser"
- @echo " inplace make inplace extension"
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks"
@@ -54,10 +53,6 @@ help:
@echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow"
-.PHONY: inplace
-inplace: clean
- python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
-
.PHONY: install
install:
python3 -m pip -v install .
@@ -65,7 +60,7 @@ install:
.PHONY: install-coverage
install-coverage:
- CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" .
+ CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install .
python3 selftest.py
.PHONY: debug
@@ -74,7 +69,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 -v install --global-option="build_ext" . > /dev/null
+ CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null
.PHONY: release-test
release-test:
diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py
index 87cad699d..69ebef9b4 100644
--- a/Tests/bench_cffi_access.py
+++ b/Tests/bench_cffi_access.py
@@ -27,25 +27,19 @@ def timer(func, label, *args):
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
- print(
- "{}: breaking at {} iterations, {:.6f} per iteration".format(
- label, x + 1, (time.time() - starttime) / (x + 1.0)
- )
- )
break
- if x == iterations - 1:
- endtime = time.time()
- print(
- "{}: {:.4f} s {:.6f} per iteration".format(
- label, endtime - starttime, (endtime - starttime) / (x + 1.0)
- )
+ endtime = time.time()
+ print(
+ "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
+ label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
)
+ )
def test_direct():
im = hopper()
im.load()
- # im = Image.new( "RGB", (2000, 2000), (1, 3, 2))
+ # im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf
new file mode 100644
index 000000000..fe200842e
Binary files /dev/null and b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf differ
diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg
new file mode 100644
index 000000000..85cfbd0a8
Binary files /dev/null and b/Tests/images/orientation_rectangle.jpg differ
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index f78c086eb..a22ac581d 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -447,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info.get("duration") == 750
+def test_apng_save_duplicate_duration(tmp_path):
+ test_file = str(tmp_path / "temp.png")
+ frame = Image.new("RGB", (1, 1))
+
+ # Test a single duration is correctly combined across duplicate frames
+ frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
+ with Image.open(test_file) as im:
+ assert im.n_frames == 1
+ assert im.info.get("duration") == 1500
+
+
def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png")
size = (128, 64)
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 8522f486a..0e50ee1ab 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5
+def test_roundtrip_save_all_1(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("1", (1, 1))
+ im2 = Image.new("1", (1, 1), 1)
+ im.save(out, save_all=True, append_images=[im2])
+
+ with Image.open(out) as reloaded:
+ assert reloaded.getpixel((0, 0)) == 0
+
+ reloaded.seek(1)
+ assert reloaded.getpixel((0, 0)) == 255
+
+
@pytest.mark.parametrize(
"path, mode",
(
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 30c6303a2..f13436ce8 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -96,10 +96,17 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
- def test_bigtiff(self):
+ def test_bigtiff(self, tmp_path):
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
+ with Image.open("Tests/images/hopper_bigtiff.tif") as im:
+ # multistrip support not yet implemented
+ del im.tag_v2[273]
+
+ outfile = str(tmp_path / "temp.tif")
+ im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
+
def test_set_legacy_api(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index 3b29769a7..665e08a7e 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -32,6 +32,14 @@ def test_putpalette():
with pytest.raises(ValueError):
palette("YCbCr")
+ with Image.open("Tests/images/hopper_gray.jpg") as im:
+ assert im.mode == "L"
+ im.putpalette(list(range(256)) * 3)
+
+ with Image.open("Tests/images/la.tga") as im:
+ assert im.mode == "LA"
+ im.putpalette(list(range(256)) * 3)
+
def test_imagepalette():
im = hopper("P")
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 7ea485a55..7fa8ff8cb 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -463,6 +463,11 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png")
+@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
+def test_getbbox(font, mode):
+ assert (0, 4, 12, 16) == font.getbbox("A", mode)
+
+
def test_getbbox_empty(font):
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")
@@ -1037,6 +1042,7 @@ def test_render_mono_size():
"test_file",
[
"Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
+ "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
],
)
def test_oom(test_file):
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index d390f3c1e..b05785be0 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -404,6 +404,18 @@ def test_exif_transpose():
assert 0x0112 not in transposed_im.getexif()
+def test_exif_transpose_in_place():
+ with Image.open("Tests/images/orientation_rectangle.jpg") as im:
+ assert im.size == (2, 1)
+ assert im.getexif()[0x0112] == 8
+ expected = im.rotate(90, expand=True)
+
+ ImageOps.exif_transpose(im, in_place=True)
+ assert im.size == (1, 2)
+ assert 0x0112 not in im.getexif()
+ assert_image_equal(im, expected)
+
+
def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
diff --git a/_custom_build/backend.py b/_custom_build/backend.py
new file mode 100755
index 000000000..9b3265a94
--- /dev/null
+++ b/_custom_build/backend.py
@@ -0,0 +1,56 @@
+import sys
+
+from setuptools.build_meta import * # noqa: F401, F403
+from setuptools.build_meta import build_wheel
+
+backend_class = build_wheel.__self__.__class__
+
+
+class _CustomBuildMetaBackend(backend_class):
+ def run_setup(self, setup_script="setup.py"):
+ if self.config_settings:
+
+ def config_has(key, value):
+ settings = self.config_settings.get(key)
+ if settings:
+ if not isinstance(settings, list):
+ settings = [settings]
+ return value in settings
+
+ flags = []
+ for dependency in (
+ "zlib",
+ "jpeg",
+ "tiff",
+ "freetype",
+ "raqm",
+ "lcms",
+ "webp",
+ "webpmux",
+ "jpeg2000",
+ "imagequant",
+ "xcb",
+ ):
+ if config_has(dependency, "enable"):
+ flags.append("--enable-" + dependency)
+ elif config_has(dependency, "disable"):
+ flags.append("--disable-" + dependency)
+ for dependency in ("raqm", "fribidi"):
+ if config_has(dependency, "vendor"):
+ flags.append("--vendor-" + dependency)
+ if self.config_settings.get("platform-guessing") == "disable":
+ flags.append("--disable-platform-guessing")
+ if self.config_settings.get("debug") == "true":
+ flags.append("--debug")
+ if flags:
+ sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:]
+ return super().run_setup(setup_script)
+
+ def build_wheel(
+ self, wheel_directory, config_settings=None, metadata_directory=None
+ ):
+ self.config_settings = config_settings
+ return super().build_wheel(wheel_directory, config_settings, metadata_directory)
+
+
+build_wheel = _CustomBuildMetaBackend().build_wheel
diff --git a/docs/Guardfile b/docs/Guardfile
index b689b079a..6cbf07b06 100755
--- a/docs/Guardfile
+++ b/docs/Guardfile
@@ -2,7 +2,7 @@
from livereload.compiler import shell
from livereload.task import Task
-Task.add('*.rst', shell('make html'))
-Task.add('*/*.rst', shell('make html'))
-Task.add('Makefile', shell('make html'))
-Task.add('conf.py', shell('make html'))
+Task.add("*.rst", shell("make html"))
+Task.add("*/*.rst", shell("make html"))
+Task.add("Makefile", shell("make html"))
+Task.add("conf.py", shell("make html"))
diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst
index e40ed4687..e0975a121 100644
--- a/docs/handbook/concepts.rst
+++ b/docs/handbook/concepts.rst
@@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel
corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5).
Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles
-are represented as 4-tuples, with the upper left corner given first. For
-example, a rectangle covering all of an 800x600 pixel image is written as (0,
-0, 800, 600).
+are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given
+first.
Palette
-------
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 74ba883b1..bbcf48e42 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1380,6 +1380,12 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
+QOI
+^^^
+
+.. versionadded:: 9.5.0
+
+Pillow identifies and reads images in Quite OK Image format.
SUN
^^^
@@ -1562,13 +1568,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 5.3.0
-QOI
-^^^
-
-.. versionadded:: 9.5.0
-
-Pillow identifies and reads images in Quite OK Image format.
-
XV Thumbnails
^^^^^^^^^^^^^
diff --git a/docs/installation.rst b/docs/installation.rst
index ad27b67ee..dcb3f2301 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -312,6 +312,11 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm
+ https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
+ MSYS2. To workaround this, before installing Pillow you must run::
+
+ export SETUPTOOLS_USE_DISTUTILS=stdlib
+
.. tab:: FreeBSD
.. Note:: Only FreeBSD 10 and 11 tested
@@ -380,40 +385,40 @@ Build Options
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-raqm``,
- ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``,
- ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``.
+* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
+ ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
+ ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``,
+ ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
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-raqm``,
- ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``,
- ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``.
+* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
+ ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
+ ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``,
+ ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
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``.
+* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
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
+* Build flag: ``-C platform-guessing=disable``. 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
+* Build flag: ``-C debug=true``. 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]"
+ python3 -m pip install --upgrade Pillow -C [feature]=enable
Platform Support
----------------
@@ -448,6 +453,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
+| Debian 12 Bookworm | 3.11 | x86 |
++----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 524f821fb..31f63695e 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -328,7 +328,7 @@ Methods
.. versionadded:: 5.3.0
-.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)
+.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None)
Draws a rounded rectangle.
@@ -341,6 +341,7 @@ Methods
:param width: The line width, in pixels.
:param corners: A tuple of whether to round each corner,
``(top_left, top_right, bottom_right, bottom_left)``.
+ Keyword-only argument.
.. versionadded:: 8.2.0
diff --git a/pyproject.toml b/pyproject.toml
index 59eb08fa9..1d32e7d90 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,4 @@
[build-system]
-build-backend = "setuptools.build_meta"
-requires = [
- "setuptools>=67.8",
-]
+requires = ["setuptools >= 67.8", "wheel"]
+build-backend = "backend"
+backend-path = ["_custom_build"]
diff --git a/setup.py b/setup.py
index 522c20991..e2ed3b315 100755
--- a/setup.py
+++ b/setup.py
@@ -515,6 +515,7 @@ class pil_build_ext(build_ext):
elif sys.platform == "cygwin":
# pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory
+ self.compiler.shared_lib_extension = ".dll.a"
_add_directory(
library_dirs,
os.path.join(
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 1c88d22c7..6b1b5947e 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
if gs_windows_binary is not None:
if not gs_windows_binary:
+ try:
+ os.unlink(outfile)
+ if infile_temp:
+ os.unlink(infile_temp)
+ except OSError:
+ pass
+
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
command[0] = gs_windows_binary
@@ -354,7 +361,6 @@ class EpsImageFile(ImageFile.ImageFile):
check_required_header_comments()
if not self._size:
- self._size = 1, 1 # errors if this isn't set. why (1,1)?
msg = "cannot determine EPS bounding box"
raise OSError(msg)
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index 7dda4f143..bafc43a19 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -47,7 +47,7 @@ class GdImageFile(ImageFile.ImageFile):
# Header
s = self.fp.read(1037)
- if not i16(s) in [65534, 65535]:
+ if i16(s) not in [65534, 65535]:
msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index eadee1560..2f92e9467 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -879,7 +879,7 @@ def _get_palette_bytes(im):
:param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header
"""
- return im.palette.palette
+ return im.palette.palette if im.palette else b""
def _get_background(im, info_background):
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index c2f050edd..27cb89f73 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -22,11 +22,11 @@ import os
import struct
import sys
-from PIL import Image, ImageFile, PngImagePlugin, features
+from . import Image, ImageFile, PngImagePlugin, features
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
- from PIL import Jpeg2KImagePlugin
+ from . import Jpeg2KImagePlugin
MAGIC = b"icns"
HEADERSIZE = 8
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index e0fb6a885..97f3f4926 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1254,7 +1254,7 @@ class Image:
if ymargin is None:
ymargin = xmargin
self.load()
- return self._new(self.im.expand(xmargin, ymargin, 0))
+ return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter):
"""
@@ -1433,12 +1433,12 @@ class Image:
self._exif.load(exif_info)
# XMP tags
- if 0x0112 not in self._exif:
+ if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
- self._exif[0x0112] = int(match[2])
+ self._exif[ExifTags.Base.Orientation] = int(match[2])
return self._exif
@@ -1731,7 +1731,7 @@ class Image:
if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple"
raise ValueError(msg)
- if not len(source) in (2, 4):
+ if len(source) not in (2, 4):
msg = "Source must be a 2 or 4-tuple"
raise ValueError(msg)
if not len(dest) == 2:
@@ -2451,8 +2451,8 @@ class Image:
The image is first saved to a temporary file. By default, it will be in
PNG format.
- On Unix, the image is then opened using the **display**, **eog** or
- **xv** utility, depending on which one can be found.
+ On Unix, the image is then opened using the **xdg-open**, **display**,
+ **gm**, **eog** or **xv** utility, depending on which one can be found.
On macOS, the image is opened with the native Preview application.
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 38cbab19c..3a337f9f2 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -18,10 +18,10 @@
import sys
from enum import IntEnum
-from PIL import Image
+from . import Image
try:
- from PIL import _imagingcms
+ from . import _imagingcms
except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing
# anything in core.
@@ -271,7 +271,7 @@ def get_display_profile(handle=None):
if sys.platform != "win32":
return None
- from PIL import ImageWin
+ from . import ImageWin
if isinstance(handle, ImageWin.HDC):
profile = core.get_display_profile_win32(handle, 1)
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 63d6dcf5c..33bc7cc2e 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter):
class Kernel(BuiltinFilter):
"""
- Create a convolution kernel. The current version only
+ Create a convolution kernel. The current version only
supports 3x3 and 5x5 integer and floating point kernels.
In the current version, kernels can only be applied to
@@ -43,9 +43,10 @@ class Kernel(BuiltinFilter):
:param size: Kernel size, given as (width, height). In the current
version, this must be (3,3) or (5,5).
- :param kernel: A sequence containing kernel weights.
+ :param kernel: A sequence containing kernel weights. The kernel will
+ be flipped vertically before being applied to the image.
:param scale: Scale factor. If given, the result for each pixel is
- divided by this value. The default is the sum of the
+ divided by this value. The default is the sum of the
kernel weights.
:param offset: Offset. If given, this value is added to the result,
after it has been divided by the scale factor.
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index ea4549cf5..3ddc1aaad 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -26,7 +26,6 @@
#
import base64
-import math
import os
import sys
import warnings
@@ -226,10 +225,6 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine)
- def _multiline_split(self, text):
- split_character = "\n" if isinstance(text, str) else b"\n"
- return text.split(split_character)
-
def getname(self):
"""
:return: A tuple of the font family (e.g. Helvetica) and the font style
@@ -551,28 +546,23 @@ class FreeTypeFont:
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
"""
- size, offset = self.font.getsize(
- text, mode, direction, features, language, anchor
- )
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
+ im, size, offset = self.font.render(
+ text,
+ Image.core.fill,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ anchor,
+ ink,
+ start[0],
+ start[1],
+ Image.MAX_IMAGE_PIXELS,
+ )
Image._decompression_bomb_check(size)
- im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0)
- if min(size):
- self.font.render(
- text,
- im.id,
- mode,
- direction,
- features,
- language,
- stroke_width,
- ink,
- start[0],
- start[1],
- )
return im, offset
def font_variant(
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 7f6d50af4..927033c60 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#
+import io
import os
import shutil
import subprocess
@@ -94,14 +95,14 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
def grabclipboard():
if sys.platform == "darwin":
- fh, filepath = tempfile.mkstemp(".jpg")
+ fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
commands = [
'set theFile to (open for access POSIX file "'
+ filepath
+ '" with write permission)',
"try",
- " write (the clipboard as JPEG picture) to theFile",
+ " write (the clipboard as «class PNGf») to theFile",
"end try",
"close access theFile",
]
@@ -128,8 +129,6 @@ def grabclipboard():
files = data[o:].decode("mbcs").split("\0")
return files[: files.index("")]
if isinstance(data, bytes):
- import io
-
data = io.BytesIO(data)
if fmt == "png":
from . import PngImagePlugin
@@ -159,13 +158,12 @@ def grabclipboard():
else:
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg)
- fh, filepath = tempfile.mkstemp()
- err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr
- os.close(fh)
+ p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ err = p.stderr
if err:
msg = f"{args[0]} error: {err.strip().decode()}"
raise ChildProcessError(msg)
- im = Image.open(filepath)
+ data = io.BytesIO(p.stdout)
+ im = Image.open(data)
im.load()
- os.unlink(filepath)
return im
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 301c593c7..17702778c 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,7 +21,7 @@ import functools
import operator
import re
-from . import Image, ImagePalette
+from . import ExifTags, Image, ImagePalette
#
# helpers
@@ -576,19 +576,20 @@ def solarize(image, threshold=128):
return _lut(image, lut)
-def exif_transpose(image):
+def exif_transpose(image, *, in_place=False):
"""
- If an image has an EXIF Orientation tag, other than 1, return a new image
- that is transposed accordingly. The new image will have the orientation
- data removed.
-
- Otherwise, return a copy of the image.
+ If an image has an EXIF Orientation tag, other than 1, transpose the image
+ accordingly, and remove the orientation data.
:param image: The image to transpose.
- :return: An image.
+ :param in_place: Boolean. Keyword-only argument.
+ If ``True``, the original image is modified in-place, and ``None`` is returned.
+ If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
+ with the transposition applied. If there is no transposition, a copy of the
+ image will be returned.
"""
- exif = image.getexif()
- orientation = exif.get(0x0112)
+ image_exif = image.getexif()
+ orientation = image_exif.get(ExifTags.Base.Orientation)
method = {
2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180,
@@ -600,22 +601,28 @@ def exif_transpose(image):
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
- transposed_exif = transposed_image.getexif()
- if 0x0112 in transposed_exif:
- del transposed_exif[0x0112]
- if "exif" in transposed_image.info:
- transposed_image.info["exif"] = transposed_exif.tobytes()
- elif "Raw profile type exif" in transposed_image.info:
- transposed_image.info[
- "Raw profile type exif"
- ] = transposed_exif.tobytes().hex()
- elif "XML:com.adobe.xmp" in transposed_image.info:
+ if in_place:
+ image.im = transposed_image.im
+ image.pyaccess = None
+ image._size = transposed_image._size
+ exif_image = image if in_place else transposed_image
+
+ exif = exif_image.getexif()
+ if ExifTags.Base.Orientation in exif:
+ del exif[ExifTags.Base.Orientation]
+ if "exif" in exif_image.info:
+ exif_image.info["exif"] = exif.tobytes()
+ elif "Raw profile type exif" in exif_image.info:
+ exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
+ elif "XML:com.adobe.xmp" in exif_image.info:
for pattern in (
r'tiff:Orientation="([0-9])"',
r"([0-9])",
):
- transposed_image.info["XML:com.adobe.xmp"] = re.sub(
- pattern, "", transposed_image.info["XML:com.adobe.xmp"]
+ exif_image.info["XML:com.adobe.xmp"] = re.sub(
+ pattern, "", exif_image.info["XML:com.adobe.xmp"]
)
- return transposed_image
- return image.copy()
+ if not in_place:
+ return transposed_image
+ elif not in_place:
+ return image.copy()
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 3f68a2696..8b1c3f8bb 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -17,7 +17,7 @@ import subprocess
import sys
from shlex import quote
-from PIL import Image
+from . import Image
_viewers = []
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 5dd1a61af..dfc7e6e9f 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile):
if os.path.exists(self.filename):
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
else:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+
msg = "Invalid Filename"
raise ValueError(msg)
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 82a74b267..aaf242b1d 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
):
- if isinstance(duration, (list, tuple)):
- previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+ previous["encoderinfo"]["duration"] += encoderinfo.get(
+ "duration", duration
+ )
continue
else:
bbox = None
+ if "duration" not in encoderinfo:
+ encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
# animation control
@@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
- frame_duration = int(round(encoderinfo.get("duration", duration)))
+ frame_duration = int(round(encoderinfo["duration"]))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index eac27e679..5614957c1 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -36,7 +36,7 @@ import os
import struct
import sys
-from PIL import Image, ImageFile
+from . import Image, ImageFile
def isInt(f):
@@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self):
- from PIL import ImageTk
+ from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 1ca1b6ea9..86961e6ef 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -49,7 +49,7 @@ from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational
-from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
+from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
@@ -1185,7 +1185,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: Photoshop "Image Resource Blocks" in a dictionary.
"""
blocks = {}
- val = self.tag_v2.get(0x8649)
+ val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val[:4] == b"8BIM":
id = i16(val[4:6])
@@ -1550,7 +1550,7 @@ class TiffImageFile(ImageFile.ImageFile):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
- self._tile_orientation = self.tag_v2.get(0x0112)
+ self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation)
#
@@ -1894,6 +1894,10 @@ class AppendingTiffWriter:
8, # srational
4, # float
8, # double
+ 4, # ifd
+ 2, # unicode
+ 4, # complex
+ 8, # long8
]
# StripOffsets = 273
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 80a16a75e..f14e60cf5 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -24,7 +24,7 @@ def check_module(feature):
:returns: ``True`` if available, ``False`` otherwise.
:raises ValueError: If the module is not defined in this version of Pillow.
"""
- if not (feature in modules):
+ if feature not in modules:
msg = f"Unknown module {feature}"
raise ValueError(msg)
diff --git a/src/_imaging.c b/src/_imaging.c
index 281f3a4d2..5c6380fee 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) {
static PyObject *
_expand_image(ImagingObject *self, PyObject *args) {
int x, y;
- int mode = 0;
- if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) {
+ if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
- return PyImagingNew(ImagingExpand(self->image, x, y, mode));
+ return PyImagingNew(ImagingExpand(self->image, x, y));
}
static PyObject *
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 78e3f7f10..6cee021d4 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return NULL;
}
+#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11
+ PyConfig config;
+ PyConfig_InitPythonConfig(&config);
+ if (!PyArg_ParseTupleAndKeywords(
+ args,
+ kw,
+ "etf|nsy#n",
+ kwlist,
+ config.filesystem_encoding,
+ &filename,
+ &size,
+ &index,
+ &encoding,
+ &font_bytes,
+ &font_bytes_size,
+ &layout_engine)) {
+ PyConfig_Clear(&config);
+ return NULL;
+ }
+ PyConfig_Clear(&config);
+#else
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
@@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
&layout_engine)) {
return NULL;
}
+#endif
self = PyObject_New(FontObject, &Font_Type);
if (!self) {
@@ -167,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
/* Don't free this before FT_Done_Face */
self->font_bytes = PyMem_Malloc(font_bytes_size);
if (!self->font_bytes) {
- error = 65; // Out of Memory in Freetype.
+ error = FT_Err_Out_Of_Memory;
}
if (!error) {
memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size);
@@ -232,9 +254,7 @@ text_layout_raqm(
const char *dir,
PyObject *features,
const char *lang,
- GlyphInfo **glyph_info,
- int mask,
- int color) {
+ GlyphInfo **glyph_info) {
size_t i = 0, count = 0, start = 0;
raqm_t *rq;
raqm_glyph_t *glyphs = NULL;
@@ -471,7 +491,7 @@ text_layout(
#ifdef HAVE_RAQM
if (have_raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(
- string, self, dir, features, lang, glyph_info, mask, color);
+ string, self, dir, features, lang, glyph_info);
} else
#endif
{
@@ -529,73 +549,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length);
}
-static PyObject *
-font_getsize(FontObject *self, PyObject *args) {
+static int
+bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) {
int position; /* pen position along primary axis, in 26.6 precision */
int advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */
int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */
int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
- int load_flags; /* FreeType load_flags parameter */
int error;
- FT_Face face;
FT_Glyph glyph;
- FT_BBox bbox; /* glyph bounding box */
- GlyphInfo *glyph_info = NULL; /* computed text layout */
- size_t i, count; /* glyph_info index and length */
- int horizontal_dir; /* is primary axis horizontal? */
- int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
- int color = 0; /* is FT_LOAD_COLOR enabled? */
- const char *mode = NULL;
- const char *dir = NULL;
- const char *lang = NULL;
- const char *anchor = NULL;
- PyObject *features = Py_None;
- PyObject *string;
-
- /* calculate size and bearing for a given string */
-
- if (!PyArg_ParseTuple(
- args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
- return NULL;
- }
-
- horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
-
- mask = mode && strcmp(mode, "1") == 0;
- color = mode && strcmp(mode, "RGBA") == 0;
-
- if (anchor == NULL) {
- anchor = horizontal_dir ? "la" : "lt";
- }
- if (strlen(anchor) != 2) {
- goto bad_anchor;
- }
-
- count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
- if (PyErr_Occurred()) {
- return NULL;
- }
-
- load_flags = FT_LOAD_DEFAULT;
- if (mask) {
- load_flags |= FT_LOAD_TARGET_MONO;
- }
- if (color) {
- load_flags |= FT_LOAD_COLOR;
- }
-
+ FT_BBox bbox; /* glyph bounding box */
+ size_t i; /* glyph_info index */
/*
* text bounds are given by:
* - bounding boxes of individual glyphs
* - pen line, i.e. 0 to `advanced` along primary axis
* this means point (0, 0) is part of the text bounding box
*/
- face = NULL;
position = x_min = x_max = y_min = y_max = 0;
for (i = 0; i < count; i++) {
- face = self->face;
-
if (horizontal_dir) {
px = PIXEL(position + glyph_info[i].x_offset);
py = PIXEL(glyph_info[i].y_offset);
@@ -618,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(face, glyph_info[i].index, load_flags);
if (error) {
- return geterror(error);
+ geterror(error);
+ return 1;
}
error = FT_Get_Glyph(face->glyph, &glyph);
if (error) {
- return geterror(error);
+ geterror(error);
+ return 1;
}
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox);
@@ -647,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) {
FT_Done_Glyph(glyph);
}
- if (glyph_info) {
- PyMem_Free(glyph_info);
- glyph_info = NULL;
+ if (anchor == NULL) {
+ anchor = horizontal_dir ? "la" : "lt";
+ }
+ if (strlen(anchor) != 2) {
+ goto bad_anchor;
}
x_anchor = y_anchor = 0;
- if (face) {
+ if (count) {
if (horizontal_dir) {
switch (anchor[0]) {
case 'l': // left
@@ -671,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) {
}
switch (anchor[1]) {
case 'a': // ascender
- y_anchor = PIXEL(self->face->size->metrics.ascender);
+ y_anchor = PIXEL(face->size->metrics.ascender);
break;
case 't': // top
y_anchor = y_max;
break;
case 'm': // middle (ascender + descender) / 2
y_anchor = PIXEL(
- (self->face->size->metrics.ascender +
- self->face->size->metrics.descender) /
+ (face->size->metrics.ascender +
+ face->size->metrics.descender) /
2);
break;
case 's': // horizontal baseline
@@ -689,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) {
y_anchor = y_min;
break;
case 'd': // descender
- y_anchor = PIXEL(self->face->size->metrics.descender);
+ y_anchor = PIXEL(face->size->metrics.descender);
break;
default:
goto bad_anchor;
@@ -729,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) {
}
}
}
-
- return Py_BuildValue(
- "(ii)(ii)",
- (x_max - x_min),
- (y_max - y_min),
- (-x_anchor + x_min),
- -(-y_anchor + y_max));
+ *width = x_max - x_min;
+ *height = y_max - y_min;
+ *x_offset = -x_anchor + x_min;
+ *y_offset = -(-y_anchor + y_max);
+ return 0;
bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
- return NULL;
+ return 1;
+}
+
+static PyObject *
+font_getsize(FontObject *self, PyObject *args) {
+ int width, height, x_offset, y_offset;
+ int load_flags; /* FreeType load_flags parameter */
+ int error;
+ GlyphInfo *glyph_info = NULL; /* computed text layout */
+ size_t count; /* glyph_info length */
+ int horizontal_dir; /* is primary axis horizontal? */
+ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
+ int color = 0; /* is FT_LOAD_COLOR enabled? */
+ const char *mode = NULL;
+ const char *dir = NULL;
+ const char *lang = NULL;
+ const char *anchor = NULL;
+ PyObject *features = Py_None;
+ PyObject *string;
+
+ /* calculate size and bearing for a given string */
+
+ if (!PyArg_ParseTuple(
+ args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
+ return NULL;
+ }
+
+ horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
+
+ mask = mode && strcmp(mode, "1") == 0;
+ color = mode && strcmp(mode, "RGBA") == 0;
+
+ count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
+ if (PyErr_Occurred()) {
+ return NULL;
+ }
+
+ load_flags = FT_LOAD_DEFAULT;
+ if (mask) {
+ load_flags |= FT_LOAD_TARGET_MONO;
+ }
+ if (color) {
+ load_flags |= FT_LOAD_COLOR;
+ }
+
+ error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
+ if (glyph_info) {
+ PyMem_Free(glyph_info);
+ glyph_info = NULL;
+ }
+ if (error) {
+ return NULL;
+ }
+
+ return Py_BuildValue(
+ "(ii)(ii)",
+ width,
+ height,
+ x_offset,
+ y_offset);
}
static PyObject *
@@ -763,6 +796,7 @@ font_render(FontObject *self, PyObject *args) {
unsigned int bitmap_y; /* glyph bitmap y index */
unsigned char *source; /* glyph bitmap source buffer */
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
+ PyObject *image;
Imaging im;
Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
@@ -773,27 +807,34 @@ font_render(FontObject *self, PyObject *args) {
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
+ const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
+ PyObject *fill;
float x_start = 0;
float y_start = 0;
+ int width, height, x_offset, y_offset;
+ int horizontal_dir; /* is primary axis horizontal? */
+ PyObject *max_image_pixels = Py_None;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
if (!PyArg_ParseTuple(
args,
- "On|zzOziLff:render",
+ "OO|zzOzizLffO:render",
&string,
- &id,
+ &fill,
&mode,
&dir,
&features,
&lang,
&stroke_width,
+ &anchor,
&foreground_ink_long,
&x_start,
- &y_start)) {
+ &y_start,
+ &max_image_pixels)) {
return NULL;
}
@@ -819,14 +860,52 @@ font_render(FontObject *self, PyObject *args) {
if (PyErr_Occurred()) {
return NULL;
}
- if (count == 0) {
- Py_RETURN_NONE;
+
+ load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
+ if (mask) {
+ load_flags |= FT_LOAD_TARGET_MONO;
+ }
+ if (color) {
+ load_flags |= FT_LOAD_COLOR;
+ }
+
+ horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
+
+ error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
+ if (error) {
+ PyMem_Del(glyph_info);
+ return NULL;
+ }
+
+ width += stroke_width * 2 + ceil(x_start);
+ height += stroke_width * 2 + ceil(y_start);
+ if (max_image_pixels != Py_None) {
+ if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0);
+ }
+ }
+
+ image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
+ if (image == NULL) {
+ PyMem_Del(glyph_info);
+ return NULL;
+ }
+ id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
+ im = (Imaging)id;
+
+ x_offset -= stroke_width;
+ y_offset -= stroke_width;
+ if (count == 0 || width == 0 || height == 0) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
}
if (stroke_width) {
error = FT_Stroker_New(library, &stroker);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
FT_Stroker_Set(
@@ -837,15 +916,6 @@ font_render(FontObject *self, PyObject *args) {
0);
}
- im = (Imaging)id;
- load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
- if (mask) {
- load_flags |= FT_LOAD_TARGET_MONO;
- }
- if (color) {
- load_flags |= FT_LOAD_COLOR;
- }
-
/*
* calculate x_min and y_max
* must match font_getsize or there may be clipping!
@@ -858,7 +928,8 @@ font_render(FontObject *self, PyObject *args) {
error =
FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
glyph_slot = self->face->glyph;
@@ -889,7 +960,8 @@ font_render(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags);
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
glyph_slot = self->face->glyph;
@@ -903,7 +975,8 @@ font_render(FontObject *self, PyObject *args) {
error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1);
}
if (error) {
- return geterror(error);
+ geterror(error);
+ goto glyph_error;
}
bitmap_glyph = (FT_BitmapGlyph)glyph;
@@ -1042,9 +1115,15 @@ font_render(FontObject *self, PyObject *args) {
}
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- Py_RETURN_NONE;
+ return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
glyph_error:
+ if (im->destroy) {
+ im->destroy(im);
+ }
+ if (im->image) {
+ free(im->image);
+ }
if (stroker != NULL) {
FT_Done_Glyph(glyph);
}
diff --git a/src/display.c b/src/display.c
index e8e7b62c2..754a6ae78 100644
--- a/src/display.c
+++ b/src/display.c
@@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};
if (!OpenClipboard(NULL)) {
- PyErr_SetString(PyExc_OSError, "failed to open clipboard");
- return NULL;
+ // Maybe the clipboard is temporarily in use by another process.
+ // Wait and try again
+ Sleep(500);
+
+ if (!OpenClipboard(NULL)) {
+ PyErr_SetString(PyExc_OSError, "failed to open clipboard");
+ return NULL;
+ }
}
// find best format as set by clipboard owner
diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c
index 4b8d2bf05..4dcd368ca 100644
--- a/src/libImaging/Filter.c
+++ b/src/libImaging/Filter.c
@@ -49,7 +49,7 @@ clip32(float in) {
}
Imaging
-ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
+ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
Imaging imOut;
int x, y;
ImagingSectionCookie cookie;
diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h
index af9996ca9..f6e7fb6b9 100644
--- a/src/libImaging/ImPlatform.h
+++ b/src/libImaging/ImPlatform.h
@@ -25,7 +25,7 @@
#endif
#endif
-#if defined(_WIN32) || defined(__CYGWIN__)
+#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */
#define WIN32_LEAN_AND_MEAN
#include
@@ -37,15 +37,33 @@
#undef WIN32
#endif
-#else
+#else /* not WIN */
/* For System that are not Windows, we'll need to define these. */
+/* We have to define them instead of using typedef because the JPEG lib also
+ defines their own types with the same names, so we need to be able to undef
+ ours before including the JPEG code. */
+
+#if __STDC_VERSION__ >= 199901L /* C99+ */
+
+#include
+
+#define INT8 int8_t
+#define UINT8 uint8_t
+#define INT16 int16_t
+#define UINT16 uint16_t
+#define INT32 int32_t
+#define UINT32 uint32_t
+
+#else /* < C99 */
+
+#define INT8 signed char
#if SIZEOF_SHORT == 2
#define INT16 short
#elif SIZEOF_INT == 2
#define INT16 int
#else
-#define INT16 short /* most things works just fine anyway... */
+#error Cannot find required 16-bit integer type
#endif
#if SIZEOF_SHORT == 4
@@ -58,19 +76,13 @@
#error Cannot find required 32-bit integer type
#endif
-#if SIZEOF_LONG == 8
-#define INT64 long
-#elif SIZEOF_LONG_LONG == 8
-#define INT64 long
-#endif
-
-#define INT8 signed char
#define UINT8 unsigned char
-
#define UINT16 unsigned INT16
#define UINT32 unsigned INT32
-#endif
+#endif /* < C99 */
+
+#endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */
#define FLOAT16 UINT16
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index d9ded1852..beec8a8f2 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b);
extern Imaging
ImagingCrop(Imaging im, int x0, int y0, int x1, int y1);
extern Imaging
-ImagingExpand(Imaging im, int x, int y, int mode);
+ImagingExpand(Imaging im, int x, int y);
extern Imaging
ImagingFill(Imaging im, const void *ink);
extern int
diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c
index 0d7e896b7..de8586706 100644
--- a/src/libImaging/Jpeg2KEncode.c
+++ b/src/libImaging/Jpeg2KEncode.c
@@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
}
if (!context->num_resolutions) {
- while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) {
+ while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) {
params.numresolution -= 1;
}
}
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 7cf00ef35..128595f65 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -37,8 +37,6 @@
#include "Imaging.h"
#include
-int ImagingNewCount = 0;
-
/* --------------------------------------------------------------------
* Standard image object.
*/
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index a0fa22c7d..206403ba6 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -1552,10 +1552,12 @@ static struct {
{"P", "P;4L", 4, unpackP4L},
{"P", "P", 8, copy1},
{"P", "P;R", 8, unpackLR},
+ {"P", "L", 8, copy1},
/* palette w. alpha */
{"PA", "PA", 16, unpackLA},
{"PA", "PA;L", 16, unpackLAL},
+ {"PA", "LA", 16, unpackLA},
/* true colour */
{"RGB", "RGB", 24, ImagingUnpackRGB},
diff --git a/tox.ini b/tox.ini
index 458a00107..a79089f51 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,7 +13,7 @@ extras =
tests
commands =
make clean
- {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
+ {envpython} -m pip install .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
allowlist_externals =
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 21b6c10a5..b41a735db 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.4.2.tar.gz/download",
- "filename": "xz-5.4.2.tar.gz",
- "dir": "xz-5.4.2",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download",
+ "filename": "xz-5.4.3.tar.gz",
+ "dir": "xz-5.4.3",
"license": "COPYING",
"build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),