Merge remote-tracking branch 'upstream/main' into add-pyproject.toml

# Conflicts:
#	pyproject.toml
This commit is contained in:
nulano 2023-06-24 13:30:15 +01:00
commit c068af7630
No known key found for this signature in database
GPG Key ID: B650CDF63B705766
55 changed files with 517 additions and 267 deletions

View File

@ -13,10 +13,6 @@ indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.rst]
# Four-space indentation
indent_size = 4
[*.yml] [*.yml]
# Two-space indentation # Two-space indentation
indent_size = 2 indent_size = 2

View File

@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8. - Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - 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. - 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 ## Reporting Issues

View File

@ -104,7 +104,7 @@ jobs:
- name: Build - name: Build
shell: bash.exe -eo pipefail -o igncr "{0}" shell: bash.exe -eo pipefail -o igncr "{0}"
run: | run: |
SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh .ci/build.sh
- name: Test - name: Test
run: | run: |

View File

@ -39,6 +39,7 @@ jobs:
centos-stream-8-amd64, centos-stream-8-amd64,
centos-stream-9-amd64, centos-stream-9-amd64,
debian-11-bullseye-x86, debian-11-bullseye-x86,
debian-12-bookworm-x86,
fedora-37-amd64, fedora-37-amd64,
fedora-38-amd64, fedora-38-amd64,
gentoo, gentoo,

View File

@ -80,7 +80,7 @@ jobs:
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow - 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 - name: Test Pillow
run: | run: |

View File

@ -28,10 +28,10 @@ jobs:
architecture: ["x86", "x64"] architecture: ["x86", "x64"]
include: include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows # PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy3.8"
architecture: "x64"
- python-version: "pypy3.9" - python-version: "pypy3.9"
architecture: "x64" architecture: "x64"
- python-version: "pypy3.10"
architecture: "x64"
timeout-minutes: 30 timeout-minutes: 30

View File

@ -29,8 +29,8 @@ jobs:
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
"pypy3.10",
"pypy3.9", "pypy3.9",
"pypy3.8",
"3.12-dev", "3.12-dev",
"3.11", "3.11",
"3.10", "3.10",

View File

@ -4,9 +4,6 @@ repos:
hooks: hooks:
- id: black - id: black
args: [--target-version=py38] 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 - repo: https://github.com/PyCQA/isort
rev: 5.12.0 rev: 5.12.0

View File

@ -5,6 +5,42 @@ Changelog (Pillow)
10.0.0 (unreleased) 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 - Improved wl-paste mimetype handling in ImageGrab #7094
[rrcgat, radarhere] [rrcgat, radarhere]

View File

@ -15,6 +15,7 @@ graft src
graft depends graft depends
graft winbuild graft winbuild
graft docs graft docs
graft _custom_build
# build/src control detritus # build/src control detritus
exclude .appveyor.yml exclude .appveyor.yml

View File

@ -46,7 +46,6 @@ help:
@echo " docserve run an HTTP server on the docs directory" @echo " docserve run an HTTP server on the docs directory"
@echo " html make HTML docs" @echo " html make HTML docs"
@echo " htmlview open the index page built by the html target in your browser" @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 make and install"
@echo " install-coverage make and install with C coverage" @echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks" @echo " lint run the lint checks"
@ -54,10 +53,6 @@ help:
@echo " release-test run code and package tests before release" @echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow" @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 .PHONY: install
install: install:
python3 -m pip -v install . python3 -m pip -v install .
@ -65,7 +60,7 @@ install:
.PHONY: install-coverage .PHONY: install-coverage
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 python3 selftest.py
.PHONY: debug .PHONY: debug
@ -74,7 +69,7 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we # for our stuff, kills optimization, and redirects to dev null so we
# see any build failures. # see any build failures.
make clean > /dev/null 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 .PHONY: release-test
release-test: release-test:

View File

@ -27,25 +27,19 @@ def timer(func, label, *args):
for x in range(iterations): for x in range(iterations):
func(*args) func(*args)
if time.time() - starttime > 10: if time.time() - starttime > 10:
print(
"{}: breaking at {} iterations, {:.6f} per iteration".format(
label, x + 1, (time.time() - starttime) / (x + 1.0)
)
)
break break
if x == iterations - 1: endtime = time.time()
endtime = time.time() print(
print( "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
"{}: {:.4f} s {:.6f} per iteration".format( label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
label, endtime - starttime, (endtime - starttime) / (x + 1.0)
)
) )
)
def test_direct(): def test_direct():
im = hopper() im = hopper()
im.load() 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) caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False) access = PyAccess.new(im, False)

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

View File

@ -447,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info.get("duration") == 750 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): def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
size = (128, 64) size = (128, 64)

View File

@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5 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( @pytest.mark.parametrize(
"path, mode", "path, mode",
( (

View File

@ -96,10 +96,17 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) 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: with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif") 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): def test_set_legacy_api(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:

View File

@ -32,6 +32,14 @@ def test_putpalette():
with pytest.raises(ValueError): with pytest.raises(ValueError):
palette("YCbCr") 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(): def test_imagepalette():
im = hopper("P") im = hopper("P")

View File

@ -463,6 +463,11 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png") 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): def test_getbbox_empty(font):
# issue #2614, should not crash. # issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("") assert (0, 0, 0, 0) == font.getbbox("")
@ -1037,6 +1042,7 @@ def test_render_mono_size():
"test_file", "test_file",
[ [
"Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
], ],
) )
def test_oom(test_file): def test_oom(test_file):

View File

@ -404,6 +404,18 @@ def test_exif_transpose():
assert 0x0112 not in transposed_im.getexif() 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(): def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast # Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:

56
_custom_build/backend.py Executable file
View File

@ -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

View File

@ -2,7 +2,7 @@
from livereload.compiler import shell from livereload.compiler import shell
from livereload.task import Task from livereload.task import Task
Task.add('*.rst', shell('make html')) Task.add("*.rst", shell("make html"))
Task.add('*/*.rst', shell('make html')) Task.add("*/*.rst", shell("make html"))
Task.add('Makefile', shell('make html')) Task.add("Makefile", shell("make html"))
Task.add('conf.py', shell('make html')) Task.add("conf.py", shell("make html"))

View File

@ -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). 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 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 are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given
example, a rectangle covering all of an 800x600 pixel image is written as (0, first.
0, 800, 600).
Palette Palette
------- -------

View File

@ -1380,6 +1380,12 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. 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 SUN
^^^ ^^^
@ -1562,13 +1568,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 5.3.0 .. versionadded:: 5.3.0
QOI
^^^
.. versionadded:: 9.5.0
Pillow identifies and reads images in Quite OK Image format.
XV Thumbnails XV Thumbnails
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -312,6 +312,11 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm 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 .. tab:: FreeBSD
.. Note:: Only FreeBSD 10 and 11 tested .. 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 using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present. available, as many as are present.
* Build flags: ``--disable-zlib``, ``--disable-jpeg``, * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``,
``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
Disable building the corresponding feature even if the development Disable building the corresponding feature even if the development
libraries are present on the building machine. libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``, * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``,
``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
Require that the corresponding feature is built. The build will raise Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata) an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together. 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 These flags are used to compile a modified version of libraqm and
a shim that dynamically loads libfribidi at runtime. These are a shim that dynamically loads libfribidi at runtime. These are
used to compile the standard Pillow wheels. Compiling libraqm requires used to compile the standard Pillow wheels. Compiling libraqm requires
a C99-compliant compiler. 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 platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the automated build systems that configure the proper paths in the
environment variables (e.g. Buildroot). 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 library search process to dump all paths searched for and found to
stdout. stdout.
Sample usage:: 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 Platform Support
---------------- ----------------
@ -448,6 +453,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 | | Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86 |
+----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 | | Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 | | Fedora 38 | 3.11 | x86-64 |

View File

@ -328,7 +328,7 @@ Methods
.. versionadded:: 5.3.0 .. 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. Draws a rounded rectangle.
@ -341,6 +341,7 @@ Methods
:param width: The line width, in pixels. :param width: The line width, in pixels.
:param corners: A tuple of whether to round each corner, :param corners: A tuple of whether to round each corner,
``(top_left, top_right, bottom_right, bottom_left)``. ``(top_left, top_right, bottom_right, bottom_left)``.
Keyword-only argument.
.. versionadded:: 8.2.0 .. versionadded:: 8.2.0

View File

@ -1,5 +1,4 @@
[build-system] [build-system]
build-backend = "setuptools.build_meta" requires = ["setuptools >= 67.8", "wheel"]
requires = [ build-backend = "backend"
"setuptools>=67.8", backend-path = ["_custom_build"]
]

View File

@ -515,6 +515,7 @@ class pil_build_ext(build_ext):
elif sys.platform == "cygwin": elif sys.platform == "cygwin":
# pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory
self.compiler.shared_lib_extension = ".dll.a"
_add_directory( _add_directory(
library_dirs, library_dirs,
os.path.join( os.path.join(

View File

@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
if gs_windows_binary is not None: if gs_windows_binary is not None:
if not gs_windows_binary: 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" msg = "Unable to locate Ghostscript on paths"
raise OSError(msg) raise OSError(msg)
command[0] = gs_windows_binary command[0] = gs_windows_binary
@ -354,7 +361,6 @@ class EpsImageFile(ImageFile.ImageFile):
check_required_header_comments() check_required_header_comments()
if not self._size: if not self._size:
self._size = 1, 1 # errors if this isn't set. why (1,1)?
msg = "cannot determine EPS bounding box" msg = "cannot determine EPS bounding box"
raise OSError(msg) raise OSError(msg)

View File

@ -47,7 +47,7 @@ class GdImageFile(ImageFile.ImageFile):
# Header # Header
s = self.fp.read(1037) 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" msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -879,7 +879,7 @@ def _get_palette_bytes(im):
:param im: Image object :param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header :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): def _get_background(im, info_background):

View File

@ -22,11 +22,11 @@ import os
import struct import struct
import sys import sys
from PIL import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
enable_jpeg2k = features.check_codec("jpg_2000") enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k: if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin from . import Jpeg2KImagePlugin
MAGIC = b"icns" MAGIC = b"icns"
HEADERSIZE = 8 HEADERSIZE = 8

View File

@ -1254,7 +1254,7 @@ class Image:
if ymargin is None: if ymargin is None:
ymargin = xmargin ymargin = xmargin
self.load() self.load()
return self._new(self.im.expand(xmargin, ymargin, 0)) return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter): def filter(self, filter):
""" """
@ -1433,12 +1433,12 @@ class Image:
self._exif.load(exif_info) self._exif.load(exif_info)
# XMP tags # 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") xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags: if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match: if match:
self._exif[0x0112] = int(match[2]) self._exif[ExifTags.Base.Orientation] = int(match[2])
return self._exif return self._exif
@ -1731,7 +1731,7 @@ class Image:
if not isinstance(dest, (list, tuple)): if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple" msg = "Destination must be a tuple"
raise ValueError(msg) 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" msg = "Source must be a 2 or 4-tuple"
raise ValueError(msg) raise ValueError(msg)
if not len(dest) == 2: 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 The image is first saved to a temporary file. By default, it will be in
PNG format. PNG format.
On Unix, the image is then opened using the **display**, **eog** or On Unix, the image is then opened using the **xdg-open**, **display**,
**xv** utility, depending on which one can be found. **gm**, **eog** or **xv** utility, depending on which one can be found.
On macOS, the image is opened with the native Preview application. On macOS, the image is opened with the native Preview application.

View File

@ -18,10 +18,10 @@
import sys import sys
from enum import IntEnum from enum import IntEnum
from PIL import Image from . import Image
try: try:
from PIL import _imagingcms from . import _imagingcms
except ImportError as ex: except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing # Allow error import for doc purposes, but error out when accessing
# anything in core. # anything in core.
@ -271,7 +271,7 @@ def get_display_profile(handle=None):
if sys.platform != "win32": if sys.platform != "win32":
return None return None
from PIL import ImageWin from . import ImageWin
if isinstance(handle, ImageWin.HDC): if isinstance(handle, ImageWin.HDC):
profile = core.get_display_profile_win32(handle, 1) profile = core.get_display_profile_win32(handle, 1)

View File

@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter):
class Kernel(BuiltinFilter): 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. supports 3x3 and 5x5 integer and floating point kernels.
In the current version, kernels can only be applied to 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 :param size: Kernel size, given as (width, height). In the current
version, this must be (3,3) or (5,5). 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 :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. kernel weights.
:param offset: Offset. If given, this value is added to the result, :param offset: Offset. If given, this value is added to the result,
after it has been divided by the scale factor. after it has been divided by the scale factor.

View File

@ -26,7 +26,6 @@
# #
import base64 import base64
import math
import os import os
import sys import sys
import warnings import warnings
@ -226,10 +225,6 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine) 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): def getname(self):
""" """
:return: A tuple of the font family (e.g. Helvetica) and the font style :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 :py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking gap between the starting coordinate and the first marking
""" """
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
if start is None: if start is None:
start = (0, 0) start = (0, 0)
size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) im, size, offset = self.font.render(
offset = offset[0] - stroke_width, offset[1] - stroke_width 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) 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 return im, offset
def font_variant( def font_variant(

View File

@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import io
import os import os
import shutil import shutil
import subprocess import subprocess
@ -94,14 +95,14 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
def grabclipboard(): def grabclipboard():
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".jpg") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)
commands = [ commands = [
'set theFile to (open for access POSIX file "' 'set theFile to (open for access POSIX file "'
+ filepath + filepath
+ '" with write permission)', + '" with write permission)',
"try", "try",
" write (the clipboard as JPEG picture) to theFile", " write (the clipboard as «class PNGf») to theFile",
"end try", "end try",
"close access theFile", "close access theFile",
] ]
@ -128,8 +129,6 @@ def grabclipboard():
files = data[o:].decode("mbcs").split("\0") files = data[o:].decode("mbcs").split("\0")
return files[: files.index("")] return files[: files.index("")]
if isinstance(data, bytes): if isinstance(data, bytes):
import io
data = io.BytesIO(data) data = io.BytesIO(data)
if fmt == "png": if fmt == "png":
from . import PngImagePlugin from . import PngImagePlugin
@ -159,13 +158,12 @@ def grabclipboard():
else: else:
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg) raise NotImplementedError(msg)
fh, filepath = tempfile.mkstemp() p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr err = p.stderr
os.close(fh)
if err: if err:
msg = f"{args[0]} error: {err.strip().decode()}" msg = f"{args[0]} error: {err.strip().decode()}"
raise ChildProcessError(msg) raise ChildProcessError(msg)
im = Image.open(filepath) data = io.BytesIO(p.stdout)
im = Image.open(data)
im.load() im.load()
os.unlink(filepath)
return im return im

View File

@ -21,7 +21,7 @@ import functools
import operator import operator
import re import re
from . import Image, ImagePalette from . import ExifTags, Image, ImagePalette
# #
# helpers # helpers
@ -576,19 +576,20 @@ def solarize(image, threshold=128):
return _lut(image, lut) 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 If an image has an EXIF Orientation tag, other than 1, transpose the image
that is transposed accordingly. The new image will have the orientation accordingly, and remove the orientation data.
data removed.
Otherwise, return a copy of the image.
:param image: The image to transpose. :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() image_exif = image.getexif()
orientation = exif.get(0x0112) orientation = image_exif.get(ExifTags.Base.Orientation)
method = { method = {
2: Image.Transpose.FLIP_LEFT_RIGHT, 2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180, 3: Image.Transpose.ROTATE_180,
@ -600,22 +601,28 @@ def exif_transpose(image):
}.get(orientation) }.get(orientation)
if method is not None: if method is not None:
transposed_image = image.transpose(method) transposed_image = image.transpose(method)
transposed_exif = transposed_image.getexif() if in_place:
if 0x0112 in transposed_exif: image.im = transposed_image.im
del transposed_exif[0x0112] image.pyaccess = None
if "exif" in transposed_image.info: image._size = transposed_image._size
transposed_image.info["exif"] = transposed_exif.tobytes() exif_image = image if in_place else transposed_image
elif "Raw profile type exif" in transposed_image.info:
transposed_image.info[ exif = exif_image.getexif()
"Raw profile type exif" if ExifTags.Base.Orientation in exif:
] = transposed_exif.tobytes().hex() del exif[ExifTags.Base.Orientation]
elif "XML:com.adobe.xmp" in transposed_image.info: 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 ( for pattern in (
r'tiff:Orientation="([0-9])"', r'tiff:Orientation="([0-9])"',
r"<tiff:Orientation>([0-9])</tiff:Orientation>", r"<tiff:Orientation>([0-9])</tiff:Orientation>",
): ):
transposed_image.info["XML:com.adobe.xmp"] = re.sub( exif_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", transposed_image.info["XML:com.adobe.xmp"] pattern, "", exif_image.info["XML:com.adobe.xmp"]
) )
return transposed_image if not in_place:
return image.copy() return transposed_image
elif not in_place:
return image.copy()

View File

@ -17,7 +17,7 @@ import subprocess
import sys import sys
from shlex import quote from shlex import quote
from PIL import Image from . import Image
_viewers = [] _viewers = []

View File

@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile):
if os.path.exists(self.filename): if os.path.exists(self.filename):
subprocess.check_call(["djpeg", "-outfile", path, self.filename]) subprocess.check_call(["djpeg", "-outfile", path, self.filename])
else: else:
try:
os.unlink(path)
except OSError:
pass
msg = "Invalid Filename" msg = "Invalid Filename"
raise ValueError(msg) raise ValueError(msg)

View File

@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
and prev_disposal == encoderinfo.get("disposal") and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend") and prev_blend == encoderinfo.get("blend")
): ):
if isinstance(duration, (list, tuple)): previous["encoderinfo"]["duration"] += encoderinfo.get(
previous["encoderinfo"]["duration"] += encoderinfo["duration"] "duration", duration
)
continue continue
else: else:
bbox = None bbox = None
if "duration" not in encoderinfo:
encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
# animation control # animation control
@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox) im_frame = im_frame.crop(bbox)
size = im_frame.size size = im_frame.size
encoderinfo = frame_data["encoderinfo"] 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_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend) frame_blend = encoderinfo.get("blend", blend)
# frame control # frame control

View File

@ -36,7 +36,7 @@ import os
import struct import struct
import sys import sys
from PIL import Image, ImageFile from . import Image, ImageFile
def isInt(f): def isInt(f):
@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# returns a ImageTk.PhotoImage object, after rescaling to 0..255 # returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self): def tkPhotoImage(self):
from PIL import ImageTk from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256) return ImageTk.PhotoImage(self.convert2byte(), palette=256)

View File

@ -49,7 +49,7 @@ from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational 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 i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
@ -1185,7 +1185,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: Photoshop "Image Resource Blocks" in a dictionary. :returns: Photoshop "Image Resource Blocks" in a dictionary.
""" """
blocks = {} blocks = {}
val = self.tag_v2.get(0x8649) val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val: if val:
while val[:4] == b"8BIM": while val[:4] == b"8BIM":
id = i16(val[4:6]) id = i16(val[4:6])
@ -1550,7 +1550,7 @@ class TiffImageFile(ImageFile.ImageFile):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) 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 8, # srational
4, # float 4, # float
8, # double 8, # double
4, # ifd
2, # unicode
4, # complex
8, # long8
] ]
# StripOffsets = 273 # StripOffsets = 273

View File

@ -24,7 +24,7 @@ def check_module(feature):
:returns: ``True`` if available, ``False`` otherwise. :returns: ``True`` if available, ``False`` otherwise.
:raises ValueError: If the module is not defined in this version of Pillow. :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}" msg = f"Unknown module {feature}"
raise ValueError(msg) raise ValueError(msg)

View File

@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) {
static PyObject * static PyObject *
_expand_image(ImagingObject *self, PyObject *args) { _expand_image(ImagingObject *self, PyObject *args) {
int x, y; int x, y;
int mode = 0; if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) {
return NULL; return NULL;
} }
return PyImagingNew(ImagingExpand(self->image, x, y, mode)); return PyImagingNew(ImagingExpand(self->image, x, y));
} }
static PyObject * static PyObject *

View File

@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return NULL; 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( if (!PyArg_ParseTupleAndKeywords(
args, args,
kw, kw,
@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
&layout_engine)) { &layout_engine)) {
return NULL; return NULL;
} }
#endif
self = PyObject_New(FontObject, &Font_Type); self = PyObject_New(FontObject, &Font_Type);
if (!self) { if (!self) {
@ -167,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
/* Don't free this before FT_Done_Face */ /* Don't free this before FT_Done_Face */
self->font_bytes = PyMem_Malloc(font_bytes_size); self->font_bytes = PyMem_Malloc(font_bytes_size);
if (!self->font_bytes) { if (!self->font_bytes) {
error = 65; // Out of Memory in Freetype. error = FT_Err_Out_Of_Memory;
} }
if (!error) { if (!error) {
memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size);
@ -232,9 +254,7 @@ text_layout_raqm(
const char *dir, const char *dir,
PyObject *features, PyObject *features,
const char *lang, const char *lang,
GlyphInfo **glyph_info, GlyphInfo **glyph_info) {
int mask,
int color) {
size_t i = 0, count = 0, start = 0; size_t i = 0, count = 0, start = 0;
raqm_t *rq; raqm_t *rq;
raqm_glyph_t *glyphs = NULL; raqm_glyph_t *glyphs = NULL;
@ -471,7 +491,7 @@ text_layout(
#ifdef HAVE_RAQM #ifdef HAVE_RAQM
if (have_raqm && self->layout_engine == LAYOUT_RAQM) { if (have_raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm( count = text_layout_raqm(
string, self, dir, features, lang, glyph_info, mask, color); string, self, dir, features, lang, glyph_info);
} else } else
#endif #endif
{ {
@ -529,73 +549,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length); return PyLong_FromLong(length);
} }
static PyObject * static int
font_getsize(FontObject *self, PyObject *args) { 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 position; /* pen position along primary axis, in 26.6 precision */
int advanced; /* pen position along primary axis, in pixels */ int advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, 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_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 x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
int load_flags; /* FreeType load_flags parameter */
int error; int error;
FT_Face face;
FT_Glyph glyph; FT_Glyph glyph;
FT_BBox bbox; /* glyph bounding box */ FT_BBox bbox; /* glyph bounding box */
GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i; /* glyph_info index */
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;
}
/* /*
* text bounds are given by: * text bounds are given by:
* - bounding boxes of individual glyphs * - bounding boxes of individual glyphs
* - pen line, i.e. 0 to `advanced` along primary axis * - pen line, i.e. 0 to `advanced` along primary axis
* this means point (0, 0) is part of the text bounding box * this means point (0, 0) is part of the text bounding box
*/ */
face = NULL;
position = x_min = x_max = y_min = y_max = 0; position = x_min = x_max = y_min = y_max = 0;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
face = self->face;
if (horizontal_dir) { if (horizontal_dir) {
px = PIXEL(position + glyph_info[i].x_offset); px = PIXEL(position + glyph_info[i].x_offset);
py = PIXEL(glyph_info[i].y_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); error = FT_Load_Glyph(face, glyph_info[i].index, load_flags);
if (error) { if (error) {
return geterror(error); geterror(error);
return 1;
} }
error = FT_Get_Glyph(face->glyph, &glyph); error = FT_Get_Glyph(face->glyph, &glyph);
if (error) { if (error) {
return geterror(error); geterror(error);
return 1;
} }
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox);
@ -647,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) {
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }
if (glyph_info) { if (anchor == NULL) {
PyMem_Free(glyph_info); anchor = horizontal_dir ? "la" : "lt";
glyph_info = NULL; }
if (strlen(anchor) != 2) {
goto bad_anchor;
} }
x_anchor = y_anchor = 0; x_anchor = y_anchor = 0;
if (face) { if (count) {
if (horizontal_dir) { if (horizontal_dir) {
switch (anchor[0]) { switch (anchor[0]) {
case 'l': // left case 'l': // left
@ -671,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) {
} }
switch (anchor[1]) { switch (anchor[1]) {
case 'a': // ascender case 'a': // ascender
y_anchor = PIXEL(self->face->size->metrics.ascender); y_anchor = PIXEL(face->size->metrics.ascender);
break; break;
case 't': // top case 't': // top
y_anchor = y_max; y_anchor = y_max;
break; break;
case 'm': // middle (ascender + descender) / 2 case 'm': // middle (ascender + descender) / 2
y_anchor = PIXEL( y_anchor = PIXEL(
(self->face->size->metrics.ascender + (face->size->metrics.ascender +
self->face->size->metrics.descender) / face->size->metrics.descender) /
2); 2);
break; break;
case 's': // horizontal baseline case 's': // horizontal baseline
@ -689,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) {
y_anchor = y_min; y_anchor = y_min;
break; break;
case 'd': // descender case 'd': // descender
y_anchor = PIXEL(self->face->size->metrics.descender); y_anchor = PIXEL(face->size->metrics.descender);
break; break;
default: default:
goto bad_anchor; goto bad_anchor;
@ -729,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) {
} }
} }
} }
*width = x_max - x_min;
return Py_BuildValue( *height = y_max - y_min;
"(ii)(ii)", *x_offset = -x_anchor + x_min;
(x_max - x_min), *y_offset = -(-y_anchor + y_max);
(y_max - y_min), return 0;
(-x_anchor + x_min),
-(-y_anchor + y_max));
bad_anchor: bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", 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 * static PyObject *
@ -763,6 +796,7 @@ font_render(FontObject *self, PyObject *args) {
unsigned int bitmap_y; /* glyph bitmap y index */ unsigned int bitmap_y; /* glyph bitmap y index */
unsigned char *source; /* glyph bitmap source buffer */ unsigned char *source; /* glyph bitmap source buffer */
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
PyObject *image;
Imaging im; Imaging im;
Py_ssize_t id; Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ 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 *mode = NULL;
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None; PyObject *features = Py_None;
PyObject *string; PyObject *string;
PyObject *fill;
float x_start = 0; float x_start = 0;
float y_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 /* render string into given buffer (the buffer *must* have
the right size, or this will crash) */ the right size, or this will crash) */
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"On|zzOziLff:render", "OO|zzOzizLffO:render",
&string, &string,
&id, &fill,
&mode, &mode,
&dir, &dir,
&features, &features,
&lang, &lang,
&stroke_width, &stroke_width,
&anchor,
&foreground_ink_long, &foreground_ink_long,
&x_start, &x_start,
&y_start)) { &y_start,
&max_image_pixels)) {
return NULL; return NULL;
} }
@ -819,14 +860,52 @@ font_render(FontObject *self, PyObject *args) {
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; 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) { if (stroke_width) {
error = FT_Stroker_New(library, &stroker); error = FT_Stroker_New(library, &stroker);
if (error) { if (error) {
return geterror(error); geterror(error);
goto glyph_error;
} }
FT_Stroker_Set( FT_Stroker_Set(
@ -837,15 +916,6 @@ font_render(FontObject *self, PyObject *args) {
0); 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 * calculate x_min and y_max
* must match font_getsize or there may be clipping! * must match font_getsize or there may be clipping!
@ -858,7 +928,8 @@ font_render(FontObject *self, PyObject *args) {
error = error =
FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER);
if (error) { if (error) {
return geterror(error); geterror(error);
goto glyph_error;
} }
glyph_slot = self->face->glyph; 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); error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags);
if (error) { if (error) {
return geterror(error); geterror(error);
goto glyph_error;
} }
glyph_slot = self->face->glyph; 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); error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1);
} }
if (error) { if (error) {
return geterror(error); geterror(error);
goto glyph_error;
} }
bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap_glyph = (FT_BitmapGlyph)glyph;
@ -1042,9 +1115,15 @@ font_render(FontObject *self, PyObject *args) {
} }
FT_Stroker_Done(stroker); FT_Stroker_Done(stroker);
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
Py_RETURN_NONE; return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
glyph_error: glyph_error:
if (im->destroy) {
im->destroy(im);
}
if (im->image) {
free(im->image);
}
if (stroker != NULL) { if (stroker != NULL) {
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }

View File

@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};
if (!OpenClipboard(NULL)) { if (!OpenClipboard(NULL)) {
PyErr_SetString(PyExc_OSError, "failed to open clipboard"); // Maybe the clipboard is temporarily in use by another process.
return NULL; // 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 // find best format as set by clipboard owner

View File

@ -49,7 +49,7 @@ clip32(float in) {
} }
Imaging Imaging
ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
Imaging imOut; Imaging imOut;
int x, y; int x, y;
ImagingSectionCookie cookie; ImagingSectionCookie cookie;

View File

@ -25,7 +25,7 @@
#endif #endif
#endif #endif
#if defined(_WIN32) || defined(__CYGWIN__) #if defined(_WIN32) || defined(__CYGWIN__) /* WIN */
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
@ -37,15 +37,33 @@
#undef WIN32 #undef WIN32
#endif #endif
#else #else /* not WIN */
/* For System that are not Windows, we'll need to define these. */ /* 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 <stdint.h>
#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 #if SIZEOF_SHORT == 2
#define INT16 short #define INT16 short
#elif SIZEOF_INT == 2 #elif SIZEOF_INT == 2
#define INT16 int #define INT16 int
#else #else
#define INT16 short /* most things works just fine anyway... */ #error Cannot find required 16-bit integer type
#endif #endif
#if SIZEOF_SHORT == 4 #if SIZEOF_SHORT == 4
@ -58,19 +76,13 @@
#error Cannot find required 32-bit integer type #error Cannot find required 32-bit integer type
#endif #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 UINT8 unsigned char
#define UINT16 unsigned INT16 #define UINT16 unsigned INT16
#define UINT32 unsigned INT32 #define UINT32 unsigned INT32
#endif #endif /* < C99 */
#endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */ /* assume IEEE; tweak if necessary (patches are welcome) */
#define FLOAT16 UINT16 #define FLOAT16 UINT16

View File

@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b);
extern Imaging extern Imaging
ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); ImagingCrop(Imaging im, int x0, int y0, int x1, int y1);
extern Imaging extern Imaging
ImagingExpand(Imaging im, int x, int y, int mode); ImagingExpand(Imaging im, int x, int y);
extern Imaging extern Imaging
ImagingFill(Imaging im, const void *ink); ImagingFill(Imaging im, const void *ink);
extern int extern int

View File

@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
} }
if (!context->num_resolutions) { 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; params.numresolution -= 1;
} }
} }

View File

@ -37,8 +37,6 @@
#include "Imaging.h" #include "Imaging.h"
#include <string.h> #include <string.h>
int ImagingNewCount = 0;
/* -------------------------------------------------------------------- /* --------------------------------------------------------------------
* Standard image object. * Standard image object.
*/ */

View File

@ -1552,10 +1552,12 @@ static struct {
{"P", "P;4L", 4, unpackP4L}, {"P", "P;4L", 4, unpackP4L},
{"P", "P", 8, copy1}, {"P", "P", 8, copy1},
{"P", "P;R", 8, unpackLR}, {"P", "P;R", 8, unpackLR},
{"P", "L", 8, copy1},
/* palette w. alpha */ /* palette w. alpha */
{"PA", "PA", 16, unpackLA}, {"PA", "PA", 16, unpackLA},
{"PA", "PA;L", 16, unpackLAL}, {"PA", "PA;L", 16, unpackLAL},
{"PA", "LA", 16, unpackLA},
/* true colour */ /* true colour */
{"RGB", "RGB", 24, ImagingUnpackRGB}, {"RGB", "RGB", 24, ImagingUnpackRGB},

View File

@ -13,7 +13,7 @@ extras =
tests tests
commands = commands =
make clean make clean
{envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} -m pip install .
{envpython} selftest.py {envpython} selftest.py
{envpython} -m pytest -W always {posargs} {envpython} -m pytest -W always {posargs}
allowlist_externals = allowlist_externals =

View File

@ -152,9 +152,9 @@ deps = {
"libs": [r"*.lib"], "libs": [r"*.lib"],
}, },
"xz": { "xz": {
"url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download",
"filename": "xz-5.4.2.tar.gz", "filename": "xz-5.4.3.tar.gz",
"dir": "xz-5.4.2", "dir": "xz-5.4.3",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),