mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26: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
|
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
|
||||||
|
|
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.
|
- 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
|
||||||
|
|
||||||
|
|
2
.github/workflows/test-cygwin.yml
vendored
2
.github/workflows/test-cygwin.yml
vendored
|
@ -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: |
|
||||||
|
|
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-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,
|
||||||
|
|
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
|
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: |
|
||||||
|
|
4
.github/workflows/test-windows.yml
vendored
4
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
36
CHANGES.rst
36
CHANGES.rst
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
9
Makefile
9
Makefile
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
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
|
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)
|
||||||
|
|
|
@ -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",
|
||||||
(
|
(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
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.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"))
|
||||||
|
|
|
@ -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
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -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
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
]
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
267
src/_imagingft.c
267
src/_imagingft.c
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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},
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -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 =
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user