Merge branch 'main' into improve-error-messages

This commit is contained in:
Yngve Mardal Moe 2024-09-11 17:11:52 +02:00 committed by GitHub
commit 9ba4e10a16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 1036 additions and 447 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

View File

@ -6,6 +6,7 @@ numpy
packaging packaging
pytest pytest
sphinx sphinx
types-atheris
types-defusedxml types-defusedxml
types-olefile types-olefile
types-setuptools types-setuptools

View File

@ -76,7 +76,7 @@ jobs:
"pyproject.toml" "pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded) - name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0 uses: deadsnakes/action@v3.2.0
if: "${{ matrix.disable-gil }}" if: "${{ matrix.disable-gil }}"
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}

View File

@ -16,7 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.5.0 if [[ "$MB_ML_VER" != 2014 ]]; then
HARFBUZZ_VERSION=9.0.0
else
HARFBUZZ_VERSION=8.5.0
fi
LIBPNG_VERSION=1.6.43 LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3 JPEGTURBO_VERSION=3.0.3
OPENJPEG_VERSION=2.5.2 OPENJPEG_VERSION=2.5.2
@ -40,7 +44,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg { 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 \ (cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install) && make install)
@ -50,7 +54,7 @@ fi
function build_brotli { function build_brotli {
local cmake=$(get_modern_cmake) 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 \ (cd $out_dir \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install) && make install)
@ -60,6 +64,25 @@ function build_brotli {
fi fi
} }
function build_harfbuzz {
if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
else
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
fi
}
function build { function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local sudo chown -R runner /usr/local
@ -109,15 +132,7 @@ function build {
build_freetype build_freetype
fi fi
if [ -z "$IS_MACOS" ]; then build_harfbuzz
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
} }
# Any stuff that you need to do before you start building the wheels # Any stuff that you need to do before you start building the wheels
@ -140,7 +155,13 @@ if [[ -n "$IS_MACOS" ]]; then
brew remove --ignore-dependencies webp brew remove --ignore-dependencies webp
fi fi
brew install pkg-config brew install meson pkg-config
elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then
if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then
yum install -y meson
fi
else
apk add meson
fi fi
wrap_wheel_builder build wrap_wheel_builder build

View File

@ -5,6 +5,54 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- 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]
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347
[radarhere]
- Check text is either string or bytes #8308
[radarhere]
- Added writing XMP bytes to JPEG #8286
[radarhere]
- Support JPEG2000 RGBA palettes #8256
[radarhere]
- Expand C image to match GIF frame image size #8237
[radarhere]
- Allow saving I;16 images as PPM #8231
[radarhere]
- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]
- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]
- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]
- Updated error message when saving WebP with invalid width or height #8322 - Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk] [radarhere, hugovk]

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

View File

@ -16,8 +16,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -14,8 +14,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -1,5 +1,5 @@
{ {
<py3_8_encode_current_locale> <py3_10_encode_current_locale>
Memcheck:Cond Memcheck:Cond
... ...
fun:encode_current_locale fun:encode_current_locale

View File

@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr")) box_blur(sample.convert("YCbCr"))
@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size
def test_radius_0() -> None: def test_radius_0() -> None:
assert_blur( assert_blur(
sample, sample,

View File

@ -1378,16 +1378,39 @@ def test_lzw_bits() -> None:
im.load() im.load()
def test_extents() -> None: @pytest.mark.parametrize(
with Image.open("Tests/images/test_extents.gif") as im: "test_file, loading_strategy",
assert im.size == (100, 100) (
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size # Check that n_frames does not change the size
assert im.n_frames == 2 assert im.n_frames == 2
assert im.size == (100, 100) assert im.size == (100, 100)
im.seek(1) im.seek(1)
assert im.size == (150, 150) assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_missing_background() -> None: def test_missing_background() -> None:
@ -1406,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA") reloaded_rgba = reloaded.convert("RGBA")
assert reloaded_rgba.load()[0, 0][3] == 0 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) assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread: with Image.open(temp_file) as reread:
reread.size = (16, 16, 2) reread.size = (16, 16)
reread.load() reread.load(2)
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
@ -87,14 +87,21 @@ def test_sizes() -> None:
for w, h, r in im.info["sizes"]: for w, h, r in im.info["sizes"]:
wr = w * r wr = w * r
hr = h * r hr = h * r
im.size = (w, h, r) with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (wr, hr) 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 # Check that we cannot load an incorrect size
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.size = (1, 1) im.size = (1, 2)
def test_older_icon() -> None: def test_older_icon() -> None:
@ -105,8 +112,8 @@ def test_older_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2: with Image.open("Tests/images/pillow2.icns") as im2:
im2.size = (w, h, r) im2.size = (w, h)
im2.load() im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"
assert im2.size == (wr, hr) assert im2.size == (wr, hr)
@ -122,8 +129,8 @@ def test_jp2_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2: with Image.open("Tests/images/pillow3.icns") as im2:
im2.size = (w, h, r) im2.size = (w, h)
im2.load() im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"
assert im2.size == (wr, hr) assert im2.size == (wr, hr)

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest import pytest
from PIL import IcoImagePlugin, Image, ImageDraw from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(outfile) as im: with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -991,12 +991,29 @@ class TestFileJpeg:
else: else:
assert im.getxmp() == {"xmpmeta": None} assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504
im.save(f)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1) @pytest.mark.timeout(timeout=1)
def test_eof(self) -> None: def test_eof(self) -> None:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder): class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0 return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder) Image.register_decoder("INFINITE", InfiniteMockPyDecoder)

View File

@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"
def test_header_errors() -> None: def test_header_errors() -> None:
for path in ( for path in (
"Tests/images/invalid_header_length.jp2", "Tests/images/invalid_header_length.jp2",
@ -391,6 +400,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256 assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0 assert im.palette.colors[(255, 255, 255)] == 0
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0
def test_comment() -> None: def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im: with Image.open("Tests/images/comment.jp2") as im:

View File

@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm") filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM") im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
im.convert("I;16").save(filename, "PPM")
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)

View File

@ -775,6 +775,22 @@ class TestImage:
exif.load(b"Exif\x00\x00") exif.load(b"Exif\x00\x00")
assert not dict(exif) assert not dict(exif)
def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]
exif = im.getexif()
assert exif[274] == 1
def test_empty_get_ifd(self) -> None:
exif = Image.Exif()
ifd = exif.get_ifd(0x8769)
assert ifd == {}
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@ -1101,6 +1117,10 @@ class TestImage:
assert len(caplog.records) == 0 assert len(caplog.records) == 0
assert im.fp is None assert im.fp is None
def test_deprecation(self) -> None:
with pytest.warns(DeprecationWarning):
assert not Image.isImageType(None)
class TestImageBytes: class TestImageBytes:
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])

View File

@ -24,7 +24,7 @@ def test_toarray() -> None:
def test_with_dtype(dtype: npt.DTypeLike) -> None: def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype) ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype assert ai.dtype.type is dtype
# assert test("1") == ((100, 128), '|b1', 1600)) # assert test("1") == ((100, 128), '|b1', 1600))
assert test("L") == ((100, 128), "|u1", 12800) assert test("L") == ((100, 128), "|u1", 12800)

View File

@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None:
assert value[:3] == (0, 255, 255) 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: def test_deprecation() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")

View File

@ -210,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0
@ -238,7 +238,9 @@ class MockImageFile(ImageFile.ImageFile):
self.rawmode = "RGBA" self.rawmode = "RGBA"
self._mode = "RGBA" self._mode = "RGBA"
self._size = (200, 200) self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] self.tile = [
ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)
]
class CodecsTest: class CodecsTest:
@ -268,7 +270,7 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
im.load() im.load()
@ -281,12 +283,12 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -294,12 +296,20 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
)
]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
)
]
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -336,7 +346,7 @@ class TestPyEncoder(CodecsTest):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
fp = BytesIO() fp = BytesIO()
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])

View File

@ -1140,6 +1140,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None:
) )
assert font.getmask2(b"test")[1] == font.getmask2("test")[1] assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
with pytest.raises(TypeError):
font.getlength((0, 0)) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
@ -1174,3 +1177,15 @@ def test_invalid_truetype_sizes_raise_valueerror(
) -> None: ) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: mock features.version_module to return fake FreeType version
def fake_version_module(module: str) -> str:
return "2.9.0"
monkeypatch.setattr(features, "version_module", fake_version_module)
# Act / Assert
with pytest.warns(DeprecationWarning):
ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -2,7 +2,7 @@
# install libimagequant # install libimagequant
archive_name=libimagequant archive_name=libimagequant
archive_version=4.3.1 archive_version=4.3.3
archive=$archive_name-$archive_version archive=$archive_name-$archive_version

View File

@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name # generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or # if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH'). # ('envvar', 'LD_LIBRARY_PATH').
# nitpick_ignore = [] nitpick_ignore = [("py:class", "_io.BytesIO")]
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------

View File

@ -109,6 +109,35 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
FreeType 2.9.0
^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0
(2025-10-15), when FreeType 2.9.1 will be the minimum supported.
We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
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 ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -142,6 +171,13 @@ Removed features
Deprecated features are only removed in major releases after an appropriate Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed. period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0
``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting.
PSFile PSFile
~~~~~~ ~~~~~~

View File

@ -246,7 +246,9 @@ class DdsImageFile(ImageFile.ImageFile):
msg = f"Unimplemented pixel format {repr(fourcc)}" msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [
ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
]
def load_seek(self, pos: int) -> None: def load_seek(self, pos: int) -> None:
pass pass
@ -255,7 +257,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DXT1Decoder(ImageFile.PyDecoder): class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
try: try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
@ -268,7 +270,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
try: try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))

View File

@ -324,12 +324,19 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
**sizes** **sizes**
A list of supported sizes found in this icon file; these are a 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 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 icon and 1 for a standard icon.
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 .. _icns-loading:
will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you
ask for ``(512, 512, 2)``, the final value of Loading
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). ~~~~~~~
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: .. _icns-saving:

View File

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.1** * Pillow has been tested with libimagequant **2.6-4.3.3**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.

View File

@ -33,6 +33,14 @@ Internal Modules
Provides a convenient way to import type hints that are not available Provides a convenient way to import type hints that are not available
on some Python versions. on some Python versions.
.. py:class:: Buffer
Typing alias.
.. py:class:: IntegralLike
Typing alias.
.. py:class:: NumpyArray .. py:class:: NumpyArray
Typing alias. Typing alias.

View File

@ -40,12 +40,53 @@ removed. Pillow's C API will now be used on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
similarly removed. similarly removed.
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 Deprecations
============ ============
FreeType 2.9.0
^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0
(2025-10-15), when FreeType 2.9.1 will be the minimum supported.
We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
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 ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead. keyword arguments can be used instead.
@ -61,6 +102,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return ``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow ``True`` if the WebP module is installed, until they are removed in Pillow
@ -77,10 +120,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
============= =============
@ -94,6 +145,8 @@ 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`.
C-level Flags C-level Flags
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -163,7 +163,3 @@ follow_imports = "silent"
warn_redundant_casts = true warn_redundant_casts = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
]

257
setup.py
View File

@ -15,18 +15,20 @@ import struct
import subprocess import subprocess
import sys import sys
import warnings import warnings
from collections.abc import Iterator
from typing import Any
from setuptools import Extension, setup from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
def get_version(): def get_version() -> str:
version_file = "src/PIL/_version.py" version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f: with open(version_file, encoding="utf-8") as f:
return f.read().split('"')[1] return f.read().split('"')[1]
configuration = {} configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version() PILLOW_VERSION = get_version()
@ -143,7 +145,7 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
def _dbg(s, tp=None): def _dbg(s: str, tp: Any = None) -> None:
if DEBUG: if DEBUG:
if tp: if tp:
print(s % tp) print(s % tp)
@ -151,10 +153,13 @@ def _dbg(s, tp=None):
print(s) print(s)
def _find_library_dirs_ldconfig(): def _find_library_dirs_ldconfig() -> list[str]:
# Based on ctypes.util from Python 2 # Based on ctypes.util from Python 2
ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
args: list[str]
env: dict[str, str]
expr: str
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4: if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32" machine = os.uname()[4] + "-32"
@ -184,13 +189,11 @@ def _find_library_dirs_ldconfig():
try: try:
p = subprocess.Popen( p = subprocess.Popen(
args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True
) )
except OSError: # E.g. command not found except OSError: # E.g. command not found
return [] return []
[data, _] = p.communicate() data = p.communicate()[0]
if isinstance(data, bytes):
data = data.decode("latin1")
dirs = [] dirs = []
for dll in re.findall(expr, data): for dll in re.findall(expr, data):
@ -200,7 +203,9 @@ def _find_library_dirs_ldconfig():
return dirs return dirs
def _add_directory(path, subdir, where=None): def _add_directory(
path: list[str], subdir: str | None, where: int | None = None
) -> None:
if subdir is None: if subdir is None:
return return
subdir = os.path.realpath(subdir) subdir = os.path.realpath(subdir)
@ -216,7 +221,7 @@ def _add_directory(path, subdir, where=None):
path.insert(where, subdir) path.insert(where, subdir)
def _find_include_file(self, include): def _find_include_file(self: pil_build_ext, include: str) -> int:
for directory in self.compiler.include_dirs: for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory)) _dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)): if os.path.isfile(os.path.join(directory, include)):
@ -225,7 +230,7 @@ def _find_include_file(self, include):
return 0 return 0
def _find_library_file(self, library): def _find_library_file(self: pil_build_ext, library: str) -> str | None:
ret = self.compiler.find_library_file(self.compiler.library_dirs, library) ret = self.compiler.find_library_file(self.compiler.library_dirs, library)
if ret: if ret:
_dbg("Found library %s at %s", (library, ret)) _dbg("Found library %s at %s", (library, ret))
@ -234,7 +239,7 @@ def _find_library_file(self, library):
return ret return ret
def _find_include_dir(self, dirname, include): def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str:
for directory in self.compiler.include_dirs: for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory)) _dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)): if os.path.isfile(os.path.join(directory, include)):
@ -245,6 +250,7 @@ def _find_include_dir(self, dirname, include):
if os.path.isfile(os.path.join(subdir, include)): if os.path.isfile(os.path.join(subdir, include)):
_dbg("Found %s in %s", (include, subdir)) _dbg("Found %s in %s", (include, subdir))
return subdir return subdir
return False
def _cmd_exists(cmd: str) -> bool: def _cmd_exists(cmd: str) -> bool:
@ -256,7 +262,7 @@ def _cmd_exists(cmd: str) -> bool:
) )
def _pkg_config(name): def _pkg_config(name: str) -> tuple[list[str], list[str]] | None:
command = os.environ.get("PKG_CONFIG", "pkg-config") command = os.environ.get("PKG_CONFIG", "pkg-config")
for keep_system in (True, False): for keep_system in (True, False):
try: try:
@ -283,10 +289,11 @@ def _pkg_config(name):
return libs, cflags return libs, cflags
except Exception: except Exception:
pass pass
return None
class pil_build_ext(build_ext): class pil_build_ext(build_ext):
class feature: class ext_feature:
features = [ features = [
"zlib", "zlib",
"jpeg", "jpeg",
@ -301,25 +308,32 @@ class pil_build_ext(build_ext):
] ]
required = {"jpeg", "zlib"} required = {"jpeg", "zlib"}
vendor = set() vendor: set[str] = set()
def __init__(self): def __init__(self) -> None:
self._settings: dict[str, str | bool | None] = {}
for f in self.features: for f in self.features:
setattr(self, f, None) self.set(f, None)
def require(self, feat): def require(self, feat: str) -> bool:
return feat in self.required return feat in self.required
def want(self, feat): def get(self, feat: str) -> str | bool | None:
return getattr(self, feat) is None return self._settings[feat]
def want_vendor(self, feat): def set(self, feat: str, value: str | bool | None) -> None:
self._settings[feat] = value
def want(self, feat: str) -> bool:
return self._settings[feat] is None
def want_vendor(self, feat: str) -> bool:
return feat in self.vendor return feat in self.vendor
def __iter__(self): def __iter__(self) -> Iterator[str]:
yield from self.features yield from self.features
feature = feature() feature = ext_feature()
user_options = ( user_options = (
build_ext.user_options build_ext.user_options
@ -337,10 +351,10 @@ class pil_build_ext(build_ext):
) )
@staticmethod @staticmethod
def check_configuration(option, value): def check_configuration(option: str, value: str) -> bool | None:
return True if value in configuration.get(option, []) else None return True if value in configuration.get(option, []) else None
def initialize_options(self): def initialize_options(self) -> None:
self.disable_platform_guessing = self.check_configuration( self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable" "platform-guessing", "disable"
) )
@ -355,7 +369,7 @@ class pil_build_ext(build_ext):
self.debug = True self.debug = True
self.parallel = configuration.get("parallel", [None])[-1] self.parallel = configuration.get("parallel", [None])[-1]
def finalize_options(self): def finalize_options(self) -> None:
build_ext.finalize_options(self) build_ext.finalize_options(self)
if self.debug: if self.debug:
global DEBUG global DEBUG
@ -363,12 +377,16 @@ class pil_build_ext(build_ext):
if not self.parallel: if not self.parallel:
# If --parallel (or -j) wasn't specified, we want to reproduce the same # If --parallel (or -j) wasn't specified, we want to reproduce the same
# behavior as before, that is, auto-detect the number of jobs. # behavior as before, that is, auto-detect the number of jobs.
try: self.parallel = None
self.parallel = int(
os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) cpu_count = os.cpu_count()
) if cpu_count is not None:
except TypeError: try:
self.parallel = None self.parallel = int(
os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
)
except TypeError:
pass
for x in self.feature: for x in self.feature:
if getattr(self, f"disable_{x}"): if getattr(self, f"disable_{x}"):
setattr(self.feature, x, False) setattr(self.feature, x, False)
@ -402,7 +420,13 @@ class pil_build_ext(build_ext):
_dbg("Using vendored version of %s", x) _dbg("Using vendored version of %s", x)
self.feature.vendor.add(x) self.feature.vendor.add(x)
def _update_extension(self, name, libraries, define_macros=None, sources=None): def _update_extension(
self,
name: str,
libraries: list[str] | list[str | bool | None],
define_macros: list[tuple[str, str | None]] | None = None,
sources: list[str] | None = None,
) -> None:
for extension in self.extensions: for extension in self.extensions:
if extension.name == name: if extension.name == name:
extension.libraries += libraries extension.libraries += libraries
@ -415,13 +439,13 @@ class pil_build_ext(build_ext):
extension.extra_link_args = ["--stdlib=libc++"] extension.extra_link_args = ["--stdlib=libc++"]
break break
def _remove_extension(self, name): def _remove_extension(self, name: str) -> None:
for extension in self.extensions: for extension in self.extensions:
if extension.name == name: if extension.name == name:
self.extensions.remove(extension) self.extensions.remove(extension)
break break
def get_macos_sdk_path(self): def get_macos_sdk_path(self) -> str | None:
try: try:
sdk_path = ( sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"]) subprocess.check_output(["xcrun", "--show-sdk-path"])
@ -442,9 +466,9 @@ class pil_build_ext(build_ext):
sdk_path = commandlinetools_sdk_path sdk_path = commandlinetools_sdk_path
return sdk_path return sdk_path
def build_extensions(self): def build_extensions(self) -> None:
library_dirs = [] library_dirs: list[str] = []
include_dirs = [] include_dirs: list[str] = []
pkg_config = None pkg_config = None
if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")):
@ -468,19 +492,22 @@ class pil_build_ext(build_ext):
root = globals()[root_name] root = globals()[root_name]
if root is None and root_name in os.environ: if root is None and root_name in os.environ:
prefix = os.environ[root_name] root_prefix = os.environ[root_name]
root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) root = (
os.path.join(root_prefix, "lib"),
os.path.join(root_prefix, "include"),
)
if root is None and pkg_config: if root is None and pkg_config:
if isinstance(lib_name, tuple): if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
else:
for lib_name2 in lib_name: for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.") _dbg(f"Looking for `{lib_name2}` using pkg-config.")
root = pkg_config(lib_name2) root = pkg_config(lib_name2)
if root: if root:
break break
else:
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
if isinstance(root, tuple): if isinstance(root, tuple):
lib_root, include_root = root lib_root, include_root = root
@ -660,22 +687,22 @@ class pil_build_ext(build_ext):
_dbg("Looking for zlib") _dbg("Looking for zlib")
if _find_include_file(self, "zlib.h"): if _find_include_file(self, "zlib.h"):
if _find_library_file(self, "z"): if _find_library_file(self, "z"):
feature.zlib = "z" feature.set("zlib", "z")
elif sys.platform == "win32" and _find_library_file(self, "zlib"): elif sys.platform == "win32" and _find_library_file(self, "zlib"):
feature.zlib = "zlib" # alternative name feature.set("zlib", "zlib") # alternative name
if feature.want("jpeg"): if feature.want("jpeg"):
_dbg("Looking for jpeg") _dbg("Looking for jpeg")
if _find_include_file(self, "jpeglib.h"): if _find_include_file(self, "jpeglib.h"):
if _find_library_file(self, "jpeg"): if _find_library_file(self, "jpeg"):
feature.jpeg = "jpeg" feature.set("jpeg", "jpeg")
elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): elif sys.platform == "win32" and _find_library_file(self, "libjpeg"):
feature.jpeg = "libjpeg" # alternative name feature.set("jpeg", "libjpeg") # alternative name
feature.openjpeg_version = None feature.set("openjpeg_version", None)
if feature.want("jpeg2000"): if feature.want("jpeg2000"):
_dbg("Looking for jpeg2000") _dbg("Looking for jpeg2000")
best_version = None best_version: tuple[int, ...] | None = None
best_path = None best_path = None
# Find the best version # Find the best version
@ -705,26 +732,26 @@ class pil_build_ext(build_ext):
# <openjpeg.h> rather than having to cope with the versioned # <openjpeg.h> rather than having to cope with the versioned
# include path # include path
_add_directory(self.compiler.include_dirs, best_path, 0) _add_directory(self.compiler.include_dirs, best_path, 0)
feature.jpeg2000 = "openjp2" feature.set("jpeg2000", "openjp2")
feature.openjpeg_version = ".".join(str(x) for x in best_version) feature.set("openjpeg_version", ".".join(str(x) for x in best_version))
if feature.want("imagequant"): if feature.want("imagequant"):
_dbg("Looking for imagequant") _dbg("Looking for imagequant")
if _find_include_file(self, "libimagequant.h"): if _find_include_file(self, "libimagequant.h"):
if _find_library_file(self, "imagequant"): if _find_library_file(self, "imagequant"):
feature.imagequant = "imagequant" feature.set("imagequant", "imagequant")
elif _find_library_file(self, "libimagequant"): elif _find_library_file(self, "libimagequant"):
feature.imagequant = "libimagequant" feature.set("imagequant", "libimagequant")
if feature.want("tiff"): if feature.want("tiff"):
_dbg("Looking for tiff") _dbg("Looking for tiff")
if _find_include_file(self, "tiff.h"): if _find_include_file(self, "tiff.h"):
if _find_library_file(self, "tiff"): if _find_library_file(self, "tiff"):
feature.tiff = "tiff" feature.set("tiff", "tiff")
if sys.platform in ["win32", "darwin"] and _find_library_file( if sys.platform in ["win32", "darwin"] and _find_library_file(
self, "libtiff" self, "libtiff"
): ):
feature.tiff = "libtiff" feature.set("tiff", "libtiff")
if feature.want("freetype"): if feature.want("freetype"):
_dbg("Looking for freetype") _dbg("Looking for freetype")
@ -745,31 +772,31 @@ class pil_build_ext(build_ext):
freetype_version = 21 freetype_version = 21
break break
if freetype_version: if freetype_version:
feature.freetype = "freetype" feature.set("freetype", "freetype")
if subdir: if subdir:
_add_directory(self.compiler.include_dirs, subdir, 0) _add_directory(self.compiler.include_dirs, subdir, 0)
if feature.freetype and feature.want("raqm"): if feature.get("freetype") and feature.want("raqm"):
if not feature.want_vendor("raqm"): # want system Raqm if not feature.want_vendor("raqm"): # want system Raqm
_dbg("Looking for Raqm") _dbg("Looking for Raqm")
if _find_include_file(self, "raqm.h"): if _find_include_file(self, "raqm.h"):
if _find_library_file(self, "raqm"): if _find_library_file(self, "raqm"):
feature.raqm = "raqm" feature.set("raqm", "raqm")
elif _find_library_file(self, "libraqm"): elif _find_library_file(self, "libraqm"):
feature.raqm = "libraqm" feature.set("raqm", "libraqm")
else: # want to build Raqm from src/thirdparty else: # want to build Raqm from src/thirdparty
_dbg("Looking for HarfBuzz") _dbg("Looking for HarfBuzz")
feature.harfbuzz = None feature.set("harfbuzz", None)
hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") hb_dir = _find_include_dir(self, "harfbuzz", "hb.h")
if hb_dir: if hb_dir:
if isinstance(hb_dir, str): if isinstance(hb_dir, str):
_add_directory(self.compiler.include_dirs, hb_dir, 0) _add_directory(self.compiler.include_dirs, hb_dir, 0)
if _find_library_file(self, "harfbuzz"): if _find_library_file(self, "harfbuzz"):
feature.harfbuzz = "harfbuzz" feature.set("harfbuzz", "harfbuzz")
if feature.harfbuzz: if feature.get("harfbuzz"):
if not feature.want_vendor("fribidi"): # want system FriBiDi if not feature.want_vendor("fribidi"): # want system FriBiDi
_dbg("Looking for FriBiDi") _dbg("Looking for FriBiDi")
feature.fribidi = None feature.set("fribidi", None)
fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h")
if fribidi_dir: if fribidi_dir:
if isinstance(fribidi_dir, str): if isinstance(fribidi_dir, str):
@ -777,19 +804,19 @@ class pil_build_ext(build_ext):
self.compiler.include_dirs, fribidi_dir, 0 self.compiler.include_dirs, fribidi_dir, 0
) )
if _find_library_file(self, "fribidi"): if _find_library_file(self, "fribidi"):
feature.fribidi = "fribidi" feature.set("fribidi", "fribidi")
feature.raqm = True feature.set("raqm", True)
else: # want to build FriBiDi shim from src/thirdparty else: # want to build FriBiDi shim from src/thirdparty
feature.raqm = True feature.set("raqm", True)
if feature.want("lcms"): if feature.want("lcms"):
_dbg("Looking for lcms") _dbg("Looking for lcms")
if _find_include_file(self, "lcms2.h"): if _find_include_file(self, "lcms2.h"):
if _find_library_file(self, "lcms2"): if _find_library_file(self, "lcms2"):
feature.lcms = "lcms2" feature.set("lcms", "lcms2")
elif _find_library_file(self, "lcms2_static"): elif _find_library_file(self, "lcms2_static"):
# alternate Windows name. # alternate Windows name.
feature.lcms = "lcms2_static" feature.set("lcms", "lcms2_static")
if feature.want("webp"): if feature.want("webp"):
_dbg("Looking for webp") _dbg("Looking for webp")
@ -803,17 +830,17 @@ class pil_build_ext(build_ext):
_find_library_file(self, prefix + library) _find_library_file(self, prefix + library)
for library in ("webp", "webpmux", "webpdemux") for library in ("webp", "webpmux", "webpdemux")
): ):
feature.webp = prefix + "webp" feature.set("webp", prefix + "webp")
break break
if feature.want("xcb"): if feature.want("xcb"):
_dbg("Looking for xcb") _dbg("Looking for xcb")
if _find_include_file(self, "xcb/xcb.h"): if _find_include_file(self, "xcb/xcb.h"):
if _find_library_file(self, "xcb"): if _find_library_file(self, "xcb"):
feature.xcb = "xcb" feature.set("xcb", "xcb")
for f in feature: for f in feature:
if not getattr(feature, f) and feature.require(f): if not feature.get(f) and feature.require(f):
if f in ("jpeg", "zlib"): if f in ("jpeg", "zlib"):
raise RequiredDependencyException(f) raise RequiredDependencyException(f)
raise DependencyException(f) raise DependencyException(f)
@ -821,10 +848,11 @@ class pil_build_ext(build_ext):
# #
# core library # core library
libs = self.add_imaging_libs.split() libs: list[str | bool | None] = []
defs = [] libs.extend(self.add_imaging_libs.split())
if feature.tiff: defs: list[tuple[str, str | None]] = []
libs.append(feature.tiff) if feature.get("tiff"):
libs.append(feature.get("tiff"))
defs.append(("HAVE_LIBTIFF", None)) defs.append(("HAVE_LIBTIFF", None))
if sys.platform == "win32": if sys.platform == "win32":
# This define needs to be defined if-and-only-if it was defined # This define needs to be defined if-and-only-if it was defined
@ -832,22 +860,22 @@ class pil_build_ext(build_ext):
# so we have to guess; by default it is defined in all Windows builds. # so we have to guess; by default it is defined in all Windows builds.
# See #4237, #5243, #5359 for more information. # See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None)) defs.append(("USE_WIN32_FILEIO", None))
if feature.jpeg: if feature.get("jpeg"):
libs.append(feature.jpeg) libs.append(feature.get("jpeg"))
defs.append(("HAVE_LIBJPEG", None)) defs.append(("HAVE_LIBJPEG", None))
if feature.jpeg2000: if feature.get("jpeg2000"):
libs.append(feature.jpeg2000) libs.append(feature.get("jpeg2000"))
defs.append(("HAVE_OPENJPEG", None)) defs.append(("HAVE_OPENJPEG", None))
if sys.platform == "win32" and not PLATFORM_MINGW: if sys.platform == "win32" and not PLATFORM_MINGW:
defs.append(("OPJ_STATIC", None)) defs.append(("OPJ_STATIC", None))
if feature.zlib: if feature.get("zlib"):
libs.append(feature.zlib) libs.append(feature.get("zlib"))
defs.append(("HAVE_LIBZ", None)) defs.append(("HAVE_LIBZ", None))
if feature.imagequant: if feature.get("imagequant"):
libs.append(feature.imagequant) libs.append(feature.get("imagequant"))
defs.append(("HAVE_LIBIMAGEQUANT", None)) defs.append(("HAVE_LIBIMAGEQUANT", None))
if feature.xcb: if feature.get("xcb"):
libs.append(feature.xcb) libs.append(feature.get("xcb"))
defs.append(("HAVE_XCB", None)) defs.append(("HAVE_XCB", None))
if sys.platform == "win32": if sys.platform == "win32":
libs.extend(["kernel32", "user32", "gdi32"]) libs.extend(["kernel32", "user32", "gdi32"])
@ -861,22 +889,22 @@ class pil_build_ext(build_ext):
# #
# additional libraries # additional libraries
if feature.freetype: if feature.get("freetype"):
srcs = [] srcs = []
libs = ["freetype"] libs = ["freetype"]
defs = [] defs = []
if feature.raqm: if feature.get("raqm"):
if not feature.want_vendor("raqm"): # using system Raqm if not feature.want_vendor("raqm"): # using system Raqm
defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM", None))
defs.append(("HAVE_RAQM_SYSTEM", None)) defs.append(("HAVE_RAQM_SYSTEM", None))
libs.append(feature.raqm) libs.append(feature.get("raqm"))
else: # building Raqm from src/thirdparty else: # building Raqm from src/thirdparty
defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM", None))
srcs.append("src/thirdparty/raqm/raqm.c") srcs.append("src/thirdparty/raqm/raqm.c")
libs.append(feature.harfbuzz) libs.append(feature.get("harfbuzz"))
if not feature.want_vendor("fribidi"): # using system FriBiDi if not feature.want_vendor("fribidi"): # using system FriBiDi
defs.append(("HAVE_FRIBIDI_SYSTEM", None)) defs.append(("HAVE_FRIBIDI_SYSTEM", None))
libs.append(feature.fribidi) libs.append(feature.get("fribidi"))
else: # building FriBiDi shim from src/thirdparty else: # building FriBiDi shim from src/thirdparty
srcs.append("src/thirdparty/fribidi-shim/fribidi.c") srcs.append("src/thirdparty/fribidi-shim/fribidi.c")
self._update_extension("PIL._imagingft", libs, defs, srcs) self._update_extension("PIL._imagingft", libs, defs, srcs)
@ -884,16 +912,17 @@ class pil_build_ext(build_ext):
else: else:
self._remove_extension("PIL._imagingft") self._remove_extension("PIL._imagingft")
if feature.lcms: if feature.get("lcms"):
extra = [] libs = [feature.get("lcms")]
if sys.platform == "win32": if sys.platform == "win32":
extra.extend(["user32", "gdi32"]) libs.extend(["user32", "gdi32"])
self._update_extension("PIL._imagingcms", [feature.lcms] + extra) self._update_extension("PIL._imagingcms", libs)
else: else:
self._remove_extension("PIL._imagingcms") self._remove_extension("PIL._imagingcms")
if feature.webp: webp = feature.get("webp")
libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] if isinstance(webp, str):
libs = [webp, webp + "mux", webp + "demux"]
self._update_extension("PIL._webp", libs) self._update_extension("PIL._webp", libs)
else: else:
self._remove_extension("PIL._webp") self._remove_extension("PIL._webp")
@ -908,14 +937,14 @@ class pil_build_ext(build_ext):
self.summary_report(feature) self.summary_report(feature)
def summary_report(self, feature): def summary_report(self, feature: ext_feature) -> None:
print("-" * 68) print("-" * 68)
print("PIL SETUP SUMMARY") print("PIL SETUP SUMMARY")
print("-" * 68) print("-" * 68)
print(f"version Pillow {PILLOW_VERSION}") print(f"version Pillow {PILLOW_VERSION}")
v = sys.version.split("[") version = sys.version.split("[")
print(f"platform {sys.platform} {v[0].strip()}") print(f"platform {sys.platform} {version[0].strip()}")
for v in v[1:]: for v in version[1:]:
print(f" [{v.strip()}") print(f" [{v.strip()}")
print("-" * 68) print("-" * 68)
@ -926,16 +955,20 @@ class pil_build_ext(build_ext):
raqm_extra_info += ", FriBiDi shim" raqm_extra_info += ", FriBiDi shim"
options = [ options = [
(feature.jpeg, "JPEG"), (feature.get("jpeg"), "JPEG"),
(feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), (
(feature.zlib, "ZLIB (PNG/ZIP)"), feature.get("jpeg2000"),
(feature.imagequant, "LIBIMAGEQUANT"), "OPENJPEG (JPEG2000)",
(feature.tiff, "LIBTIFF"), feature.get("openjpeg_version"),
(feature.freetype, "FREETYPE2"), ),
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.get("zlib"), "ZLIB (PNG/ZIP)"),
(feature.lcms, "LITTLECMS2"), (feature.get("imagequant"), "LIBIMAGEQUANT"),
(feature.webp, "WEBP"), (feature.get("tiff"), "LIBTIFF"),
(feature.xcb, "XCB (X protocol)"), (feature.get("freetype"), "FREETYPE2"),
(feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info),
(feature.get("lcms"), "LITTLECMS2"),
(feature.get("webp"), "WEBP"),
(feature.get("xcb"), "XCB (X protocol)"),
] ]
all = 1 all = 1
@ -964,7 +997,7 @@ class pil_build_ext(build_ext):
print("") print("")
def debug_build(): def debug_build() -> bool:
return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD

View File

@ -273,13 +273,13 @@ class BlpImageFile(ImageFile.ImageFile):
raise BLPFormatError(msg) raise BLPFormatError(msg)
self._mode = "RGBA" if self._blp_alpha_depth else "RGB" self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_blp_header()
self._load() self._load()
@ -372,7 +372,10 @@ class BLP1Decoder(_BLPBaseDecoder):
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
if image.mode == "CMYK": if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0] decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))] assert isinstance(args, tuple)
image.tile = [
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
]
r, g, b = image.convert("RGB").split() r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r)) reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes()) self.set_as_raw(reversed_image.tobytes())

View File

@ -296,7 +296,7 @@ class BmpImageFile(ImageFile.ImageFile):
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"]) args.append(file_info["direction"])
self.tile = [ self.tile = [
( ImageFile._Tile(
decoder_name, decoder_name,
(0, 0, file_info["width"], file_info["height"]), (0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(), offset or self.fp.tell(),
@ -321,7 +321,7 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder): class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
rle4 = self.args[1] rle4 = self.args[1]
data = bytearray() data = bytearray()
@ -387,7 +387,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
if self.fd.tell() % 2 != 0: if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR) self.fd.seek(1, os.SEEK_CUR)
rawmode = "L" if self.mode == "L" else "P" rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
return -1, 0 return -1, 0

View File

@ -17,7 +17,7 @@
# #
from __future__ import annotations from __future__ import annotations
from . import BmpImagePlugin, Image from . import BmpImagePlugin, Image, ImageFile
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
@ -64,7 +64,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
# patch up the bitmap height # patch up the bitmap height
self._size = self.size[0], self.size[1] // 2 self._size = self.size[0], self.size[1] // 2
d, e, o, a = self.tile[0] d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
# #

View File

@ -367,7 +367,7 @@ class DdsImageFile(ImageFile.ImageFile):
mask_count = 3 mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))] self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
return return
elif pfflags & DDPF.LUMINANCE: elif pfflags & DDPF.LUMINANCE:
if bitcount == 8: if bitcount == 8:
@ -481,7 +481,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder): class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
bitcount, masks = self.args bitcount, masks = self.args

View File

@ -67,7 +67,7 @@ class FitsImageFile(ImageFile.ImageFile):
raise ValueError(msg) raise ValueError(msg)
offset += self.fp.tell() - 80 offset += self.fp.tell() - 80
self.tile = [(decoder_name, (0, 0) + self.size, offset, args)] self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
def _get_size( def _get_size(
self, headers: dict[bytes, bytes], prefix: bytes self, headers: dict[bytes, bytes], prefix: bytes
@ -126,7 +126,7 @@ class FitsImageFile(ImageFile.ImageFile):
class FitsGzipDecoder(ImageFile.PyDecoder): class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
value = gzip.decompress(self.fd.read()) value = gzip.decompress(self.fd.read())

View File

@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
framesize = i32(s) framesize = i32(s)
self.decodermaxblock = framesize self.decodermaxblock = framesize
self.tile = [("fli", (0, 0) + self.size, self.__offset, None)] self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)]
self.__offset += framesize self.__offset += framesize

View File

@ -166,7 +166,7 @@ class FpxImageFile(ImageFile.ImageFile):
if compression == 0: if compression == 0:
self.tile.append( self.tile.append(
( ImageFile._Tile(
"raw", "raw",
(x, y, x1, y1), (x, y, x1, y1),
i32(s, i) + 28, i32(s, i) + 28,
@ -177,7 +177,7 @@ class FpxImageFile(ImageFile.ImageFile):
elif compression == 1: elif compression == 1:
# FIXME: the fill decoder is not implemented # FIXME: the fill decoder is not implemented
self.tile.append( self.tile.append(
( ImageFile._Tile(
"fill", "fill",
(x, y, x1, y1), (x, y, x1, y1),
i32(s, i) + 28, i32(s, i) + 28,
@ -205,7 +205,7 @@ class FpxImageFile(ImageFile.ImageFile):
jpegmode = rawmode jpegmode = rawmode
self.tile.append( self.tile.append(
( ImageFile._Tile(
"jpeg", "jpeg",
(x, y, x1, y1), (x, y, x1, y1),
i32(s, i) + 28, i32(s, i) + 28,

View File

@ -93,9 +93,9 @@ class FtexImageFile(ImageFile.ImageFile):
if format == Format.DXT1: if format == Format.DXT1:
self._mode = "RGBA" self._mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, 1)] self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED: elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else: else:
msg = f"Invalid texture compression format: {repr(format)}" msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg) raise ValueError(msg)

View File

@ -72,7 +72,7 @@ class GdImageFile(ImageFile.ImageFile):
) )
self.tile = [ self.tile = [
( ImageFile._Tile(
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4, 7 + true_color_offset + 4 + 256 * 4,

View File

@ -29,7 +29,6 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
@ -49,6 +48,7 @@ from ._binary import o16le as o16
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
from ._typing import Buffer
class LoadingStrategy(IntEnum): class LoadingStrategy(IntEnum):
@ -407,7 +407,7 @@ class GifImageFile(ImageFile.ImageFile):
elif self.mode not in ("RGB", "RGBA"): elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency transparency = frame_transparency
self.tile = [ self.tile = [
( ImageFile._Tile(
"gif", "gif",
(x0, y0, x1, y1), (x0, y0, x1, y1),
self.__offset, self.__offset,
@ -438,6 +438,13 @@ class GifImageFile(ImageFile.ImageFile):
self.im.putpalette("RGB", *self._frame_palette.getdata()) self.im.putpalette("RGB", *self._frame_palette.getdata())
else: else:
self._im = None self._im = None
if not self._prev_im and self._im is not None and self.size != self.im.size:
expanded_im = Image.core.fill(self.im.mode, self.size)
if self._frame_palette:
expanded_im.putpalette("RGB", *self._frame_palette.getdata())
expanded_im.paste(self.im, (0, 0) + self.im.size)
self.im = expanded_im
self._mode = temp_mode self._mode = temp_mode
self._frame_palette = None self._frame_palette = None
@ -455,6 +462,17 @@ class GifImageFile(ImageFile.ImageFile):
return return
if not self._prev_im: if not self._prev_im:
return return
if self.size != self._prev_im.size:
if self._frame_transparency is not None:
expanded_im = Image.core.fill("RGBA", self.size)
else:
expanded_im = Image.core.fill("P", self.size)
expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
expanded_im = expanded_im.convert("RGB")
expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)
self._prev_im = expanded_im
assert self._prev_im is not None
if self._frame_transparency is not None: if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0) self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA") frame_im = self.im.convert("RGBA")
@ -535,7 +553,9 @@ def _normalize_palette(
if im.mode == "P": if im.mode == "P":
if not source_palette: 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 else: # L-mode
if not source_palette: if not source_palette:
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
@ -611,7 +631,10 @@ def _write_single_frame(
def _getbbox( def _getbbox(
base_im: Image.Image, im_frame: Image.Image base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]: ) -> 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") im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA") base_im = base_im.convert("RGBA")
delta = ImageChops.subtract_modulo(im_frame, base_im) delta = ImageChops.subtract_modulo(im_frame, base_im)
@ -966,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
:param im: Image object :param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header :returns: Bytes, len<=768 suitable for inclusion in gif header
""" """
return 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( def _get_background(
@ -1139,18 +1168,9 @@ def getdata(
class Collector(BytesIO): class Collector(BytesIO):
data = [] data = []
if sys.version_info >= (3, 12): def write(self, data: Buffer) -> int:
from collections.abc import Buffer self.data.append(data)
return len(data)
def write(self, data: Buffer) -> int:
self.data.append(data)
return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available im.load() # make sure raster data is available

View File

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

View File

@ -228,7 +228,7 @@ class IcoFile:
# change tile dimension to only encompass XOR image # change tile dimension to only encompass XOR image
im._size = (im.size[0], int(im.size[1] / 2)) im._size = (im.size[0], int(im.size[1] / 2))
d, e, o, a = im.tile[0] d, e, o, a = im.tile[0]
im.tile[0] = d, (0, 0) + im.size, o, a im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
# figure out where AND mask image starts # figure out where AND mask image starts
if header.bpp == 32: if header.bpp == 32:
@ -243,13 +243,19 @@ class IcoFile:
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
# convert to an 8bpp grayscale image # convert to an 8bpp grayscale image
mask = Image.frombuffer( try:
"L", # 8bpp mask = Image.frombuffer(
im.size, # (w, h) "L", # 8bpp
alpha_bytes, # source chars im.size, # (w, h)
"raw", # raw decoder alpha_bytes, # source chars
("L", 0, -1), # 8bpp inverted, unpadded, reversed "raw", # raw decoder
) ("L", 0, -1), # 8bpp inverted, unpadded, reversed
)
except ValueError:
if ImageFile.LOAD_TRUNCATED_IMAGES:
mask = None
else:
raise
else: else:
# get AND image from end of bitmap # get AND image from end of bitmap
w = im.size[0] w = im.size[0]
@ -267,19 +273,26 @@ class IcoFile:
mask_data = self.buf.read(total_bytes) mask_data = self.buf.read(total_bytes)
# convert raw data to image # convert raw data to image
mask = Image.frombuffer( try:
"1", # 1 bpp mask = Image.frombuffer(
im.size, # (w, h) "1", # 1 bpp
mask_data, # source chars im.size, # (w, h)
"raw", # raw decoder mask_data, # source chars
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed "raw", # raw decoder
) ("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
)
except ValueError:
if ImageFile.LOAD_TRUNCATED_IMAGES:
mask = None
else:
raise
# now we have two images, im is XOR image and mask is AND image # now we have two images, im is XOR image and mask is AND image
# apply mask image as alpha channel # apply mask image as alpha channel
im = im.convert("RGBA") if mask:
im.putalpha(mask) im = im.convert("RGBA")
im.putalpha(mask)
return im return im

View File

@ -253,7 +253,11 @@ class ImImageFile(ImageFile.ImageFile):
# use bit decoder (if necessary) # use bit decoder (if necessary)
bits = int(self.rawmode[2:]) bits = int(self.rawmode[2:])
if bits not in [8, 16, 32]: if bits not in [8, 16, 32]:
self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))] self.tile = [
ImageFile._Tile(
"bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
)
]
return return
except ValueError: except ValueError:
pass pass
@ -263,13 +267,17 @@ class ImImageFile(ImageFile.ImageFile):
# ever stumbled upon such a file ;-) # ever stumbled upon such a file ;-)
size = self.size[0] * self.size[1] size = self.size[0] * self.size[1]
self.tile = [ self.tile = [
("raw", (0, 0) + self.size, offs, ("G", 0, -1)), ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)), ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)), ImageFile._Tile(
"raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
),
] ]
else: else:
# LabEye/IFUNC files # LabEye/IFUNC files
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
]
@property @property
def n_frames(self) -> int: def n_frames(self) -> int:
@ -295,7 +303,9 @@ class ImImageFile(ImageFile.ImageFile):
self.fp = self._fp self.fp = self._fp
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
]
def tell(self) -> int: def tell(self) -> int:
return self.frame return self.frame

View File

@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]:
:param t: object to check if it's an image :param t: object to check if it's an image
:returns: True if the object is an image :returns: True if the object is an image
""" """
deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)")
return hasattr(t, "im") return hasattr(t, "im")
@ -225,6 +226,11 @@ if TYPE_CHECKING:
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -851,7 +857,10 @@ class Image:
) )
def frombytes( def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any self,
data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> None: ) -> None:
""" """
Loads this image with pixel data from a bytes object. Loads this image with pixel data from a bytes object.
@ -1058,7 +1067,7 @@ class Image:
trns_im = new(self.mode, (1, 1)) trns_im = new(self.mode, (1, 1))
if self.mode == "P": if self.mode == "P":
assert self.palette is not None assert self.palette is not None
trns_im.putpalette(self.palette) trns_im.putpalette(self.palette, self.palette.mode)
if isinstance(t, tuple): if isinstance(t, tuple):
err = "Couldn't allocate a palette color for transparency" err = "Couldn't allocate a palette color for transparency"
assert trns_im.palette is not None assert trns_im.palette is not None
@ -1122,17 +1131,23 @@ class Image:
return new_im return new_im
if "LAB" in (self.mode, mode): 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"): if other_mode in ("RGB", "RGBA", "RGBX"):
from . import ImageCms from . import ImageCms
srgb = ImageCms.createProfile("sRGB") srgb = ImageCms.createProfile("sRGB")
lab = ImageCms.createProfile("LAB") 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( 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 # colorspace conversion
if dither is None: if dither is None:
@ -1598,7 +1613,7 @@ class Image:
self.fp.seek(offset) self.fp.seek(offset)
return child_images return child_images
def getim(self): def getim(self) -> CapsuleType:
""" """
Returns a capsule that points to the internal image memory. Returns a capsule that points to the internal image memory.
@ -1809,23 +1824,22 @@ class Image:
:param mask: An optional mask image. :param mask: An optional mask image.
""" """
if isImageType(box): if isinstance(box, Image):
if mask is not None: if mask is not None:
msg = "If using second argument as mask, third argument must be None" msg = "If using second argument as mask, third argument must be None"
raise ValueError(msg) raise ValueError(msg)
# abbreviated paste(im, mask) syntax # abbreviated paste(im, mask) syntax
mask = box mask = box
box = None box = None
assert not isinstance(box, Image)
if box is None: if box is None:
box = (0, 0) box = (0, 0)
if len(box) == 2: if len(box) == 2:
# upper left corner given; get size from image or mask # upper left corner given; get size from image or mask
if isImageType(im): if isinstance(im, Image):
size = im.size size = im.size
elif isImageType(mask): elif isinstance(mask, Image):
size = mask.size size = mask.size
else: else:
# FIXME: use self.size here? # FIXME: use self.size here?
@ -1838,17 +1852,15 @@ class Image:
from . import ImageColor from . import ImageColor
source = ImageColor.getcolor(im, self.mode) source = ImageColor.getcolor(im, self.mode)
elif isImageType(im): elif isinstance(im, Image):
im.load() im.load()
if self.mode != im.mode: if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this! # should use an adapter for this!
im = im.convert(self.mode) im = im.convert(self.mode)
source = im.im source = im.im
elif isinstance(im, tuple):
source = im
else: else:
source = cast(float, im) source = im
self._ensure_mutable() self._ensure_mutable()
@ -2009,7 +2021,7 @@ class Image:
else: else:
band = 3 band = 3
if isImageType(alpha): if isinstance(alpha, Image):
# alpha layer # alpha layer
if alpha.mode not in ("1", "L"): if alpha.mode not in ("1", "L"):
msg = "illegal image mode" msg = "illegal image mode"
@ -2019,7 +2031,6 @@ class Image:
alpha = alpha.convert("L") alpha = alpha.convert("L")
else: else:
# constant alpha # constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try: try:
self.im.fillband(band, alpha) self.im.fillband(band, alpha)
except (AttributeError, ValueError): except (AttributeError, ValueError):
@ -2168,6 +2179,9 @@ class Image:
source_palette = self.im.getpalette(palette_mode, palette_mode) source_palette = self.im.getpalette(palette_mode, palette_mode)
else: # L-mode else: # L-mode
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
elif len(source_palette) > 768:
bands = 4
palette_mode = "RGBA"
palette_bytes = b"" palette_bytes = b""
new_positions = [0] * 256 new_positions = [0] * 256
@ -3140,7 +3154,7 @@ def new(
def frombytes( def frombytes(
mode: str, mode: str,
size: tuple[int, int], size: tuple[int, int],
data: bytes | bytearray, data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw", decoder_name: str = "raw",
*args: Any, *args: Any,
) -> Image: ) -> Image:
@ -3184,7 +3198,11 @@ def frombytes(
def frombuffer( def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any mode: str,
size: tuple[int, int],
data: bytes | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> Image: ) -> Image:
""" """
Creates an image memory referencing pixel data in a byte buffer. Creates an image memory referencing pixel data in a byte buffer.
@ -3641,7 +3659,10 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open( def register_open(
id: str, id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], factory: (
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile]
| type[ImageFile.ImageFile]
),
accept: Callable[[bytes], bool | str] | None = None, accept: Callable[[bytes], bool | str] | None = None,
) -> None: ) -> None:
""" """
@ -3960,7 +3981,7 @@ class Exif(_ExifBase):
self._data.clear() self._data.clear()
self._hidden_data.clear() self._hidden_data.clear()
self._ifds.clear() self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"): while data and data.startswith(b"Exif\x00\x00"):
data = data[6:] data = data[6:]
if not data: if not data:
self._info = None self._info = None
@ -4136,7 +4157,7 @@ class Exif(_ExifBase):
ifd = self._get_ifd_dict(tag_data, tag) ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None: if ifd is not None:
self._ifds[tag] = ifd self._ifds[tag] = ifd
ifd = self._ifds.get(tag, {}) ifd = self._ifds.setdefault(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data: if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = { ifd = {
k: v k: v

View File

@ -34,12 +34,15 @@ import itertools
import os import os
import struct import struct
import sys import sys
from typing import IO, Any, NamedTuple from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import Image from . import Image
from ._deprecate import deprecate from ._deprecate import deprecate
from ._util import is_path from ._util import is_path
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
MAXBLOCK = 65536 MAXBLOCK = 65536
SAFEBLOCK = 1024 * 1024 SAFEBLOCK = 1024 * 1024
@ -107,32 +110,34 @@ class _Tile(NamedTuple):
class ImageFile(Image.Image): class ImageFile(Image.Image):
"""Base class for image file format handlers.""" """Base class for image file format handlers."""
def __init__(self, fp=None, filename=None): def __init__(
self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
) -> None:
super().__init__() super().__init__()
self._min_frame = 0 self._min_frame = 0
self.custom_mimetype = None self.custom_mimetype: str | None = None
self.tile = None self.tile: list[_Tile] = []
""" A list of tile descriptors, or ``None`` """ """ A list of tile descriptors, or ``None`` """
self.readonly = 1 # until we know better self.readonly = 1 # until we know better
self.decoderconfig = () self.decoderconfig: tuple[Any, ...] = ()
self.decodermaxblock = MAXBLOCK self.decodermaxblock = MAXBLOCK
if is_path(fp): if is_path(fp):
# filename # filename
self.fp = open(fp, "rb") self.fp = open(fp, "rb")
self.filename = fp self.filename = os.path.realpath(os.fspath(fp))
self._exclusive_fp = True self._exclusive_fp = True
else: else:
# stream # stream
self.fp = fp self.fp = cast(IO[bytes], fp)
self.filename = filename self.filename = filename if filename is not None else ""
# can be overridden # can be overridden
self._exclusive_fp = None self._exclusive_fp = False
try: try:
try: try:
@ -155,6 +160,9 @@ class ImageFile(Image.Image):
self.fp.close() self.fp.close()
raise raise
def _open(self) -> None:
pass
def get_format_mimetype(self) -> str | None: def get_format_mimetype(self) -> str | None:
if self.custom_mimetype: if self.custom_mimetype:
return self.custom_mimetype return self.custom_mimetype
@ -178,7 +186,7 @@ class ImageFile(Image.Image):
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:
"""Load image data based on tile list""" """Load image data based on tile list"""
if self.tile is None: if not self.tile and self._im is None:
msg = "cannot load this image" msg = "cannot load this image"
raise OSError(msg) raise OSError(msg)
@ -214,6 +222,7 @@ class ImageFile(Image.Image):
args = (args, 0, 1) args = (args, 0, 1)
if ( if (
decoder_name == "raw" decoder_name == "raw"
and isinstance(args, tuple)
and len(args) >= 3 and len(args) >= 3
and args[0] == self.mode and args[0] == self.mode
and args[0] in Image._MAPMODES and args[0] in Image._MAPMODES
@ -724,7 +733,7 @@ class PyDecoder(PyCodec):
def pulls_fd(self) -> bool: def pulls_fd(self) -> bool:
return self._pulls_fd return self._pulls_fd
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
""" """
Override to perform the decoding process. Override to perform the decoding process.
@ -736,19 +745,22 @@ class PyDecoder(PyCodec):
msg = "unavailable in base decoder" msg = "unavailable in base decoder"
raise NotImplementedError(msg) raise NotImplementedError(msg)
def set_as_raw(self, data: bytes, rawmode=None) -> None: def set_as_raw(
self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = ()
) -> None:
""" """
Convenience method to set the internal image from a stream of raw data Convenience method to set the internal image from a stream of raw data
:param data: Bytes to be set :param data: Bytes to be set
:param rawmode: The rawmode to be used for the decoder. :param rawmode: The rawmode to be used for the decoder.
If not specified, it will default to the mode of the image If not specified, it will default to the mode of the image
:param extra: Extra arguments for the decoder.
:returns: None :returns: None
""" """
if not rawmode: if not rawmode:
rawmode = self.mode rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode) d = Image._getdecoder(self.mode, "raw", rawmode, extra)
assert self.im is not None assert self.im is not None
d.setimage(self.im, self.state.extents()) d.setimage(self.im, self.state.extents())
s = d.decode(data) s = d.decode(data)

View File

@ -34,9 +34,9 @@ import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
from . import Image from . import Image, features
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
@ -215,7 +215,7 @@ class FreeTypeFont:
def __init__( def __init__(
self, self,
font: StrOrBytesPath | BinaryIO | None = None, font: StrOrBytesPath | BinaryIO,
size: float = 10, size: float = 10,
index: int = 0, index: int = 0,
encoding: str = "", encoding: str = "",
@ -235,6 +235,21 @@ class FreeTypeFont:
self.index = index self.index = index
self.encoding = encoding self.encoding = encoding
try:
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if freetype_version := features.version_module("freetype2"):
if parse_version(freetype_version) < parse_version("2.9.1"):
warnings.warn(
"Support for FreeType 2.9.0 is deprecated and will be removed "
"in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 "
"or newer, preferably FreeType 2.10.4 which fixes "
"CVE-2020-15999.",
DeprecationWarning,
)
if layout_engine not in (Layout.BASIC, Layout.RAQM): if layout_engine not in (Layout.BASIC, Layout.RAQM):
layout_engine = Layout.BASIC layout_engine = Layout.BASIC
if core.HAVE_RAQM: if core.HAVE_RAQM:
@ -248,7 +263,7 @@ class FreeTypeFont:
self.layout_engine = layout_engine self.layout_engine = layout_engine
def load_from_bytes(f) -> None: def load_from_bytes(f: IO[bytes]) -> None:
self.font_bytes = f.read() self.font_bytes = f.read()
self.font = core.getfont( self.font = core.getfont(
"", size, index, encoding, self.font_bytes, layout_engine "", size, index, encoding, self.font_bytes, layout_engine
@ -270,7 +285,7 @@ class FreeTypeFont:
font, size, index, encoding, layout_engine=layout_engine font, size, index, encoding, layout_engine=layout_engine
) )
else: else:
load_from_bytes(font) load_from_bytes(cast(IO[bytes], font))
def __getstate__(self) -> list[Any]: def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine] return [self.path, self.size, self.index, self.encoding, self.layout_engine]
@ -785,7 +800,7 @@ def load(filename: str) -> ImageFont:
def truetype( def truetype(
font: StrOrBytesPath | BinaryIO | None = None, font: StrOrBytesPath | BinaryIO,
size: float = 10, size: float = 10,
index: int = 0, index: int = 0,
encoding: str = "", encoding: str = "",
@ -857,7 +872,7 @@ def truetype(
:exception ValueError: If the font size is not greater than zero. :exception ValueError: If the font size is not greater than zero.
""" """
def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont:
return FreeTypeFont(font, size, index, encoding, layout_engine) return FreeTypeFont(font, size, index, encoding, layout_engine)
try: try:

View File

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

View File

@ -58,7 +58,7 @@ class ImtImageFile(ImageFile.ImageFile):
if s == b"\x0C": if s == b"\x0C":
# image data begins # image data begins
self.tile = [ self.tile = [
( ImageFile._Tile(
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
self.fp.tell() - len(buffer), self.fp.tell() - len(buffer),

View File

@ -147,7 +147,9 @@ class IptcImageFile(ImageFile.ImageFile):
# tile # tile
if tag == (8, 10): if tag == (8, 10):
self.tile = [("iptc", (0, 0) + self.size, offset, compression)] self.tile = [
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
]
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc": if len(self.tile) != 1 or self.tile[0][0] != "iptc":

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from collections.abc import Callable
from typing import IO, cast from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@ -205,7 +206,7 @@ def _parse_jp2_header(
if bitdepth > max_bitdepth: if bitdepth > max_bitdepth:
max_bitdepth = bitdepth max_bitdepth = bitdepth
if max_bitdepth <= 8: if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette() palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB")
for i in range(ne): for i in range(ne):
color: list[int] = [] color: list[int] = []
for value in header.read_fields(">" + ("B" * npc)): for value in header.read_fields(">" + ("B" * npc)):
@ -286,7 +287,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
length = -1 length = -1
self.tile = [ self.tile = [
( ImageFile._Tile(
"jpeg2k", "jpeg2k",
(0, 0) + self.size, (0, 0) + self.size,
0, 0,
@ -316,8 +317,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
else: else:
self.fp.seek(length - 2, os.SEEK_CUR) self.fp.seek(length - 2, os.SEEK_CUR)
@property @property # type: ignore[override]
def reduce(self): def reduce(
self,
) -> (
Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image]
| int
):
# https://github.com/python-pillow/Pillow/issues/4343 found that the # https://github.com/python-pillow/Pillow/issues/4343 found that the
# new Image 'reduce' method was shadowed by this plugin's 'reduce' # new Image 'reduce' method was shadowed by this plugin's 'reduce'
# property. This attempts to allow for both scenarios # property. This attempts to allow for both scenarios
@ -338,8 +344,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
# Update the reduce and layers settings # Update the reduce and layers settings
t = self.tile[0] t = self.tile[0]
assert isinstance(t[3], tuple)
t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4]) t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)

View File

@ -372,7 +372,9 @@ class JpegImageFile(ImageFile.ImageFile):
rawmode = self.mode rawmode = self.mode
if self.mode == "CMYK": if self.mode == "CMYK":
rawmode = "CMYK;I" # assume adobe conventions rawmode = "CMYK;I" # assume adobe conventions
self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] self.tile = [
ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
]
# self.__offset = self.fp.tell() # self.__offset = self.fp.tell()
break break
s = self.fp.read(1) s = self.fp.read(1)
@ -423,6 +425,7 @@ class JpegImageFile(ImageFile.ImageFile):
scale = 1 scale = 1
original_size = self.size original_size = self.size
assert isinstance(a, tuple)
if a[0] == "RGB" and mode in ["L", "YCbCr"]: if a[0] == "RGB" and mode in ["L", "YCbCr"]:
self._mode = mode self._mode = mode
a = mode, "" a = mode, ""
@ -432,6 +435,7 @@ class JpegImageFile(ImageFile.ImageFile):
for s in [8, 4, 2, 1]: for s in [8, 4, 2, 1]:
if scale >= s: if scale >= s:
break break
assert e is not None
e = ( e = (
e[0], e[0],
e[1], e[1],
@ -441,7 +445,7 @@ class JpegImageFile(ImageFile.ImageFile):
self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s) self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
scale = s scale = s
self.tile = [(d, e, o, a)] self.tile = [ImageFile._Tile(d, e, o, a)]
self.decoderconfig = (scale, 0) self.decoderconfig = (scale, 0)
box = (0, 0, original_size[0] / scale, original_size[1] / scale) box = (0, 0, original_size[0] / scale, original_size[1] / scale)
@ -747,17 +751,27 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
extra = info.get("extra", b"") extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533 MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp"))
if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
if len(xmp) > max_data_bytes_in_marker:
msg = "XMP data is too long"
raise ValueError(msg)
size = o16(2 + overhead_len + len(xmp))
extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
icc_profile = info.get("icc_profile") icc_profile = info.get("icc_profile")
if icc_profile: if icc_profile:
ICC_OVERHEAD_LEN = 14 overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
markers = [] markers = []
while icc_profile: while icc_profile:
markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER]) markers.append(icc_profile[:max_data_bytes_in_marker])
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] icc_profile = icc_profile[max_data_bytes_in_marker:]
i = 1 i = 1
for marker in markers: for marker in markers:
size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) size = o16(2 + overhead_len + len(marker))
extra += ( extra += (
b"\xFF\xE2" b"\xFF\xE2"
+ size + size
@ -844,7 +858,7 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
## ##
# Factory for making JPEG and MPO instances # Factory for making JPEG and MPO instances
def jpeg_factory( def jpeg_factory(
fp: IO[bytes] | None = None, filename: str | bytes | None = None fp: IO[bytes], filename: str | bytes | None = None
) -> JpegImageFile | MpoImageFile: ) -> JpegImageFile | MpoImageFile:
im = JpegImageFile(fp, filename) im = JpegImageFile(fp, filename)
try: try:

View File

@ -67,7 +67,9 @@ class McIdasImageFile(ImageFile.ImageFile):
offset = w[34] + w[15] offset = w[34] + w[15]
stride = w[15] + w[10] * w[11] * w[14] stride = w[15] + w[10] * w[11] * w[14]
self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
]
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -26,6 +26,7 @@ from typing import IO, Any, cast
from . import ( from . import (
Image, Image,
ImageFile,
ImageSequence, ImageSequence,
JpegImagePlugin, JpegImagePlugin,
TiffImagePlugin, TiffImagePlugin,
@ -145,7 +146,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
if self.info.get("exif") != original_exif: if self.info.get("exif") != original_exif:
self._reload_exif() self._reload_exif()
self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] self.tile = [
ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
]
self.__frame = frame self.__frame = frame
def tell(self) -> int: def tell(self) -> int:

View File

@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile):
self._size = i16(s, 4), i16(s, 6) self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM": if s[:4] == b"DanM":
self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
else: else:
self.tile = [("MSP", (0, 0) + self.size, 32, None)] self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)]
class MspDecoder(ImageFile.PyDecoder): class MspDecoder(ImageFile.PyDecoder):
@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
img = io.BytesIO() img = io.BytesIO()
@ -152,7 +152,7 @@ class MspDecoder(ImageFile.PyDecoder):
msg = f"Corrupted MSP file in row {x}" msg = f"Corrupted MSP file in row {x}"
raise OSError(msg) from e raise OSError(msg) from e
self.set_as_raw(img.getvalue(), ("1", 0, 1)) self.set_as_raw(img.getvalue(), "1")
return -1, 0 return -1, 0

View File

@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images! self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)]
def load_end(self) -> None: def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:

View File

@ -128,7 +128,9 @@ class PcxImageFile(ImageFile.ImageFile):
bbox = (0, 0) + self.size bbox = (0, 0) + self.size
logger.debug("size: %sx%s", *self.size) logger.debug("size: %sx%s", *self.size)
self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] self.tile = [
ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
]
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -61,7 +61,9 @@ class PixarImageFile(ImageFile.ImageFile):
# FIXME: to be continued... # FIXME: to be continued...
# create tile descriptor (assuming "dumped") # create tile descriptor (assuming "dumped")
self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))
]
# #

View File

@ -258,7 +258,9 @@ class iTXt(str):
tkey: str | bytes | None tkey: str | bytes | None
@staticmethod @staticmethod
def __new__(cls, text, lang=None, tkey=None): def __new__(
cls, text: str, lang: str | None = None, tkey: str | None = None
) -> iTXt:
""" """
:param cls: the class to use when creating the instance :param cls: the class to use when creating the instance
:param text: value for this key :param text: value for this key
@ -368,21 +370,27 @@ class PngInfo:
# PNG image stream (IHDR/IEND) # PNG image stream (IHDR/IEND)
class _RewindState(NamedTuple):
info: dict[str | tuple[int, int], Any]
tile: list[ImageFile._Tile]
seq_num: int | None
class PngStream(ChunkStream): class PngStream(ChunkStream):
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
super().__init__(fp) super().__init__(fp)
# local copies of Image attributes # local copies of Image attributes
self.im_info = {} self.im_info: dict[str | tuple[int, int], Any] = {}
self.im_text = {} self.im_text: dict[str, str | iTXt] = {}
self.im_size = (0, 0) self.im_size = (0, 0)
self.im_mode = None self.im_mode = ""
self.im_tile = None self.im_tile: list[ImageFile._Tile] = []
self.im_palette = None self.im_palette: tuple[str, bytes] | None = None
self.im_custom_mimetype = None self.im_custom_mimetype: str | None = None
self.im_n_frames = None self.im_n_frames: int | None = None
self._seq_num = None self._seq_num: int | None = None
self.rewind_state = None self.rewind_state = _RewindState({}, [], None)
self.text_memory = 0 self.text_memory = 0
@ -396,16 +404,16 @@ class PngStream(ChunkStream):
raise ValueError(msg) raise ValueError(msg)
def save_rewind(self) -> None: def save_rewind(self) -> None:
self.rewind_state = { self.rewind_state = _RewindState(
"info": self.im_info.copy(), self.im_info.copy(),
"tile": self.im_tile, self.im_tile,
"seq_num": self._seq_num, self._seq_num,
} )
def rewind(self) -> None: def rewind(self) -> None:
self.im_info = self.rewind_state["info"].copy() self.im_info = self.rewind_state.info.copy()
self.im_tile = self.rewind_state["tile"] self.im_tile = self.rewind_state.tile
self._seq_num = self.rewind_state["seq_num"] self._seq_num = self.rewind_state.seq_num
def chunk_iCCP(self, pos: int, length: int) -> bytes: def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile # ICC profile
@ -459,11 +467,11 @@ class PngStream(ChunkStream):
def chunk_IDAT(self, pos: int, length: int) -> NoReturn: def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
# image data # image data
if "bbox" in self.im_info: if "bbox" in self.im_info:
tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)]
else: else:
if self.im_n_frames is not None: if self.im_n_frames is not None:
self.im_info["default_image"] = True self.im_info["default_image"] = True
tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)] tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
self.im_tile = tile self.im_tile = tile
self.im_idat = length self.im_idat = length
msg = "image data found" msg = "image data found"

View File

@ -151,7 +151,9 @@ class PpmImageFile(ImageFile.ImageFile):
decoder_name = "ppm" decoder_name = "ppm"
args = rawmode if decoder_name == "raw" else (rawmode, maxval) args = rawmode if decoder_name == "raw" else (rawmode, maxval)
self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] self.tile = [
ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args)
]
# #
@ -282,7 +284,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break break
return data return data
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
self._comment_spans = False self._comment_spans = False
if self.mode == "1": if self.mode == "1":
data = self._decode_bitonal() data = self._decode_bitonal()
@ -298,7 +300,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
data = bytearray() data = bytearray()
@ -333,7 +335,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":
rawmode, head = "L", b"P5" rawmode, head = "L", b"P5"
elif im.mode == "I": elif im.mode in ("I", "I;16"):
rawmode, head = "I;16B", b"P5" rawmode, head = "I;16B", b"P5"
elif im.mode in ("RGB", "RGBA"): elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6" rawmode, head = "RGB", b"P6"

View File

@ -277,8 +277,8 @@ def _layerinfo(
def _maketile( def _maketile(
file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int
) -> list[ImageFile._Tile] | None: ) -> list[ImageFile._Tile]:
tiles = None tiles = []
read = file.read read = file.read
compression = i16(read(2)) compression = i16(read(2))
@ -291,7 +291,6 @@ def _maketile(
if compression == 0: if compression == 0:
# #
# raw compression # raw compression
tiles = []
for channel in range(channels): for channel in range(channels):
layer = mode[channel] layer = mode[channel]
if mode == "CMYK": if mode == "CMYK":
@ -303,7 +302,6 @@ def _maketile(
# #
# packbits compression # packbits compression
i = 0 i = 0
tiles = []
bytecount = read(channels * ysize * 2) bytecount = read(channels * ysize * 2)
offset = file.tell() offset = file.tell()
for channel in range(channels): for channel in range(channels):

View File

@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile):
self._mode = "RGB" if channels == 3 else "RGBA" self._mode = "RGB" if channels == 3 else "RGBA"
self.fp.seek(1, os.SEEK_CUR) # colorspace self.fp.seek(1, os.SEEK_CUR) # colorspace
self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)]
class QoiDecoder(ImageFile.PyDecoder): class QoiDecoder(ImageFile.PyDecoder):
@ -47,7 +47,7 @@ class QoiDecoder(ImageFile.PyDecoder):
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value self._previously_seen_pixels[hash_value] = value
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}

View File

@ -109,19 +109,28 @@ class SgiImageFile(ImageFile.ImageFile):
pagesize = xsize * ysize * bpc pagesize = xsize * ysize * bpc
if bpc == 2: if bpc == 2:
self.tile = [ self.tile = [
("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation)) ImageFile._Tile(
"SGI16",
(0, 0) + self.size,
headlen,
(self.mode, 0, orientation),
)
] ]
else: else:
self.tile = [] self.tile = []
offset = headlen offset = headlen
for layer in self.mode: for layer in self.mode:
self.tile.append( self.tile.append(
("raw", (0, 0) + self.size, offset, (layer, 0, orientation)) ImageFile._Tile(
"raw", (0, 0) + self.size, offset, (layer, 0, orientation)
)
) )
offset += pagesize offset += pagesize
elif compression == 1: elif compression == 1:
self.tile = [ self.tile = [
("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)) ImageFile._Tile(
"sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)
)
] ]
@ -205,7 +214,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
class SGI16Decoder(ImageFile.PyDecoder): class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
assert self.im is not None assert self.im is not None

View File

@ -154,7 +154,9 @@ class SpiderImageFile(ImageFile.ImageFile):
self.rawmode = "F;32F" self.rawmode = "F;32F"
self._mode = "F" self._mode = "F"
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
]
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
@property @property

View File

@ -124,9 +124,13 @@ class SunImageFile(ImageFile.ImageFile):
# (https://www.fileformat.info/format/sunraster/egff.htm) # (https://www.fileformat.info/format/sunraster/egff.htm)
if file_type in (0, 1, 3, 4, 5): if file_type in (0, 1, 3, 4, 5):
self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride))
]
elif file_type == 2: elif file_type == 2:
self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] self.tile = [
ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode)
]
else: else:
msg = "Unsupported Sun Raster file type" msg = "Unsupported Sun Raster file type"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -137,7 +137,7 @@ class TgaImageFile(ImageFile.ImageFile):
if imagetype & 8: if imagetype & 8:
# compressed # compressed
self.tile = [ self.tile = [
( ImageFile._Tile(
"tga_rle", "tga_rle",
(0, 0) + self.size, (0, 0) + self.size,
self.fp.tell(), self.fp.tell(),
@ -146,7 +146,7 @@ class TgaImageFile(ImageFile.ImageFile):
] ]
else: else:
self.tile = [ self.tile = [
( ImageFile._Tile(
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
self.fp.tell(), self.fp.tell(),

View File

@ -61,12 +61,14 @@ from ._typing import StrOrBytesPath
from ._util import is_path from ._util import is_path
from .TiffTags import TYPES from .TiffTags import TYPES
if TYPE_CHECKING:
from ._typing import Buffer, IntegralLike
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Set these to true to force use of libtiff for reading or writing. # Set these to true to force use of libtiff for reading or writing.
READ_LIBTIFF = False READ_LIBTIFF = False
WRITE_LIBTIFF = False WRITE_LIBTIFF = False
IFD_LEGACY_API = True
STRIP_SIZE = 65536 STRIP_SIZE = 65536
II = b"II" # little-endian (Intel style) II = b"II" # little-endian (Intel style)
@ -291,22 +293,24 @@ def _accept(prefix: bytes) -> bool:
def _limit_rational( def _limit_rational(
val: float | Fraction | IFDRational, max_val: int val: float | Fraction | IFDRational, max_val: int
) -> tuple[float, float]: ) -> tuple[IntegralLike, IntegralLike]:
inv = abs(float(val)) > 1 inv = abs(float(val)) > 1
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d return n_d[::-1] if inv else n_d
def _limit_signed_rational(val, max_val, min_val): def _limit_signed_rational(
val: IFDRational, max_val: int, min_val: int
) -> tuple[IntegralLike, IntegralLike]:
frac = Fraction(val) frac = Fraction(val)
n_d = frac.numerator, frac.denominator n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator
if min(n_d) < min_val: if min(float(i) for i in n_d) < min_val:
n_d = _limit_rational(val, abs(min_val)) n_d = _limit_rational(val, abs(min_val))
if max(n_d) > max_val: n_d_float = tuple(float(i) for i in n_d)
val = Fraction(*n_d) if max(n_d_float) > max_val:
n_d = _limit_rational(val, max_val) n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val)
return n_d return n_d
@ -318,8 +322,10 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op: str): def _delegate(op: str) -> Any:
def delegate(self, *args): def delegate(
self: IFDRational, *args: tuple[float, ...]
) -> bool | float | Fraction:
return getattr(self._val, op)(*args) return getattr(self._val, op)(*args)
return delegate return delegate
@ -358,7 +364,10 @@ class IFDRational(Rational):
self._numerator = value.numerator self._numerator = value.numerator
self._denominator = value.denominator self._denominator = value.denominator
else: else:
self._numerator = value if TYPE_CHECKING:
self._numerator = cast(IntegralLike, value)
else:
self._numerator = value
self._denominator = denominator self._denominator = denominator
if denominator == 0: if denominator == 0:
@ -371,14 +380,14 @@ class IFDRational(Rational):
self._val = Fraction(value / denominator) self._val = Fraction(value / denominator)
@property @property
def numerator(self): def numerator(self) -> IntegralLike:
return self._numerator return self._numerator
@property @property
def denominator(self) -> int: def denominator(self) -> int:
return self._denominator return self._denominator
def limit_rational(self, max_denominator: int) -> tuple[float, int]: def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]:
""" """
:param max_denominator: Integer, the maximum denominator value :param max_denominator: Integer, the maximum denominator value
@ -406,14 +415,18 @@ class IFDRational(Rational):
val = float(val) val = float(val)
return val == other return val == other
def __getstate__(self) -> list[float | Fraction]: def __getstate__(self) -> list[float | Fraction | IntegralLike]:
return [self._val, self._numerator, self._denominator] return [self._val, self._numerator, self._denominator]
def __setstate__(self, state: list[float | Fraction]) -> None: def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None:
IFDRational.__init__(self, 0) IFDRational.__init__(self, 0)
_val, _numerator, _denominator = state _val, _numerator, _denominator = state
assert isinstance(_val, (float, Fraction))
self._val = _val self._val = _val
self._numerator = _numerator if TYPE_CHECKING:
self._numerator = cast(IntegralLike, _numerator)
else:
self._numerator = _numerator
assert isinstance(_denominator, int) assert isinstance(_denominator, int)
self._denominator = _denominator self._denominator = _denominator
@ -471,8 +484,8 @@ def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc
return decorator return decorator
def _register_writer(idx: int): def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func): def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
_write_dispatch[idx] = func # noqa: F821 _write_dispatch[idx] = func # noqa: F821
return func return func
@ -1111,7 +1124,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
return val return val
# undone -- switch this pointer when IFD_LEGACY_API == False # undone -- switch this pointer
ImageFileDirectory = ImageFileDirectory_v1 ImageFileDirectory = ImageFileDirectory_v1
@ -1126,7 +1139,7 @@ class TiffImageFile(ImageFile.ImageFile):
def __init__( def __init__(
self, self,
fp: StrOrBytesPath | IO[bytes] | None = None, fp: StrOrBytesPath | IO[bytes],
filename: str | bytes | None = None, filename: str | bytes | None = None,
) -> None: ) -> None:
self.tag_v2: ImageFileDirectory_v2 self.tag_v2: ImageFileDirectory_v2
@ -1298,7 +1311,7 @@ class TiffImageFile(ImageFile.ImageFile):
# (self._compression, (extents tuple), # (self._compression, (extents tuple),
# 0, (rawmode, self._compression, fp)) # 0, (rawmode, self._compression, fp))
extents = self.tile[0][1] extents = self.tile[0][1]
args = list(self.tile[0][3]) args = self.tile[0][3]
# To be nice on memory footprint, if there's a # To be nice on memory footprint, if there's a
# file descriptor, use that instead of reading # file descriptor, use that instead of reading
@ -1316,11 +1329,12 @@ class TiffImageFile(ImageFile.ImageFile):
fp = False fp = False
if fp: if fp:
args[2] = fp assert isinstance(args, tuple)
args_list = list(args)
args_list[2] = fp
args = tuple(args_list)
decoder = Image._getdecoder( decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig)
self.mode, "libtiff", tuple(args), self.decoderconfig
)
try: try:
decoder.setimage(self.im, extents) decoder.setimage(self.im, extents)
except ValueError as e: except ValueError as e:
@ -1538,7 +1552,7 @@ class TiffImageFile(ImageFile.ImageFile):
# Offset in the tile tuple is 0, we go from 0,0 to # Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds # w,h, and we only do this once -- eds
a = (rawmode, self._compression, False, self.tag_v2.offset) a = (rawmode, self._compression, False, self.tag_v2.offset)
self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a))
elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
# striped image # striped image
@ -1571,7 +1585,7 @@ class TiffImageFile(ImageFile.ImageFile):
args = (tile_rawmode, int(stride), 1) args = (tile_rawmode, int(stride), 1)
self.tile.append( self.tile.append(
( ImageFile._Tile(
self._compression, self._compression,
(x, y, min(x + w, xsize), min(y + h, ysize)), (x, y, min(x + w, xsize), min(y + h, ysize)),
offset, offset,
@ -1632,7 +1646,7 @@ SAVE_INFO = {
} }
def _save(im: Image.Image, fp, filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e: except KeyError as e:
@ -1943,7 +1957,7 @@ def _save(im: Image.Image, fp, filename: str | bytes) -> None:
setattr(im, "_debug_multipage", ifd) setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter: class AppendingTiffWriter(io.BytesIO):
fieldSizes = [ fieldSizes = [
0, # None 0, # None
1, # byte 1, # byte
@ -2053,6 +2067,12 @@ class AppendingTiffWriter:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
"""
:param offset: Distance to seek.
:param whence: Whether the distance is relative to the start,
end or current position.
:returns: The resulting position, relative to the start.
"""
if whence == os.SEEK_SET: if whence == os.SEEK_SET:
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
@ -2086,7 +2106,7 @@ class AppendingTiffWriter:
num_tags = self.readShort() num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data: bytes) -> int | None: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
def readShort(self) -> int: def readShort(self) -> int:
@ -2128,7 +2148,8 @@ class AppendingTiffWriter:
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
self.f.close() if self.close_fp:
self.f.close()
def fixIFD(self) -> None: def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self.readShort()

View File

@ -142,7 +142,7 @@ class WebPImageFile(ImageFile.ImageFile):
if self.fp and self._exclusive_fp: if self.fp and self._exclusive_fp:
self.fp.close() self.fp.close()
self.fp = BytesIO(data) self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)]
return super().load() return super().load()

View File

@ -73,7 +73,11 @@ class XVThumbImageFile(ImageFile.ImageFile):
self.palette = ImagePalette.raw("RGB", PALETTE) self.palette = ImagePalette.raw("RGB", PALETTE)
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))] self.tile = [
ImageFile._Tile(
"raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)
)
]
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile):
self._mode = "1" self._mode = "1"
self._size = xsize, ysize self._size = xsize, ysize
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)]
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -101,7 +101,9 @@ class XpmImageFile(ImageFile.ImageFile):
self._mode = "P" self._mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette)) self.palette = ImagePalette.raw("RGB", b"".join(palette))
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))
]
def load_read(self, read_bytes: int) -> bytes: def load_read(self, read_bytes: int) -> bytes:
# #

View File

@ -6,6 +6,8 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from numbers import _IntegralLike as IntegralLike
try: try:
import numpy.typing as npt import numpy.typing as npt
@ -13,6 +15,11 @@ if TYPE_CHECKING:
except (ImportError, AttributeError): except (ImportError, AttributeError):
pass pass
if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
Buffer = Any
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from typing import TypeGuard from typing import TypeGuard
else: else:
@ -38,4 +45,4 @@ class SupportsRead(Protocol[_T_co]):
StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]

View File

@ -272,7 +272,7 @@ text_layout_raqm(
} }
set_text = raqm_set_text(rq, text, size); set_text = raqm_set_text(rq, text, size);
PyMem_Free(text); PyMem_Free(text);
} else { } else if (PyBytes_Check(string)) {
char *buffer; char *buffer;
PyBytes_AsStringAndSize(string, &buffer, &size); PyBytes_AsStringAndSize(string, &buffer, &size);
if (!buffer || !size) { if (!buffer || !size) {
@ -281,6 +281,9 @@ text_layout_raqm(
goto failed; goto failed;
} }
set_text = raqm_set_text_utf8(rq, buffer, size); set_text = raqm_set_text_utf8(rq, buffer, size);
} else {
PyErr_SetString(PyExc_TypeError, "expected string or bytes");
goto failed;
} }
if (!set_text) { if (!set_text) {
PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
@ -425,8 +428,11 @@ text_layout_fallback(
if (PyUnicode_Check(string)) { if (PyUnicode_Check(string)) {
count = PyUnicode_GET_LENGTH(string); count = PyUnicode_GET_LENGTH(string);
} else { } else if (PyBytes_Check(string)) {
PyBytes_AsStringAndSize(string, &buffer, &count); PyBytes_AsStringAndSize(string, &buffer, &count);
} else {
PyErr_SetString(PyExc_TypeError, "expected string or bytes");
return 0;
} }
if (count == 0) { if (count == 0) {
return 0; return 0;

View File

@ -185,7 +185,7 @@ put_pixel_32(Imaging im, int x, int y, const void *color) {
} }
void void
ImagingAccessInit() { ImagingAccessInit(void) {
#define ADD(mode_, get_pixel_, put_pixel_) \ #define ADD(mode_, get_pixel_, put_pixel_) \
{ \ { \
ImagingAccess access = add_item(mode_); \ ImagingAccess access = add_item(mode_); \

View File

@ -238,6 +238,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n)
int i; int i;
Imaging imTransposed; Imaging imTransposed;
if (imOut->xsize == 0 || imOut->ysize == 0) {
return imOut;
}
if (n < 1) { if (n < 1) {
return ImagingError_ValueError("number of passes must be greater than zero"); return ImagingError_ValueError("number of passes must be greater than zero");
} }

View File

@ -30,26 +30,57 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
bbox[1] = -1; bbox[1] = -1;
bbox[2] = bbox[3] = 0; bbox[2] = bbox[3] = 0;
#define GETBBOX(image, mask) \ #define GETBBOX(image, mask) \
for (y = 0; y < im->ysize; y++) { \ /* first stage: looking for any pixels from top */ \
has_data = 0; \ for (y = 0; y < im->ysize; y++) { \
for (x = 0; x < im->xsize; x++) { \ has_data = 0; \
if (im->image[y][x] & mask) { \ for (x = 0; x < im->xsize; x++) { \
has_data = 1; \ if (im->image[y][x] & mask) { \
if (x < bbox[0]) { \ has_data = 1; \
bbox[0] = x; \ bbox[0] = x; \
} \ bbox[1] = y; \
if (x >= bbox[2]) { \ break; \
bbox[2] = x + 1; \ } \
} \ } \
} \ if (has_data) { \
} \ break; \
if (has_data) { \ } \
if (bbox[1] < 0) { \ } \
bbox[1] = y; \ /* Check that we have a box */ \
} \ if (bbox[1] < 0) { \
bbox[3] = y + 1; \ 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) { \
has_data = 1; \
if (x < bbox[0]) { \
bbox[0] = x; \
} \
bbox[3] = y + 1; \
break; \
} \
} \
if (has_data) { \
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; \
} \
} \
} }
if (im->image8) { if (im->image8) {
@ -71,11 +102,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
GETBBOX(image32, mask); GETBBOX(image32, mask);
} }
/* Check that we got a box */
if (bbox[1] < 0) {
return 0; /* no data */
}
return 1; /* ok */ return 1; /* ok */
} }
@ -144,6 +170,9 @@ ImagingGetExtrema(Imaging im, void *extrema) {
imax = in[x]; imax = in[x];
} }
} }
if (imin == 0 && imax == 255) {
break;
}
} }
((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[0] = (UINT8)imin;
((UINT8 *)extrema)[1] = (UINT8)imax; ((UINT8 *)extrema)[1] = (UINT8)imax;

View File

@ -698,8 +698,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
} }
/* Check that this image is something we can handle */ /* Check that this image is something we can handle */
if (image->numcomps < 1 || image->numcomps > 4 || if (image->numcomps < 1 || image->numcomps > 4) {
image->color_space == OPJ_CLRSPC_UNKNOWN) {
state->errcode = IMAGING_CODEC_BROKEN; state->errcode = IMAGING_CODEC_BROKEN;
state->state = J2K_STATE_FAILED; state->state = J2K_STATE_FAILED;
goto quick_exit; goto quick_exit;
@ -744,7 +743,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
/* Find the correct unpacker */ /* Find the correct unpacker */
color_space = image->color_space; color_space = image->color_space;
if (color_space == OPJ_CLRSPC_UNSPECIFIED) { if (color_space == OPJ_CLRSPC_UNKNOWN || color_space == OPJ_CLRSPC_UNSPECIFIED) {
switch (image->numcomps) { switch (image->numcomps) {
case 1: case 1:
case 2: case 2:

View File

@ -48,7 +48,7 @@ char *libjpeg_turbo_version = NULL;
#endif #endif
int int
ImagingJpegUseJCSExtensions() { ImagingJpegUseJCSExtensions(void) {
int use_jcs_extensions = 0; int use_jcs_extensions = 0;
#ifdef JCS_EXTENSIONS #ifdef JCS_EXTENSIONS
#if defined(LIBJPEG_TURBO_VERSION_NUMBER) #if defined(LIBJPEG_TURBO_VERSION_NUMBER)

View File

@ -36,4 +36,4 @@ deps =
extras = extras =
typing typing
commands = commands =
mypy docs src winbuild Tests {posargs} mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs}

View File

@ -110,9 +110,9 @@ ARCHITECTURES = {
V = { V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.2", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.15", "FRIBIDI": "1.0.15",
"HARFBUZZ": "8.5.0", "HARFBUZZ": "9.0.0",
"JPEGTURBO": "3.0.3", "JPEGTURBO": "3.0.3",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.43", "LIBPNG": "1.6.43",
@ -292,12 +292,8 @@ DEPS: dict[str, dict[str, Any]] = {
}, },
"build": [ "build": [
cmd_rmdir("objs"), cmd_rmdir("objs"),
cmd_msbuild( cmd_msbuild("MSBuild.sln", "Release Static", "Clean"),
r"builds\windows\vc2010\freetype.sln", "Release Static", "Clean" cmd_msbuild("MSBuild.sln", "Release Static", "Build"),
),
cmd_msbuild(
r"builds\windows\vc2010\freetype.sln", "Release Static", "Build"
),
cmd_xcopy("include", "{inc_dir}"), cmd_xcopy("include", "{inc_dir}"),
], ],
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],