mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-24 17:06:16 +03:00
Merge remote-tracking branch 'upstream/main' into add-pyproject.toml
# Conflicts: # pyproject.toml
This commit is contained in:
commit
c068af7630
|
@ -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
|
||||
|
|
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/test-cygwin.yml
vendored
2
.github/workflows/test-cygwin.yml
vendored
|
@ -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: |
|
||||
|
|
1
.github/workflows/test-docker.yml
vendored
1
.github/workflows/test-docker.yml
vendored
|
@ -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,
|
||||
|
|
2
.github/workflows/test-mingw.yml
vendored
2
.github/workflows/test-mingw.yml
vendored
|
@ -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: |
|
||||
|
|
4
.github/workflows/test-windows.yml
vendored
4
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -29,8 +29,8 @@ jobs:
|
|||
"ubuntu-latest",
|
||||
]
|
||||
python-version: [
|
||||
"pypy3.10",
|
||||
"pypy3.9",
|
||||
"pypy3.8",
|
||||
"3.12-dev",
|
||||
"3.11",
|
||||
"3.10",
|
||||
|
|
|
@ -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
|
||||
|
|
36
CHANGES.rst
36
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]
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ graft src
|
|||
graft depends
|
||||
graft winbuild
|
||||
graft docs
|
||||
graft _custom_build
|
||||
|
||||
# build/src control detritus
|
||||
exclude .appveyor.yml
|
||||
|
|
9
Makefile
9
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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
BIN
Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf
Normal file
BIN
Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf
Normal file
Binary file not shown.
BIN
Tests/images/orientation_rectangle.jpg
Normal file
BIN
Tests/images/orientation_rectangle.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 669 B |
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
56
_custom_build/backend.py
Executable file
56
_custom_build/backend.py
Executable 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
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
-------
|
||||
|
|
|
@ -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
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
1
setup.py
1
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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"<tiff:Orientation>([0-9])</tiff:Orientation>",
|
||||
):
|
||||
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()
|
||||
|
|
|
@ -17,7 +17,7 @@ import subprocess
|
|||
import sys
|
||||
from shlex import quote
|
||||
|
||||
from PIL import Image
|
||||
from . import Image
|
||||
|
||||
_viewers = []
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 *
|
||||
|
|
267
src/_imagingft.c
267
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
@ -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 <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
|
||||
#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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
#include "Imaging.h"
|
||||
#include <string.h>
|
||||
|
||||
int ImagingNewCount = 0;
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
* Standard image object.
|
||||
*/
|
||||
|
|
|
@ -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},
|
||||
|
|
2
tox.ini
2
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 =
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in New Issue
Block a user