mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
Merge branch 'main' into int_def
This commit is contained in:
commit
68edbbca94
|
@ -22,7 +22,8 @@ set -e
|
|||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
|
@ -41,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||
python3 -m pip install pyqt6
|
||||
fi
|
||||
|
|
|
@ -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/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,
|
||||
|
|
4
.github/workflows/test-windows.yml
vendored
4
.github/workflows/test-windows.yml
vendored
|
@ -65,8 +65,8 @@ jobs:
|
|||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
- name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -84,7 +84,9 @@ jobs:
|
|||
python3 -m pip install pytest-reverse
|
||||
fi
|
||||
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
|
||||
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
|
||||
xvfb-run -s '-screen 0 1024x768x24' sway&
|
||||
export WAYLAND_DISPLAY=wayland-1
|
||||
.ci/test.sh
|
||||
else
|
||||
.ci/test.sh
|
||||
fi
|
||||
|
|
|
@ -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
|
||||
|
|
24
CHANGES.rst
24
CHANGES.rst
|
@ -5,6 +5,30 @@ Changelog (Pillow)
|
|||
10.0.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- 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]
|
||||
|
||||
- Added _repr_jpeg_() for IPython display_jpeg #7135
|
||||
[n3011, radarhere, nulano]
|
||||
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -98,3 +98,18 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
|
||||
@pytest.mark.skipif(
|
||||
(
|
||||
sys.platform != "linux"
|
||||
or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy"))
|
||||
),
|
||||
reason="Linux with wl-clipboard only",
|
||||
)
|
||||
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
|
||||
def test_grabclipboard_wl_clipboard(self, ext):
|
||||
image_path = "Tests/images/hopper." + ext
|
||||
with open(image_path, "rb") as fp:
|
||||
subprocess.call(["wl-copy"], stdin=fp)
|
||||
im = ImageGrab.grabclipboard()
|
||||
assert_image_equal_tofile(im, image_path)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Documentation: https://docs.codecov.io/docs/codecov-yaml
|
||||
# Documentation: https://docs.codecov.com/docs/codecov-yaml
|
||||
|
||||
codecov:
|
||||
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]"
|
||||
# https://github.com/codecov/support/issues/363
|
||||
# https://docs.codecov.io/docs/comparing-commits
|
||||
# https://docs.codecov.com/docs/comparing-commits
|
||||
allow_coverage_offsets: true
|
||||
|
||||
comment: false
|
||||
|
|
|
@ -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
|
||||
-------
|
||||
|
|
|
@ -448,6 +448,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 |
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -43,7 +43,8 @@ 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
|
||||
kernel weights.
|
||||
|
|
|
@ -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
|
||||
Image._decompression_bomb_check(size)
|
||||
im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0)
|
||||
if min(size):
|
||||
self.font.render(
|
||||
im, size, offset = self.font.render(
|
||||
text,
|
||||
im.id,
|
||||
Image.core.fill,
|
||||
mode,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
anchor,
|
||||
ink,
|
||||
start[0],
|
||||
start[1],
|
||||
Image.MAX_IMAGE_PIXELS,
|
||||
)
|
||||
Image._decompression_bomb_check(size)
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -142,19 +141,29 @@ def grabclipboard():
|
|||
return None
|
||||
else:
|
||||
if shutil.which("wl-paste"):
|
||||
output = subprocess.check_output(["wl-paste", "-l"]).decode()
|
||||
mimetypes = output.splitlines()
|
||||
if "image/png" in mimetypes:
|
||||
mimetype = "image/png"
|
||||
elif mimetypes:
|
||||
mimetype = mimetypes[0]
|
||||
else:
|
||||
mimetype = None
|
||||
|
||||
args = ["wl-paste"]
|
||||
if mimetype:
|
||||
args.extend(["-t", mimetype])
|
||||
elif shutil.which("xclip"):
|
||||
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1894,6 +1894,10 @@ class AppendingTiffWriter:
|
|||
8, # srational
|
||||
4, # float
|
||||
8, # double
|
||||
4, # ifd
|
||||
2, # unicode
|
||||
4, # complex
|
||||
8, # long8
|
||||
]
|
||||
|
||||
# StripOffsets = 273
|
||||
|
|
|
@ -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 *
|
||||
|
|
239
src/_imagingft.c
239
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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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 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,8 +860,41 @@ 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 (width * height > PyLong_AsLong(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);
|
||||
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) {
|
||||
|
@ -837,15 +911,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!
|
||||
|
@ -1042,7 +1107,7 @@ 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 (stroker != NULL) {
|
||||
|
|
|
@ -436,10 +436,16 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
|
|||
UINT formats[] = {CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0};
|
||||
LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};
|
||||
|
||||
if (!OpenClipboard(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
|
||||
format = 0;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -53,10 +53,6 @@
|
|||
#define UINT16 uint16_t
|
||||
#define INT32 int32_t
|
||||
#define UINT32 uint32_t
|
||||
#ifdef INT64_MAX
|
||||
#define INT64 int64_t
|
||||
#define UINT64 uint64_t
|
||||
#endif
|
||||
|
||||
#else /* < C99 */
|
||||
|
||||
|
@ -80,20 +76,9 @@
|
|||
#error Cannot find required 32-bit integer type
|
||||
#endif
|
||||
|
||||
#if SIZEOF_LONG == 8
|
||||
#define INT64 long
|
||||
#elif SIZEOF_LONG_LONG == 8
|
||||
#define INT64 long long
|
||||
#else
|
||||
#warning Cannot find required 64-bit integer type
|
||||
#endif
|
||||
|
||||
#define UINT8 unsigned char
|
||||
#define UINT16 unsigned INT16
|
||||
#define UINT32 unsigned INT32
|
||||
#ifdef INT64
|
||||
#define UINT64 unsigned INT64
|
||||
#endif
|
||||
|
||||
#endif /* < C99 */
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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