Merge branch 'main' into multiband

This commit is contained in:
Andrew Murray 2024-10-05 17:52:47 +10:00 committed by GitHub
commit 0cbc26537b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 797 additions and 409 deletions

View File

@ -34,8 +34,8 @@ install:
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- choco install ghostscript --version=10.4.0
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
@ -53,9 +53,8 @@ test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe

View File

@ -21,7 +21,7 @@ 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\
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev
fi
@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports free-threading
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
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -1 +1 @@
cibuildwheel==2.20.0
cibuildwheel==2.21.2

3
.ci/test.cmd Normal file
View File

@ -0,0 +1,3 @@
python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image"
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE

View File

@ -6,11 +6,13 @@ on:
- "**"
paths:
- ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh"
- "**.c"
- "**.h"
pull_request:
paths:
- ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh"
- "**.c"
- "**.h"
workflow_dispatch:
@ -24,8 +26,6 @@ concurrency:
jobs:
Fuzzing:
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
if: false
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers

View File

@ -80,8 +80,7 @@ jobs:
- name: Test Pillow
run: |
python3 selftest.py --installed
python3 -c "from PIL import Image"
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
.ci/test.sh
- name: Upload coverage
uses: codecov/codecov-action@v4

View File

@ -86,8 +86,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@ -190,8 +190,8 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH%
python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd
- name: Prepare to upload errors

View File

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

View File

@ -16,11 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3
HARFBUZZ_VERSION=10.0.1
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@ -50,7 +50,7 @@ fi
function build_brotli {
local cmake=$(get_modern_cmake)
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@ -60,6 +60,19 @@ function build_brotli {
fi
}
function build_harfbuzz {
python3 -m pip install meson ninja
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \
&& meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \
&& meson install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
fi
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local
@ -109,15 +122,7 @@ function build {
build_freetype
fi
if [ -z "$IS_MACOS" ]; then
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
fi
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
if [ -z "$IS_MACOS" ]; then
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
fi
build_harfbuzz
}
# Any stuff that you need to do before you start building the wheels

View File

@ -13,14 +13,7 @@ else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# 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
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -102,12 +102,23 @@ jobs:
fail-fast: false
matrix:
include:
- name: "macOS x86_64"
- name: "macOS 10.10 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{9,10,11}*"
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 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-14
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
@ -145,7 +156,7 @@ jobs:
- uses: actions/upload-artifact@v4
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
windows:
@ -267,7 +278,7 @@ jobs:
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}

View File

@ -5,6 +5,57 @@ Changelog (Pillow)
11.0.0 (unreleased)
-------------------
- Support all resampling filters when resizing I;16* images #8422
[radarhere]
- Free memory on early return #8413
[radarhere]
- Cast int before potentially exceeding INT_MAX #8402
[radarhere]
- Check image value before use #8400
[radarhere]
- Improved copying imagequant libraries #8420
[radarhere]
- Use Capsule for WebP saving #8386
[homm, radarhere]
- Fixed writing multiple StripOffsets to TIFF #8317
[Yay295, radarhere]
- Fix dereference before checking for NULL in ImagingTransformAffine #8398
[PavlNekrasov]
- Use transposed size after opening for TIFF images #8390
[radarhere, homm]
- Improve ImageFont error messages #8338
[yngvem, radarhere, hugovk]
- Mention MAX_TEXT_CHUNK limit in PNG error message #8391
[radarhere]
- Cast Dib handle to int #8385
[radarhere]
- 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
[radarhere]
- Support converting more modes to LAB by converting to RGBA first #8358
[radarhere]
- Deprecate support for FreeType 2.9.0 #8356
[hugovk, radarhere]

View File

@ -117,7 +117,7 @@ lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
python3 -m ruff check --fix .
.PHONY: mypy
mypy:

View File

@ -51,7 +51,7 @@ As of 2019, Pillow development is
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
<a href="https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow"><img
<a href="https://issues.oss-fuzz.com/issues?q=title:pillow"><img
alt="Fuzzing Status"
src="https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg"></a>
</td>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1429,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA")
assert reloaded_rgba.load()[0, 0][3] == 0
def test_optimizing_p_rgba(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1)
d.ellipse([(40, 40), (60, 60)], fill=1)
data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(data, "RGBA")
im2 = Image.new("P", (100, 100))
im2.putpalette(data, "RGBA")
im1.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.n_frames == 2

View File

@ -63,8 +63,8 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread:
reread.size = (16, 16, 2)
reread.load()
reread.size = (16, 16)
reread.load(2)
assert_image_equal(reread, provided_im)
@ -87,14 +87,21 @@ def test_sizes() -> None:
for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load()
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Test using load() with scale
im.size = (w, h)
im.load(scale=r)
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Check that we cannot load an incorrect size
with pytest.raises(ValueError):
im.size = (1, 1)
im.size = (1, 2)
def test_older_icon() -> None:
@ -105,8 +112,8 @@ def test_older_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)
@ -122,8 +129,8 @@ def test_jp2_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)

View File

@ -108,7 +108,8 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# multistrip support not yet implemented
# The data type of this file's StripOffsets tag is LONG8,
# which is not yet supported for offset data when saving multiple frames.
del im.tag_v2[273]
outfile = str(tmp_path / "temp.tif")

View File

@ -181,6 +181,29 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
def test_save_multiple_stripoffsets() -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456)
assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG
# all values are in little-endian
assert ifd.tobytes() == (
# number of tags == 1
b"\x01\x00"
# tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes)
# TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18
# where STRIPOFFSETS is 273, LONG is 4
# and 18 is the offset of the tag data
b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00"
# end of entries
b"\x00\x00\x00\x00"
# 26 is the total number of bytes output,
# the offset for any auxiliary strip data that will then be appended
# (123 + 26, 456 + 26) == (149, 482)
b"\x95\x00\x00\x00\xe2\x01\x00\x00"
)
def test_no_duplicate_50741_tag() -> None:
assert TAG_IDS["MakerNoteSafety"] == 50741
assert TAG_IDS["BestQualityScale"] == 50780

View File

@ -72,7 +72,7 @@ class TestFileWebp:
def _roundtrip(
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
hopper(mode).save(temp_file, **args)
with Image.open(temp_file) as image:
@ -116,7 +116,7 @@ class TestFileWebp:
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
@ -127,6 +127,11 @@ class TestFileWebp:
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)
def test_unsupported_image_mode(self) -> None:
im = Image.new("1", (1, 1))
with pytest.raises(ValueError):
_webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "")
def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
self._roundtrip(
@ -151,18 +156,16 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
im.save(temp_file, method=0)
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
im.save(tmp_path / "temp.webp")
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
@ -187,9 +190,8 @@ class TestFileWebp:
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with warnings.catch_warnings():
image.save(temp_file)
image.save(tmp_path / "temp.webp")
def test_file_pointer_could_be_reused(self) -> None:
file_path = "Tests/images/hopper.webp"
@ -204,15 +206,16 @@ class TestFileWebp:
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)
def test_background_from_gif(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save P mode GIF with background
@ -220,11 +223,10 @@ class TestFileWebp:
original_value = im.convert("RGB").getpixel((1, 1))
# Save as WEBP
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save as GIF
out_gif = str(tmp_path / "temp.gif")
out_gif = tmp_path / "temp.gif"
with Image.open(out_webp) as im:
im.save(out_gif)
@ -234,10 +236,10 @@ class TestFileWebp:
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"
with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
with Image.open(out_webp) as reloaded:
@ -245,7 +247,7 @@ class TestFileWebp:
assert reloaded.info["duration"] == 1000
def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None

View File

@ -1086,22 +1086,17 @@ class TestImage:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
"""
with Image.open(os.path.join("Tests/images", path)) as im:
try:
with pytest.raises(OSError) as e:
im.load()
pytest.fail()
except OSError as e:
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
buffer_overrun = str(e.value) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e.value)
assert buffer_overrun or truncated
def test_fli_overrun2(self) -> None:
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)
pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self) -> None:
with Image.new("L", (1, 1)) as im:
@ -1117,6 +1112,10 @@ class TestImage:
assert len(caplog.records) == 0
assert im.fp is None
def test_deprecation(self) -> None:
with pytest.warns(DeprecationWarning):
assert not Image.isImageType(None)
class TestImageBytes:
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])

View File

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

View File

@ -44,9 +44,19 @@ class TestImagingCoreResize:
self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError):
self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError):
self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR)
for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]:
for mode in [
"L",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"F",
"RGB",
"RGBA",
"CMYK",
"YCbCr",
]:
im = hopper(mode)
r = self.resize(im, (15, 12), Image.Resampling.BILINEAR)
assert r.mode == mode
@ -300,21 +310,19 @@ class TestImageResize:
im.resize((10, 10), "unknown")
@skip_unless_feature("libtiff")
def test_load_first(self) -> None:
# load() may change the size of the image
# Test that resize() is calling it before getting the size
def test_transposed(self) -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im = im.resize((64, 64))
assert im.size == (64, 64)
@pytest.mark.parametrize("mode", ("L", "RGB", "I", "F"))
@pytest.mark.parametrize(
"mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F")
)
def test_default_filter_bicubic(self, mode: str) -> None:
im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
@pytest.mark.parametrize(
"mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16")
)
@pytest.mark.parametrize("mode", ("1", "P", "BGR;15", "BGR;16"))
def test_default_filter_nearest(self, mode: str) -> None:
im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))

View File

@ -92,15 +92,13 @@ def test_no_resize() -> None:
@skip_unless_feature("libtiff")
def test_load_first() -> None:
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
def test_transposed() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
im.thumbnail((64, 64))
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:
im.thumbnail((590, 88), reducing_gap=None)
assert im.size == (590, 88)

View File

@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None:
assert value[:3] == (0, 255, 255)
def test_cmyk_lab() -> None:
im = Image.new("CMYK", (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (255, 128, 128)
def test_deprecation() -> None:
with pytest.warns(DeprecationWarning):
assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")

View File

@ -1368,6 +1368,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")
def test_stroke_descender() -> None:
# Arrange

View File

@ -5,6 +5,7 @@ import os
import re
import shutil
import sys
import tempfile
from io import BytesIO
from pathlib import Path
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)
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:
# Arrange
filename = "somefilenamethatdoesntexist.ttf"
# Act/Assert
with pytest.raises(OSError):
with pytest.raises(OSError) as e:
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):
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:
with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError):

View File

@ -204,6 +204,17 @@ def test_overflow_segfault() -> None:
x[i] = b"0" * 16
def test_compact_within_map() -> None:
p = ImagePath.Path([0, 1])
def map_func(x: float, y: float) -> tuple[float, float]:
p.compact()
return 0, 0
with pytest.raises(ValueError):
p.map(map_func)
class Evil:
def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000)

View File

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

View File

@ -60,6 +60,18 @@ class TestImageWinDib:
with pytest.raises(ValueError):
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:
# Arrange
im = hopper()

View File

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

View File

@ -23,14 +23,14 @@ else
cargo cinstall --prefix=/usr --destdir=.
# Copy into place
sudo cp usr/lib/libimagequant.so* /usr/lib/
sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \;
sudo cp usr/include/libimagequant.h /usr/include/
if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-$archive_name
mkdir ~/cache-$archive_name
cp usr/lib/libimagequant.so* ~/cache-$archive_name/
find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \;
cp usr/include/libimagequant.h ~/cache-$archive_name/
fi

View File

@ -2,7 +2,7 @@
# install raqm
archive=libraqm-0.10.1
archive=libraqm-0.10.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -122,6 +122,22 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
deprecated. Instead, ``load(scale)`` can be used.
Image isImageType()
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)``
instead.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -324,12 +324,19 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
**sizes**
A list of supported sizes found in this icon file; these are a
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina
icon and 1 for a standard icon. You *are* permitted to use this 3-tuple
format for the :py:attr:`~PIL.Image.Image.size` property if you set it
before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size
will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you
ask for ``(512, 512, 2)``, the final value of
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).
icon and 1 for a standard icon.
.. _icns-loading:
Loading
~~~~~~~
You can call the :py:meth:`~PIL.Image.Image.load` method with the following parameter.
**scale**
Affects the scale of the resultant image. If the size is set to ``(512, 512)``,
after loading at scale 2, the final value of :py:attr:`~PIL.Image.Image.size` will
be ``(1024, 1024)``.
.. _icns-saving:

View File

@ -54,7 +54,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:alt: Tidelift
.. image:: https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg
:target: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow
:target: https://issues.oss-fuzz.com/issues?q=title:pillow
:alt: Fuzzing Status
.. image:: https://img.shields.io/pypi/v/pillow.svg

View File

@ -17,13 +17,13 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Operating system | Tested Python versions | Tested architecture |
+==================================+============================+=====================+
| Alpine | 3.9 | x86-64 |
| Alpine | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Amazon Linux 2023 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Arch | 3.9 | x86-64 |
| Arch | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
@ -33,7 +33,7 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
@ -48,7 +48,7 @@ These platforms are built and tested for every change.
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.9 | x86-64 |
| Windows Server 2019 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
@ -75,6 +75,8 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 15 Sequoia | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |

View File

@ -23,6 +23,13 @@ Python 3.8
Pillow has dropped support for Python 3.8,
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
^^^^^^
@ -45,6 +52,11 @@ TiffImagePlugin IFD_LEGACY_API
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
============
@ -61,9 +73,27 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
deprecated. Instead, ``load(scale)`` can be used.
Image isImageType()
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)``
instead.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead.
@ -79,6 +109,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
@ -87,18 +119,27 @@ Specific WebP Feature Checks
API Changes
===========
TODO
^^^^
Default resampling filter for I;16* image modes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from
``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes.
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
=============
@ -112,6 +153,10 @@ of 3.13.0 final (2024-10-01, :pep:`719`).
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
^^^^^^^^^^^^^

View File

@ -97,9 +97,13 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
[tool.ruff]
fix = true
[tool.black]
exclude = "wheels/multibuild"
[tool.ruff]
exclude = [ "wheels/multibuild" ]
fix = true
lint.select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors
@ -125,7 +129,6 @@ lint.ignore = [
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11

View File

@ -553,7 +553,9 @@ def _normalize_palette(
if im.mode == "P":
if not source_palette:
source_palette = im.im.getpalette("RGB")[:768]
im_palette = im.getpalette(None)
assert im_palette is not None
source_palette = bytearray(im_palette)
else: # L-mode
if not source_palette:
source_palette = bytearray(i // 3 for i in range(768))
@ -629,7 +631,10 @@ def _write_single_frame(
def _getbbox(
base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
palette_bytes = [
bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
]
if palette_bytes[0] != palette_bytes[1]:
im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA")
delta = ImageChops.subtract_modulo(im_frame, base_im)
@ -984,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
:param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header
"""
return bytes(im.palette.palette) if im.palette else b""
if not im.palette:
return b""
palette = bytes(im.palette.palette)
if im.palette.mode == "RGBA":
palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
return palette
def _get_background(

View File

@ -25,6 +25,7 @@ import sys
from typing import IO
from . import Image, ImageFile, PngImagePlugin, features
from ._deprecate import deprecate
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
@ -275,37 +276,37 @@ class IcnsImageFile(ImageFile.ImageFile):
self.best_size[1] * self.best_size[2],
)
@property
def size(self):
@property # type: ignore[override]
def size(self) -> tuple[int, int] | tuple[int, int, int]:
return self._size
@size.setter
def size(self, value) -> None:
info_size = value
if info_size not in self.info["sizes"] and len(info_size) == 2:
info_size = (info_size[0], info_size[1], 1)
if (
info_size not in self.info["sizes"]
and len(info_size) == 3
and info_size[2] == 1
):
simple_sizes = [
(size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"]
]
if value in simple_sizes:
info_size = self.info["sizes"][simple_sizes.index(value)]
if info_size not in self.info["sizes"]:
def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None:
if len(value) == 3:
deprecate("Setting size to (width, height, scale)", 12, "load(scale)")
if value in self.info["sizes"]:
self._size = value # type: ignore[assignment]
return
else:
# Check that a matching size exists,
# or that there is a scale that would create a size that matches
for size in self.info["sizes"]:
simple_size = size[0] * size[2], size[1] * size[2]
scale = simple_size[0] // value[0]
if simple_size[1] / value[1] == scale:
self._size = value
return
msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg)
self._size = value
def load(self) -> Image.core.PixelAccess | None:
if len(self.size) == 3:
self.best_size = self.size
self.size = (
self.best_size[0] * self.best_size[2],
self.best_size[1] * self.best_size[2],
)
def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
if scale is not None or len(self.size) == 3:
if scale is None and len(self.size) == 3:
scale = self.size[2]
assert scale is not None
width, height = self.size[:2]
self.size = width * scale, height * scale
self.best_size = width, height, scale
px = Image.Image.load(self)
if self._im is not None and self.im.size == self.size:

View File

@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]:
:param t: object to check if it's an image
:returns: True if the object is an image
"""
deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)")
return hasattr(t, "im")
@ -1066,7 +1067,7 @@ class Image:
trns_im = new(self.mode, (1, 1))
if self.mode == "P":
assert self.palette is not None
trns_im.putpalette(self.palette)
trns_im.putpalette(self.palette, self.palette.mode)
if isinstance(t, tuple):
err = "Couldn't allocate a palette color for transparency"
assert trns_im.palette is not None
@ -1130,17 +1131,23 @@ class Image:
return new_im
if "LAB" in (self.mode, mode):
other_mode = mode if self.mode == "LAB" else self.mode
im = self
if mode == "LAB":
if im.mode not in ("RGB", "RGBA", "RGBX"):
im = im.convert("RGBA")
other_mode = im.mode
else:
other_mode = mode
if other_mode in ("RGB", "RGBA", "RGBX"):
from . import ImageCms
srgb = ImageCms.createProfile("sRGB")
lab = ImageCms.createProfile("LAB")
profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab]
profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab]
transform = ImageCms.buildTransform(
profiles[0], profiles[1], self.mode, mode
profiles[0], profiles[1], im.mode, mode
)
return transform.apply(self)
return transform.apply(im)
# colorspace conversion
if dither is None:
@ -1817,23 +1824,22 @@ class Image:
:param mask: An optional mask image.
"""
if isImageType(box):
if isinstance(box, Image):
if mask is not None:
msg = "If using second argument as mask, third argument must be None"
raise ValueError(msg)
# abbreviated paste(im, mask) syntax
mask = box
box = None
assert not isinstance(box, Image)
if box is None:
box = (0, 0)
if len(box) == 2:
# upper left corner given; get size from image or mask
if isImageType(im):
if isinstance(im, Image):
size = im.size
elif isImageType(mask):
elif isinstance(mask, Image):
size = mask.size
else:
# FIXME: use self.size here?
@ -1846,17 +1852,15 @@ class Image:
from . import ImageColor
source = ImageColor.getcolor(im, self.mode)
elif isImageType(im):
elif isinstance(im, Image):
im.load()
if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this!
im = im.convert(self.mode)
source = im.im
elif isinstance(im, tuple):
source = im
else:
source = cast(float, im)
source = im
self._ensure_mutable()
@ -2017,7 +2021,7 @@ class Image:
else:
band = 3
if isImageType(alpha):
if isinstance(alpha, Image):
# alpha layer
if alpha.mode not in ("1", "L"):
msg = "illegal image mode"
@ -2027,7 +2031,6 @@ class Image:
alpha = alpha.convert("L")
else:
# constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try:
self.im.fillband(band, alpha)
except (AttributeError, ValueError):
@ -2176,6 +2179,9 @@ class Image:
source_palette = self.im.getpalette(palette_mode, palette_mode)
else: # L-mode
source_palette = bytearray(i // 3 for i in range(768))
elif len(source_palette) > 768:
bands = 4
palette_mode = "RGBA"
palette_bytes = b""
new_positions = [0] * 256
@ -2272,8 +2278,8 @@ class Image:
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
If the image has mode "1" or "P", it is always set to
:py:data:`Resampling.NEAREST`. If the image mode specifies a number
of bits, such as "I;16", then the default filter is
:py:data:`Resampling.NEAREST`. If the image mode is "BGR;15",
"BGR;16" or "BGR;24", then the default filter is
:py:data:`Resampling.NEAREST`. Otherwise, the default filter is
:py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing
@ -2296,8 +2302,8 @@ class Image:
"""
if resample is None:
type_special = ";" in self.mode
resample = Resampling.NEAREST if type_special else Resampling.BICUBIC
bgr = self.mode.startswith("BGR;")
resample = Resampling.NEAREST if bgr else Resampling.BICUBIC
elif resample not in (
Resampling.NEAREST,
Resampling.BILINEAR,
@ -2326,7 +2332,6 @@ class Image:
msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg)
self.load()
if box is None:
box = (0, 0) + self.size
@ -2775,27 +2780,18 @@ class Image:
)
return x, y
box = None
final_size: tuple[int, int]
if reducing_gap is not None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
final_size = preserved_size
box = None
if reducing_gap is not None:
res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
)
if res is not None:
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:
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)

View File

@ -55,7 +55,9 @@ class Color(_Enhance):
if "A" in image.getbands():
self.intermediate_mode = "LA"
self.degenerate = image.convert(self.intermediate_mode).convert(image.mode)
if self.intermediate_mode != image.mode:
image = image.convert(self.intermediate_mode).convert(image.mode)
self.degenerate = image
class Contrast(_Enhance):
@ -68,11 +70,15 @@ class Contrast(_Enhance):
def __init__(self, image: Image.Image) -> None:
self.image = image
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
if image.mode != "L":
image = image.convert("L")
mean = int(ImageStat.Stat(image).mean[0] + 0.5)
self.degenerate = Image.new("L", image.size, mean)
if self.degenerate.mode != self.image.mode:
self.degenerate = self.degenerate.convert(self.image.mode)
if "A" in image.getbands():
self.degenerate.putalpha(image.getchannel("A"))
if "A" in self.image.getbands():
self.degenerate.putalpha(self.image.getchannel("A"))
class Brightness(_Enhance):

View File

@ -324,7 +324,7 @@ class ImageFile(Image.Image):
def load_prepare(self) -> None:
# 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.mb_config)
# create palette (optional)
if self.mode == "P":

View File

@ -98,11 +98,13 @@ class ImageFont:
def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp:
image: ImageFile.ImageFile | None = None
root = os.path.splitext(filename)[0]
for ext in (".png", ".gif", ".pbm"):
if image:
image.close()
try:
fullname = os.path.splitext(filename)[0] + ext
fullname = root + ext
image = Image.open(fullname)
except Exception:
pass
@ -112,7 +114,8 @@ class ImageFont:
else:
if image:
image.close()
msg = "cannot find glyph data file"
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
raise OSError(msg)
self.file = fullname
@ -224,7 +227,7 @@ class FreeTypeFont:
raise core.ex
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)
self.path = font
@ -784,7 +787,8 @@ class TransposedFont:
def load(filename: str) -> ImageFont:
"""
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.
:return: A font object.
@ -804,9 +808,10 @@ def truetype(
) -> FreeTypeFont:
"""
Load a TrueType or OpenType font from a file or file-like object,
and create a font object.
This function loads a font object from the given file or file-like
object, and creates a font object for a font of the given size.
and create a font object. This function loads a font object from the given
file or file-like object, and creates a font object for a font of the given
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
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))
except OSError:
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)

View File

@ -268,7 +268,7 @@ def lambda_eval(
args.update(options)
args.update(kw)
for k, v in args.items():
if hasattr(v, "im"):
if isinstance(v, Image.Image):
args[k] = _Operand(v)
out = expression(args)
@ -319,7 +319,7 @@ def unsafe_eval(
args.update(options)
args.update(kw)
for k, v in args.items():
if hasattr(v, "im"):
if isinstance(v, Image.Image):
args[k] = _Operand(v)
compiled_code = compile(expression, "<string>", "eval")

View File

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

View File

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

View File

@ -1037,8 +1037,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
if stripoffsets is not None:
tag, typ, count, value, data = entries[stripoffsets]
if data:
msg = "multistrip support not yet implemented"
raise NotImplementedError(msg)
size, handler = self._load_dispatch[typ]
values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values)
else:
value = self._pack("L", self._unpack("L", value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data
@ -1240,8 +1242,8 @@ class TiffImageFile(ImageFile.ImageFile):
# Create a new core image object on second and
# subsequent frames in the image. Image may be
# different size/mode.
Image._decompression_bomb_check(self.size)
self.im = Image.core.new(self.mode, self.size)
Image._decompression_bomb_check(self._tile_size)
self.im = Image.core.new(self.mode, self._tile_size)
def _seek(self, frame: int) -> None:
self.fp = self._fp
@ -1321,6 +1323,11 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff()
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:
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
@ -1462,6 +1469,11 @@ class TiffImageFile(ImageFile.ImageFile):
if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions"
raise ValueError(msg)
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)
@ -1600,7 +1612,7 @@ class TiffImageFile(ImageFile.ImageFile):
if STRIPOFFSETS in self.tag_v2:
offsets = self.tag_v2[STRIPOFFSETS]
h = self.tag_v2.get(ROWSPERSTRIP, ysize)
w = self.size[0]
w = xsize
else:
# tiled image
offsets = self.tag_v2[TILEOFFSETS]
@ -1634,9 +1646,9 @@ class TiffImageFile(ImageFile.ImageFile):
)
)
x = x + w
if x >= self.size[0]:
if x >= xsize:
x, y = 0, y + h
if y >= self.size[1]:
if y >= ysize:
x = y = 0
layer += 1
else:

View File

@ -13,10 +13,6 @@ except ImportError:
SUPPORTED = False
_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True}
_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True}
_VP8_MODES_BY_IDENTIFIER = {
b"VP8 ": "RGB",
b"VP8X": "RGBA",
@ -153,6 +149,13 @@ class WebPImageFile(ImageFile.ImageFile):
return self.__logical_frame
def _convert_frame(im: Image.Image) -> Image.Image:
# Make sure image mode is supported
if im.mode not in ("RGBX", "RGBA", "RGB"):
im = im.convert("RGBA" if im.has_transparency_data else "RGB")
return im
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", []))
@ -243,31 +246,13 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
for idx in range(nfr):
ims.seek(idx)
ims.load()
# Make sure image mode is supported
frame = ims
rawmode = ims.mode
if ims.mode not in _VALID_WEBP_MODES:
alpha = (
"A" in ims.mode
or "a" in ims.mode
or (ims.mode == "P" and "A" in ims.im.getpalettemode())
)
rawmode = "RGBA" if alpha else "RGB"
frame = ims.convert(rawmode)
if rawmode == "RGB":
# For faster conversion, use RGBX
rawmode = "RGBX"
frame = _convert_frame(ims)
# Append the frame to the animation encoder
enc.add(
frame.tobytes("raw", rawmode),
frame.getim(),
round(timestamp),
frame.size[0],
frame.size[1],
rawmode,
lossless,
quality,
alpha_quality,
@ -285,7 +270,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
im.seek(cur_idx)
# Force encoder to flush frames
enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0)
enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0)
# Get the final output from the encoder
data = enc.assemble(icc_profile, exif, xmp)
@ -310,17 +295,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
method = im.encoderinfo.get("method", 4)
exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
im = im.convert("RGBA" if im.has_transparency_data else "RGB")
im = _convert_frame(im)
data = _webp.WebPEncode(
im.tobytes(),
im.size[0],
im.size[1],
im.getim(),
lossless,
float(quality),
float(alpha_quality),
im.mode,
icc_profile,
method,
exact,

View File

@ -1616,16 +1616,12 @@ _putdata(ImagingObject *self, PyObject *args) {
int bigendian = 0;
if (image->type == IMAGING_TYPE_SPECIAL) {
// I;16*
if (strcmp(image->mode, "I;16N") == 0) {
if (strcmp(image->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
bigendian = 1;
#else
bigendian = 0;
|| strcmp(image->mode, "I;16N") == 0
#endif
} else if (strcmp(image->mode, "I;16B") == 0) {
) {
bigendian = 1;
} else {
bigendian = 0;
}
}
for (i = x = y = 0; i < n; i++) {

View File

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

View File

@ -83,6 +83,49 @@ HandleMuxError(WebPMuxError err, char *chunk) {
return NULL;
}
/* -------------------------------------------------------------------- */
/* Frame import */
/* -------------------------------------------------------------------- */
static int
import_frame_libwebp(WebPPicture *frame, Imaging im) {
if (strcmp(im->mode, "RGBA") && strcmp(im->mode, "RGB") &&
strcmp(im->mode, "RGBX")) {
PyErr_SetString(PyExc_ValueError, "unsupported image mode");
return -1;
}
frame->width = im->xsize;
frame->height = im->ysize;
frame->use_argb = 1; // Don't convert RGB pixels to YUV
if (!WebPPictureAlloc(frame)) {
PyErr_SetString(PyExc_MemoryError, "can't allocate picture frame");
return -2;
}
int ignore_fourth_channel = strcmp(im->mode, "RGBA");
for (int y = 0; y < im->ysize; ++y) {
UINT8 *src = (UINT8 *)im->image32[y];
UINT32 *dst = frame->argb + frame->argb_stride * y;
if (ignore_fourth_channel) {
for (int x = 0; x < im->xsize; ++x) {
dst[x] =
((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) |
((UINT32)(src[x * 4]) << 16) | (0xff << 24));
}
} else {
for (int x = 0; x < im->xsize; ++x) {
dst[x] =
((UINT32)(src[x * 4 + 2]) | ((UINT32)(src[x * 4 + 1]) << 8) |
((UINT32)(src[x * 4]) << 16) | ((UINT32)(src[x * 4 + 3]) << 24));
}
}
}
return 0;
}
/* -------------------------------------------------------------------- */
/* WebP Animation Support */
/* -------------------------------------------------------------------- */
@ -180,16 +223,14 @@ _anim_encoder_dealloc(PyObject *self) {
PyObject *
_anim_encoder_add(PyObject *self, PyObject *args) {
uint8_t *rgb;
Py_ssize_t size;
PyObject *i0;
Imaging im;
int timestamp;
int width;
int height;
char *mode;
int lossless;
float quality_factor;
float alpha_quality_factor;
int method;
ImagingSectionCookie cookie;
WebPConfig config;
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPAnimEncoder *enc = encp->enc;
@ -197,13 +238,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"z#iiisiffi",
(char **)&rgb,
&size,
"Oiiffi",
&i0,
&timestamp,
&width,
&height,
&mode,
&lossless,
&quality_factor,
&alpha_quality_factor,
@ -213,11 +250,18 @@ _anim_encoder_add(PyObject *self, PyObject *args) {
}
// Check for NULL frame, which sets duration of final frame
if (!rgb) {
if (i0 == Py_None) {
WebPAnimEncoderAdd(enc, NULL, timestamp, NULL);
Py_RETURN_NONE;
}
if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
// Setup config for this frame
if (!WebPConfigInit(&config)) {
PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!");
@ -234,20 +278,15 @@ _anim_encoder_add(PyObject *self, PyObject *args) {
return NULL;
}
// Populate the frame with raw bytes passed to us
frame->width = width;
frame->height = height;
frame->use_argb = 1; // Don't convert RGB pixels to YUV
if (strcmp(mode, "RGBA") == 0) {
WebPPictureImportRGBA(frame, rgb, 4 * width);
} else if (strcmp(mode, "RGBX") == 0) {
WebPPictureImportRGBX(frame, rgb, 4 * width);
} else {
WebPPictureImportRGB(frame, rgb, 3 * width);
if (import_frame_libwebp(frame, im)) {
return NULL;
}
// Add the frame to the encoder
if (!WebPAnimEncoderAdd(enc, frame, timestamp, &config)) {
ImagingSectionEnter(&cookie);
int ok = WebPAnimEncoderAdd(enc, frame, timestamp, &config);
ImagingSectionLeave(&cookie);
if (!ok) {
PyErr_SetString(PyExc_RuntimeError, WebPAnimEncoderGetError(enc));
return NULL;
}
@ -572,26 +611,21 @@ static PyTypeObject WebPAnimDecoder_Type = {
PyObject *
WebPEncode_wrapper(PyObject *self, PyObject *args) {
int width;
int height;
int lossless;
float quality_factor;
float alpha_quality_factor;
int method;
int exact;
uint8_t *rgb;
Imaging im;
PyObject *i0;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
uint8_t *xmp_bytes;
uint8_t *output;
char *mode;
Py_ssize_t size;
Py_ssize_t icc_size;
Py_ssize_t exif_size;
Py_ssize_t xmp_size;
size_t ret_size;
int rgba_mode;
int channels;
int ok;
ImagingSectionCookie cookie;
WebPConfig config;
@ -600,15 +634,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
"y#iiiffss#iis#s#",
(char **)&rgb,
&size,
&width,
&height,
"Oiffs#iis#s#",
&i0,
&lossless,
&quality_factor,
&alpha_quality_factor,
&mode,
&icc_bytes,
&icc_size,
&method,
@ -621,15 +651,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
return NULL;
}
rgba_mode = strcmp(mode, "RGBA") == 0;
if (!rgba_mode && strcmp(mode, "RGB") != 0) {
Py_RETURN_NONE;
if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
channels = rgba_mode ? 4 : 3;
if (size < width * height * channels) {
Py_RETURN_NONE;
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
// Setup config for this frame
if (!WebPConfigInit(&config)) {
@ -652,14 +679,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "could not initialise picture");
return NULL;
}
pic.width = width;
pic.height = height;
pic.use_argb = 1; // Don't convert RGB pixels to YUV
if (rgba_mode) {
WebPPictureImportRGBA(&pic, rgb, channels * width);
} else {
WebPPictureImportRGB(&pic, rgb, channels * width);
if (import_frame_libwebp(&pic, im)) {
return NULL;
}
WebPMemoryWriterInit(&writer);

View File

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

View File

@ -224,7 +224,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
break;
case 16:
/* COPY chunk */
if (INT32_MAX / state->xsize < state->ysize) {
if (INT32_MAX < (uint64_t)state->xsize * state->ysize) {
/* Integer overflow, bail */
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;

View File

@ -734,15 +734,15 @@ ImagingGenericTransform(
char *out;
double xx, yy;
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) {
return (Imaging)ImagingError_ModeError();
}
ImagingTransformFilter filter = getfilter(imIn, filterid);
if (!filter) {
return (Imaging)ImagingError_ValueError("bad filter number");
}
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) {
return (Imaging)ImagingError_ModeError();
}
ImagingCopyPalette(imOut, imIn);
ImagingSectionEnter(&cookie);
@ -978,6 +978,10 @@ ImagingTransformAffine(
double xx, yy;
double xo, yo;
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) {
return (Imaging)ImagingError_ModeError();
}
if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) {
return ImagingGenericTransform(
imOut, imIn, x0, y0, x1, y1, affine_transform, a, filterid, fill
@ -989,10 +993,6 @@ ImagingTransformAffine(
return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill);
}
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) {
return (Imaging)ImagingError_ModeError();
}
if (x0 < 0) {
x0 = 0;
}

View File

@ -31,7 +31,27 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
bbox[2] = bbox[3] = 0;
#define GETBBOX(image, mask) \
/* first stage: looking for any pixels from top */ \
for (y = 0; y < im->ysize; y++) { \
has_data = 0; \
for (x = 0; x < im->xsize; x++) { \
if (im->image[y][x] & mask) { \
has_data = 1; \
bbox[0] = x; \
bbox[1] = y; \
break; \
} \
} \
if (has_data) { \
break; \
} \
} \
/* Check that we have a box */ \
if (bbox[1] < 0) { \
return 0; /* no data */ \
} \
/* second stage: looking for any pixels from bottom */ \
for (y = im->ysize - 1; y >= bbox[1]; y--) { \
has_data = 0; \
for (x = 0; x < im->xsize; x++) { \
if (im->image[y][x] & mask) { \
@ -39,16 +59,27 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
if (x < bbox[0]) { \
bbox[0] = x; \
} \
if (x >= bbox[2]) { \
bbox[2] = x + 1; \
} \
bbox[3] = y + 1; \
break; \
} \
} \
if (has_data) { \
if (bbox[1] < 0) { \
bbox[1] = y; \
break; \
} \
} \
/* third stage: looking for left and right boundaries */ \
for (y = bbox[1]; y < bbox[3]; y++) { \
for (x = 0; x < bbox[0]; x++) { \
if (im->image[y][x] & mask) { \
bbox[0] = x; \
break; \
} \
} \
for (x = im->xsize - 1; x >= bbox[2]; x--) { \
if (im->image[y][x] & mask) { \
bbox[2] = x + 1; \
break; \
} \
bbox[3] = y + 1; \
} \
}
@ -71,11 +102,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
GETBBOX(image32, mask);
}
/* Check that we got a box */
if (bbox[1] < 0) {
return 0; /* no data */
}
return 1; /* ok */
}
@ -144,6 +170,9 @@ ImagingGetExtrema(Imaging im, void *extrema) {
imax = in[x];
}
}
if (imin == 0 && imax == 255) {
break;
}
}
((UINT8 *)extrema)[0] = (UINT8)imin;
((UINT8 *)extrema)[1] = (UINT8)imax;

View File

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

View File

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

View File

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

View File

@ -460,6 +460,83 @@ ImagingResampleVertical_8bpc(
ImagingSectionLeave(&cookie);
}
void
ImagingResampleHorizontal_16bpc(
Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk
) {
ImagingSectionCookie cookie;
double ss;
int xx, yy, x, xmin, xmax, ss_int;
double *k;
int bigendian = 0;
if (strcmp(imIn->mode, "I;16N") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(imIn->mode, "I;16B") == 0
#endif
) {
bigendian = 1;
}
ImagingSectionEnter(&cookie);
for (yy = 0; yy < imOut->ysize; yy++) {
for (xx = 0; xx < imOut->xsize; xx++) {
xmin = bounds[xx * 2 + 0];
xmax = bounds[xx * 2 + 1];
k = &kk[xx * ksize];
ss = 0.0;
for (x = 0; x < xmax; x++) {
ss += (imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 1 : 0)] +
(imIn->image8[yy + offset][(x + xmin) * 2 + (bigendian ? 0 : 1)]
<< 8)) *
k[x];
}
ss_int = ROUND_UP(ss);
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
}
}
ImagingSectionLeave(&cookie);
}
void
ImagingResampleVertical_16bpc(
Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk
) {
ImagingSectionCookie cookie;
double ss;
int xx, yy, y, ymin, ymax, ss_int;
double *k;
int bigendian = 0;
if (strcmp(imIn->mode, "I;16N") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(imIn->mode, "I;16B") == 0
#endif
) {
bigendian = 1;
}
ImagingSectionEnter(&cookie);
for (yy = 0; yy < imOut->ysize; yy++) {
ymin = bounds[yy * 2 + 0];
ymax = bounds[yy * 2 + 1];
k = &kk[yy * ksize];
for (xx = 0; xx < imOut->xsize; xx++) {
ss = 0.0;
for (y = 0; y < ymax; y++) {
ss += (imIn->image8[y + ymin][xx * 2 + (bigendian ? 1 : 0)] +
(imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) *
k[y];
}
ss_int = ROUND_UP(ss);
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
}
}
ImagingSectionLeave(&cookie);
}
void
ImagingResampleHorizontal_32bpc(
Imaging imOut, Imaging imIn, int offset, int ksize, int *bounds, double *kk
@ -574,7 +651,12 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) {
}
if (imIn->type == IMAGING_TYPE_SPECIAL) {
if (strncmp(imIn->mode, "I;16", 4) == 0) {
ResampleHorizontal = ImagingResampleHorizontal_16bpc;
ResampleVertical = ImagingResampleVertical_16bpc;
} else {
return (Imaging)ImagingError_ModeError();
}
} else if (imIn->image8) {
ResampleHorizontal = ImagingResampleHorizontal_8bpc;
ResampleVertical = ImagingResampleVertical_8bpc;

View File

@ -183,7 +183,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
each with 4 bytes per element of tablen
Check here before we allocate any memory
*/
if (c->bufsize < 8 * c->tablen) {
if (c->bufsize < 8 * (int64_t)c->tablen) {
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;
}
@ -195,6 +195,7 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
}
_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET);
if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) {
free(ptr);
state->errcode = IMAGING_CODEC_UNKNOWN;
return -1;
}

View File

@ -44,7 +44,7 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view);
typedef struct {
PyObject_HEAD Py_ssize_t count;
double *xy;
int index; /* temporary use, e.g. in decimate */
int mapping;
} PyPathObject;
static PyTypeObject PyPathType;
@ -92,6 +92,7 @@ path_new(Py_ssize_t count, double *xy, int duplicate) {
path->count = count;
path->xy = xy;
path->mapping = 0;
return path;
}
@ -277,6 +278,10 @@ path_compact(PyPathObject *self, PyObject *args) {
double cityblock = 2.0;
if (self->mapping) {
PyErr_SetString(PyExc_ValueError, "Path compacted during mapping");
return NULL;
}
if (!PyArg_ParseTuple(args, "|d:compact", &cityblock)) {
return NULL;
}
@ -394,11 +399,13 @@ path_map(PyPathObject *self, PyObject *args) {
xy = self->xy;
/* apply function to coordinate set */
self->mapping = 1;
for (i = 0; i < self->count; i++) {
double x = xy[i + i];
double y = xy[i + i + 1];
PyObject *item = PyObject_CallFunction(function, "dd", x, y);
if (!item || !PyArg_ParseTuple(item, "dd", &x, &y)) {
self->mapping = 0;
Py_XDECREF(item);
return NULL;
}
@ -406,6 +413,7 @@ path_map(PyPathObject *self, PyObject *args) {
xy[i + i + 1] = y;
Py_DECREF(item);
}
self->mapping = 0;
Py_INCREF(Py_None);
return Py_None;

View File

@ -1,11 +1,11 @@
README
------
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux
wheels for tagged versions of Pillow.
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged
versions of Pillow.
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
--------
@ -30,6 +30,3 @@ Wheels
Wheels are
[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

@ -11,7 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component).
* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
* Tested on Windows Server 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor).
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
The following is a simplified version of the script used on AppVeyor:

View File

@ -111,15 +111,15 @@ ARCHITECTURES = {
V = {
"BROTLI": "1.1.0",
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.15",
"HARFBUZZ": "9.0.0",
"JPEGTURBO": "3.0.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "10.0.1",
"JPEGTURBO": "3.0.4",
"LCMS2": "2.16",
"LIBPNG": "1.6.43",
"LIBPNG": "1.6.44",
"LIBWEBP": "1.4.0",
"OPENJPEG": "2.5.2",
"TIFF": "4.6.0",
"XZ": "5.4.5",
"XZ": "5.6.3",
"ZLIB": "1.3.1",
}
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = {
"libs": [r"*.lib"],
},
"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",
"dir": f"xz-{V['XZ']}",
"license": "COPYING",
@ -185,7 +185,7 @@ DEPS: dict[str, dict[str, Any]] = {
cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"),
],
"headers": [r"src\liblzma\api\lzma.h"],
"libs": [r"liblzma.lib"],
"libs": [r"lzma.lib"],
},
"libwebp": {
"url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz",
@ -216,8 +216,8 @@ DEPS: dict[str, dict[str, Any]] = {
"license": "LICENSE.md",
"patch": {
r"libtiff\tif_lzma.c": {
# link against liblzma.lib
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
# link against lzma.lib
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "lzma.lib")', # noqa: E501
},
r"libtiff\tif_webp.c": {
# link against libwebp.lib
@ -292,8 +292,12 @@ DEPS: dict[str, dict[str, Any]] = {
},
"build": [
cmd_rmdir("objs"),
cmd_msbuild("MSBuild.sln", "Release Static", "Clean"),
cmd_msbuild("MSBuild.sln", "Release Static", "Build"),
cmd_msbuild(
r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Clean"
),
cmd_msbuild(
r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Build"
),
cmd_xcopy("include", "{inc_dir}"),
],
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],