Merge branch 'main' into context_manager

This commit is contained in:
Andrew Murray 2024-09-19 08:42:27 +10:00
commit 6af0425283
33 changed files with 191 additions and 138 deletions

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev sway wl-clipboard libopenblas-dev
fi fi
@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports free-threading python3 -m pip install numpy
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -1 +1 @@
cibuildwheel==2.20.0 cibuildwheel==2.21.1

View File

@ -37,7 +37,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ os: [
"macos-14", "macos-latest",
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
@ -56,7 +56,7 @@ jobs:
# M1 only available for 3.10+ # M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" } - { os: "macos-13", python-version: "3.9" }
exclude: exclude:
- { os: "macos-14", python-version: "3.9" } - { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}

View File

@ -21,10 +21,10 @@ if [[ "$MB_ML_VER" != 2014 ]]; then
else else
HARFBUZZ_VERSION=8.5.0 HARFBUZZ_VERSION=8.5.0
fi fi
LIBPNG_VERSION=1.6.43 LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.3 JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2 OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5 XZ_VERSION=5.6.2
TIFF_VERSION=4.6.0 TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16 LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then

View File

@ -13,14 +13,7 @@ else
yum install -y fribidi yum install -y fribidi
fi fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then python3 -m pip install numpy
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
fi
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -102,12 +102,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: "macOS x86_64" - name: "macOS 10.10 x86_64"
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "pp310* cp3{9,10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS arm64" - name: "macOS arm64"
os: macos-14 os: macos-latest
cibw_arch: arm64 cibw_arch: arm64
macosx_deployment_target: "11.0" macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64" - name: "manylinux2014 and musllinux x86_64"
@ -145,7 +151,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:

View File

@ -5,6 +5,15 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Accept float stroke widths #8369
[radarhere]
- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352
[radarhere]
- Improved handling of RGBA palettes when saving GIF images #8366
[radarhere]
- Deprecate isImageType #8364 - Deprecate isImageType #8364
[radarhere] [radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1088,22 +1088,17 @@ class TestImage:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
""" """
with Image.open(os.path.join("Tests/images", path)) as im: with Image.open(os.path.join("Tests/images", path)) as im:
try: with pytest.raises(OSError) as e:
im.load() im.load()
pytest.fail() buffer_overrun = str(e.value) == "buffer overrun when reading image file"
except OSError as e: truncated = "image file is truncated" in str(e.value)
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
assert buffer_overrun or truncated assert buffer_overrun or truncated
def test_fli_overrun2(self) -> None: def test_fli_overrun2(self) -> None:
with Image.open("Tests/images/fli_overrun2.bin") as im: with Image.open("Tests/images/fli_overrun2.bin") as im:
try: with pytest.raises(OSError, match="buffer overrun when reading image file"):
im.seek(1) im.seek(1)
pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self) -> None: def test_exit_fp(self) -> None:
with Image.new("L", (1, 1)) as im: with Image.new("L", (1, 1)) as im:

View File

@ -49,5 +49,7 @@ def test_copy_zero() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_deepcopy() -> None: def test_deepcopy() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
out = copy.deepcopy(im) out = copy.deepcopy(im)
assert out.size == (590, 88) assert out.size == (590, 88)

View File

@ -300,9 +300,7 @@ class TestImageResize:
im.resize((10, 10), -1) im.resize((10, 10), -1)
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first(self) -> None: def test_transposed(self) -> None:
# load() may change the size of the image
# Test that resize() is calling it before getting the size
with Image.open("Tests/images/g4_orientation_5.tif") as img: with Image.open("Tests/images/g4_orientation_5.tif") as img:
im = img.resize((64, 64)) im = img.resize((64, 64))
assert im.size == (64, 64) assert im.size == (64, 64)

View File

@ -92,15 +92,13 @@ def test_no_resize() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first() -> None: def test_transposed() -> None:
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
im.thumbnail((64, 64)) im.thumbnail((64, 64))
assert im.size == (64, 10) assert im.size == (64, 10)
# Test thumbnail(), without draft(),
# on an image that is large enough once load() has changed the size
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
im.thumbnail((590, 88), reducing_gap=None) im.thumbnail((590, 88), reducing_gap=None)
assert im.size == (590, 88) assert im.size == (590, 88)

View File

@ -1369,6 +1369,20 @@ def test_stroke() -> None:
) )
@skip_unless_feature("freetype2")
def test_stroke_float() -> None:
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.text((12, 12), "A", "#f00", font, stroke_width=0.5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1)
@skip_unless_feature("freetype2") @skip_unless_feature("freetype2")
def test_stroke_descender() -> None: def test_stroke_descender() -> None:
# Arrange # Arrange

View File

@ -5,6 +5,7 @@ import os
import re import re
import shutil import shutil
import sys import sys
import tempfile
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, BinaryIO from typing import Any, BinaryIO
@ -460,17 +461,43 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
assert mask.size == (108, 13) assert mask.size == (108, 13)
def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass
with pytest.raises(OSError) as e:
ImageFont.load(tmp.name)
os.unlink(tmp.name)
root = os.path.splitext(tmp.name)[0]
assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}"
def test_load_path_not_found() -> None: def test_load_path_not_found() -> None:
# Arrange # Arrange
filename = "somefilenamethatdoesntexist.ttf" filename = "somefilenamethatdoesntexist.ttf"
# Act/Assert # Act/Assert
with pytest.raises(OSError): with pytest.raises(OSError) as e:
ImageFont.load_path(filename) ImageFont.load_path(filename)
# The file doesn't exist, so don't suggest `load`
assert filename in str(e.value)
assert "did you mean" not in str(e.value)
with pytest.raises(OSError): with pytest.raises(OSError):
ImageFont.truetype(filename) ImageFont.truetype(filename)
def test_load_path_existing_path() -> None:
with tempfile.NamedTemporaryFile() as tmp:
with pytest.raises(OSError) as e:
ImageFont.load_path(tmp.name)
# The file exists, so the error message suggests to use `load` instead
assert tmp.name in str(e.value)
assert " did you mean" in str(e.value)
def test_load_non_font_bytes() -> None: def test_load_non_font_bytes() -> None:
with open("Tests/images/hopper.jpg", "rb") as f: with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -115,7 +115,7 @@ def test_ipythonviewer() -> None:
test_viewer = viewer test_viewer = viewer
break break
else: else:
pytest.fail() pytest.fail("IPythonViewer not found")
im = hopper() im = hopper()
assert test_viewer.show(im) == 1 assert test_viewer.show(im) == 1

View File

@ -60,6 +60,18 @@ class TestImageWinDib:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageWin.Dib(mode) ImageWin.Dib(mode)
def test_dib_hwnd(self) -> None:
mode = "RGBA"
size = (128, 128)
wnd = 0
dib = ImageWin.Dib(mode, size)
hwnd = ImageWin.HWND(wnd)
dib.expose(hwnd)
dib.draw(hwnd, (0, 0) + size)
assert isinstance(dib.query_palette(hwnd), int)
def test_dib_paste(self) -> None: def test_dib_paste(self) -> None:
# Arrange # Arrange
im = hopper() im = hopper()

View File

@ -238,8 +238,10 @@ def test_zero_size() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first() -> None: def test_transposed() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
a = numpy.array(im) a = numpy.array(im)
assert a.shape == (88, 590) assert a.shape == (88, 590)

View File

@ -23,6 +23,13 @@ Python 3.8
Pillow has dropped support for Python 3.8, Pillow has dropped support for Python 3.8,
which reached end-of-life in October 2024. which reached end-of-life in October 2024.
Python 3.12 on macOS <= 10.12
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The latest version of Python 3.12 only supports macOS versions 10.13 and later,
and so Pillow has also updated the deployment target for its prebuilt Python 3.12
wheels.
PSFile PSFile
^^^^^^ ^^^^^^
@ -45,6 +52,11 @@ TiffImagePlugin IFD_LEGACY_API
An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed.
WebP 0.4
^^^^^^^^
Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported.
Deprecations Deprecations
============ ============
@ -115,10 +127,18 @@ TODO
API Additions API Additions
============= =============
TODO Writing XMP bytes to JPEG and MPO
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO XMP data can now be saved to JPEG files using an ``xmp`` argument::
im.save("out.jpg", xmp=b"test")
The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving
either JPEG or MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
Other Changes Other Changes
============= =============
@ -132,6 +152,10 @@ of 3.13.0 final (2024-10-01, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.
Support has also been added for the experimental free-threaded mode of :pep:`703`.
Python 3.13 only supports macOS versions 10.13 and later.
C-level Flags C-level Flags
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -125,7 +125,6 @@ lint.ignore = [
"PT007", # pytest-parametrize-values-wrong-type "PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad "PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements "PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except "PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11 "PYI034", # flake8-pyi: typing.Self added in Python 3.11

View File

@ -2272,7 +2272,6 @@ class Image:
msg = "reducing_gap must be 1.0 or greater" msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg) raise ValueError(msg)
self.load()
if box is None: if box is None:
box = (0, 0) + self.size box = (0, 0) + self.size
@ -2724,27 +2723,18 @@ class Image:
) )
return x, y return x, y
box = None preserved_size = preserve_aspect_ratio()
final_size: tuple[int, int] if preserved_size is None:
if reducing_gap is not None: return
preserved_size = preserve_aspect_ratio() final_size = preserved_size
if preserved_size is None:
return
final_size = preserved_size
box = None
if reducing_gap is not None:
res = self.draft( res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
) )
if res is not None: if res is not None:
box = res[1] box = res[1]
if box is None:
self.load()
# load() may have changed the size of the image
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
final_size = preserved_size
if self.size != final_size: if self.size != final_size:
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)

View File

@ -415,7 +415,7 @@ class ImageFile(Image.Image):
def load_prepare(self) -> None: def load_prepare(self) -> None:
# create image memory if necessary # create image memory if necessary
if self._im is None or self.im.mode != self.mode or self.im.size != self.size: if self._im is None:
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
# create palette (optional) # create palette (optional)
if self.mode == "P": if self.mode == "P":

View File

@ -98,11 +98,13 @@ class ImageFont:
def _load_pilfont(self, filename: str) -> None: def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
image: ImageFile.ImageFile | None = None image: ImageFile.ImageFile | None = None
root = os.path.splitext(filename)[0]
for ext in (".png", ".gif", ".pbm"): for ext in (".png", ".gif", ".pbm"):
if image: if image:
image.close() image.close()
try: try:
fullname = os.path.splitext(filename)[0] + ext fullname = root + ext
image = Image.open(fullname) image = Image.open(fullname)
except Exception: except Exception:
pass pass
@ -112,7 +114,8 @@ class ImageFont:
else: else:
if image: if image:
image.close() image.close()
msg = "cannot find glyph data file"
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
raise OSError(msg) raise OSError(msg)
self.file = fullname self.file = fullname
@ -224,7 +227,7 @@ class FreeTypeFont:
raise core.ex raise core.ex
if size <= 0: if size <= 0:
msg = "font size must be greater than 0" msg = f"font size must be greater than 0, not {size}"
raise ValueError(msg) raise ValueError(msg)
self.path = font self.path = font
@ -783,8 +786,9 @@ class TransposedFont:
def load(filename: str) -> ImageFont: def load(filename: str) -> ImageFont:
""" """
Load a font file. This function loads a font object from the given Load a font file. This function loads a font object from the given
bitmap font file, and returns the corresponding font object. bitmap font file, and returns the corresponding font object. For loading TrueType
or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
:param filename: Name of font file. :param filename: Name of font file.
:return: A font object. :return: A font object.
@ -804,9 +808,10 @@ def truetype(
) -> FreeTypeFont: ) -> FreeTypeFont:
""" """
Load a TrueType or OpenType font from a file or file-like object, Load a TrueType or OpenType font from a file or file-like object,
and create a font object. and create a font object. This function loads a font object from the given
This function loads a font object from the given file or file-like file or file-like object, and creates a font object for a font of the given
object, and creates a font object for a font of the given size. size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
and :py:func:`~PIL.ImageFont.load_path`.
Pillow uses FreeType to open font files. On Windows, be aware that FreeType Pillow uses FreeType to open font files. On Windows, be aware that FreeType
will keep the file open as long as the FreeTypeFont object exists. Windows will keep the file open as long as the FreeTypeFont object exists. Windows
@ -942,7 +947,10 @@ def load_path(filename: str | bytes) -> ImageFont:
return load(os.path.join(directory, filename)) return load(os.path.join(directory, filename))
except OSError: except OSError:
pass pass
msg = "cannot find font file" msg = f'cannot find font file "{filename}" in sys.path'
if os.path.exists(filename):
msg += f', did you mean ImageFont.load("{filename}") instead?'
raise OSError(msg) raise OSError(msg)

View File

@ -98,14 +98,15 @@ class Dib:
HDC or HWND instance. In PythonWin, you can use HDC or HWND instance. In PythonWin, you can use
``CDC.GetHandleAttrib()`` to get a suitable handle. ``CDC.GetHandleAttrib()`` to get a suitable handle.
""" """
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle_int)
try: try:
self.image.expose(dc) self.image.expose(dc)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle_int, dc)
else: else:
self.image.expose(handle) self.image.expose(handle_int)
def draw( def draw(
self, self,
@ -124,14 +125,15 @@ class Dib:
""" """
if src is None: if src is None:
src = (0, 0) + self.size src = (0, 0) + self.size
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle_int)
try: try:
self.image.draw(dc, dst, src) self.image.draw(dc, dst, src)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle_int, dc)
else: else:
self.image.draw(handle, dst, src) self.image.draw(handle_int, dst, src)
def query_palette(self, handle: int | HDC | HWND) -> int: def query_palette(self, handle: int | HDC | HWND) -> int:
""" """
@ -148,14 +150,15 @@ class Dib:
:return: The number of entries that were changed (if one or more entries, :return: The number of entries that were changed (if one or more entries,
this indicates that the image should be redrawn). this indicates that the image should be redrawn).
""" """
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
handle = self.image.getdc(handle) handle = self.image.getdc(handle_int)
try: try:
result = self.image.query_palette(handle) result = self.image.query_palette(handle)
finally: finally:
self.image.releasedc(handle, handle) self.image.releasedc(handle, handle)
else: else:
result = self.image.query_palette(handle) result = self.image.query_palette(handle_int)
return result return result
def paste( def paste(

View File

@ -141,7 +141,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes:
dobj = zlib.decompressobj() dobj = zlib.decompressobj()
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
if dobj.unconsumed_tail: if dobj.unconsumed_tail:
msg = "Decompressed Data Too Large" msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK"
raise ValueError(msg) raise ValueError(msg)
return plaintext return plaintext

View File

@ -1195,8 +1195,8 @@ class TiffImageFile(ImageFile.ImageFile):
# Create a new core image object on second and # Create a new core image object on second and
# subsequent frames in the image. Image may be # subsequent frames in the image. Image may be
# different size/mode. # different size/mode.
Image._decompression_bomb_check(self.size) Image._decompression_bomb_check(self._tile_size)
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self._tile_size)
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError): if isinstance(self._fp, DeferredError):
@ -1278,6 +1278,11 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff() return self._load_libtiff()
return super().load() return super().load()
def load_prepare(self) -> None:
if self._im is None:
self.im = Image.core.new(self.mode, self._tile_size)
ImageFile.ImageFile.load_prepare(self)
def load_end(self) -> None: def load_end(self) -> None:
# allow closing if we're on the first frame, there's no next # allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below. # This is the ImageFile.load path only, libtiff specific below.
@ -1421,7 +1426,12 @@ class TiffImageFile(ImageFile.ImageFile):
if not isinstance(xsize, int) or not isinstance(ysize, int): if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions" msg = "Invalid dimensions"
raise ValueError(msg) raise ValueError(msg)
self._size = xsize, ysize self._tile_size = xsize, ysize
orientation = self.tag_v2.get(ExifTags.Base.Orientation)
if orientation in (5, 6, 7, 8):
self._size = ysize, xsize
else:
self._size = xsize, ysize
logger.debug("- size: %s", self.size) logger.debug("- size: %s", self.size)
@ -1564,7 +1574,7 @@ class TiffImageFile(ImageFile.ImageFile):
if STRIPOFFSETS in self.tag_v2: if STRIPOFFSETS in self.tag_v2:
offsets = self.tag_v2[STRIPOFFSETS] offsets = self.tag_v2[STRIPOFFSETS]
h = self.tag_v2.get(ROWSPERSTRIP, ysize) h = self.tag_v2.get(ROWSPERSTRIP, ysize)
w = self.size[0] w = xsize
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
@ -1598,9 +1608,9 @@ class TiffImageFile(ImageFile.ImageFile):
) )
) )
x = x + w x = x + w
if x >= self.size[0]: if x >= xsize:
x, y = 0, y + h x, y = 0, y + h
if y >= self.size[1]: if y >= ysize:
x = y = 0 x = y = 0
layer += 1 layer += 1
else: else:

View File

@ -833,7 +833,7 @@ font_render(FontObject *self, PyObject *args) {
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? */
int color = 0; /* is FT_LOAD_COLOR enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */
int stroke_width = 0; float stroke_width = 0;
PY_LONG_LONG foreground_ink_long = 0; PY_LONG_LONG foreground_ink_long = 0;
unsigned int foreground_ink; unsigned int foreground_ink;
const char *mode = NULL; const char *mode = NULL;
@ -853,7 +853,7 @@ font_render(FontObject *self, PyObject *args) {
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"OO|zzOzizLffO:render", "OO|zzOzfzLffO:render",
&string, &string,
&fill, &fill,
&mode, &mode,
@ -919,8 +919,8 @@ font_render(FontObject *self, PyObject *args) {
return NULL; return NULL;
} }
width += stroke_width * 2 + ceil(x_start); width += ceil(stroke_width * 2 + x_start);
height += stroke_width * 2 + ceil(y_start); height += ceil(stroke_width * 2 + y_start);
image = PyObject_CallFunction(fill, "ii", width, height); image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) { if (image == Py_None) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
@ -934,8 +934,8 @@ font_render(FontObject *self, PyObject *args) {
Py_XDECREF(imageId); Py_XDECREF(imageId);
im = (Imaging)id; im = (Imaging)id;
x_offset -= stroke_width; x_offset = round(x_offset - stroke_width);
y_offset -= stroke_width; y_offset = round(y_offset - stroke_width);
if (count == 0 || width == 0 || height == 0) { if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return Py_BuildValue("N(ii)", image, x_offset, y_offset); return Py_BuildValue("N(ii)", image, x_offset, y_offset);
@ -950,7 +950,7 @@ font_render(FontObject *self, PyObject *args) {
FT_Stroker_Set( FT_Stroker_Set(
stroker, stroker,
(FT_Fixed)stroke_width * 64, (FT_Fixed)round(stroke_width * 64),
FT_STROKER_LINECAP_ROUND, FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_ROUND, FT_STROKER_LINEJOIN_ROUND,
0 0
@ -988,8 +988,8 @@ font_render(FontObject *self, PyObject *args) {
} }
/* set pen position to text origin */ /* set pen position to text origin */
x = (-x_min + stroke_width + x_start) * 64; x = round((-x_min + stroke_width + x_start) * 64);
y = (-y_max + (-stroke_width) - y_start) * 64; y = round((-y_max + (-stroke_width) - y_start) * 64);
if (stroker == NULL) { if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER; load_flags |= FT_LOAD_RENDER;

View File

@ -1411,10 +1411,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
} }
#endif #endif
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -104,10 +104,3 @@ typedef struct {
int plt; int plt;
} JPEG2KENCODESTATE; } JPEG2KENCODESTATE;
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -979,10 +979,3 @@ ImagingJpeg2KVersion(void) {
} }
#endif /* HAVE_OPENJPEG */ #endif /* HAVE_OPENJPEG */
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -652,10 +652,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) {
} }
#endif /* HAVE_OPENJPEG */ #endif /* HAVE_OPENJPEG */
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -44,7 +44,6 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view);
typedef struct { typedef struct {
PyObject_HEAD Py_ssize_t count; PyObject_HEAD Py_ssize_t count;
double *xy; double *xy;
int index; /* temporary use, e.g. in decimate */
} PyPathObject; } PyPathObject;
static PyTypeObject PyPathType; static PyTypeObject PyPathType;

View File

@ -1,11 +1,11 @@
README README
------ ------
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux [cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged
wheels for tagged versions of Pillow. versions of Pillow.
This directory contains [multibuild](https://github.com/multi-build/multibuild) to This directory contains [multibuild](https://github.com/multi-build/multibuild) to
build dependencies for the wheels, and dependency licenses to be included. build dependencies for macOS and Linux wheels, and dependency licenses to be included.
Archives Archives
-------- --------
@ -30,6 +30,3 @@ Wheels
Wheels are Wheels are
[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
Windows wheels are created separately. They are
[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain).

View File

@ -113,13 +113,13 @@ V = {
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.15", "FRIBIDI": "1.0.15",
"HARFBUZZ": "9.0.0", "HARFBUZZ": "9.0.0",
"JPEGTURBO": "3.0.3", "JPEGTURBO": "3.0.4",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.43", "LIBPNG": "1.6.44",
"LIBWEBP": "1.4.0", "LIBWEBP": "1.4.0",
"OPENJPEG": "2.5.2", "OPENJPEG": "2.5.2",
"TIFF": "4.6.0", "TIFF": "4.6.0",
"XZ": "5.4.5", "XZ": "5.6.2",
"ZLIB": "1.3.1", "ZLIB": "1.3.1",
} }
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = {
"libs": [r"*.lib"], "libs": [r"*.lib"],
}, },
"xz": { "xz": {
"url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz",
"filename": f"xz-{V['XZ']}.tar.gz", "filename": f"xz-{V['XZ']}.tar.gz",
"dir": f"xz-{V['XZ']}", "dir": f"xz-{V['XZ']}",
"license": "COPYING", "license": "COPYING",