Merge branch 'main' into multiband

This commit is contained in:
Andrew Murray 2024-10-14 22:30:08 +11:00 committed by GitHub
commit a9e1dcfed2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 641 additions and 398 deletions

View File

@ -18,7 +18,7 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python312 - PYTHON: C:/Python313
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python39-x64 - PYTHON: C:/Python39-x64

View File

@ -1 +1 @@
cibuildwheel==2.21.2 cibuildwheel==2.21.3

View File

@ -38,16 +38,6 @@ BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
touch openjpeg-stamp
}
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-$BROTLI_VERSION.tar.gz) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)

View File

@ -41,7 +41,7 @@ env:
jobs: jobs:
build-1-QEMU-emulated-wheels: build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' if: github.event_name != 'schedule'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -301,3 +301,5 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3 rev: v0.6.9
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.9 rev: 1.7.10
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8 rev: v19.1.1
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v5.0.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -50,29 +50,30 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.2 rev: 0.29.3
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.9.1 rev: v1.0.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.2.1 rev: 2.2.4
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.19 rev: v0.20.2
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.3.1 rev: 1.4.1
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -5,6 +5,33 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Update licence to MIT-CMU #8460
[hugovk]
- Conditionally define ImageCms type hint to avoid requiring core #8197
[radarhere]
- Support writing LONG8 offsets in AppendingTiffWriter #8417
[radarhere]
- Use ImageFile.MAXBLOCK when saving TIFF images #8461
[radarhere]
- Do not close provided file handles with libtiff when saving #8458
[radarhere]
- Support ImageFilter.BuiltinFilter for I;16* images #8438
[radarhere]
- Use ImagingCore.ptr instead of ImagingCore.id #8341
[homm, radarhere, hugovk]
- Updated EPS mode when opening images without transparency #8281
[Yay295, radarhere]
- Use transparency when combining P frames from APNGs #8443
[radarhere]
- Support all resampling filters when resizing I;16* images #8422 - Support all resampling filters when resizing I;16* images #8422
[radarhere] [radarhere]

View File

@ -17,12 +17,10 @@ coverage:
.PHONY: doc .PHONY: doc
.PHONY: html .PHONY: html
doc html: doc html:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html $(MAKE) -C docs html
.PHONY: htmlview .PHONY: htmlview
htmlview: htmlview:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs htmlview $(MAKE) -C docs htmlview
.PHONY: doccheck .PHONY: doccheck

BIN
Tests/images/eps/1.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -56,17 +56,17 @@ def test_version() -> None:
def test_webp_transparency() -> None: def test_webp_transparency() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == features.check_module("webp") assert (features.check("transp_webp") or False) == features.check_module("webp")
def test_webp_mux() -> None: def test_webp_mux() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("webp_mux") == features.check_module("webp") assert (features.check("webp_mux") or False) == features.check_module("webp")
def test_webp_anim() -> None: def test_webp_anim() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert features.check("webp_anim") == features.check_module("webp") assert (features.check("webp_anim") or False) == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")

View File

@ -258,8 +258,8 @@ def test_apng_mode() -> None:
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (255, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P" assert im.mode == "P"

View File

@ -8,6 +8,7 @@ import pytest
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
from .helper import ( from .helper import (
assert_image_equal_tofile,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
@ -19,18 +20,18 @@ from .helper import (
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
# Our two EPS test files (they are identical except for their bounding boxes) # Our two EPS test files (they are identical except for their bounding boxes)
FILE1 = "Tests/images/zero_bb.eps" FILE1 = "Tests/images/eps/zero_bb.eps"
FILE2 = "Tests/images/non_zero_bb.eps" FILE2 = "Tests/images/eps/non_zero_bb.eps"
# Due to palletization, we'll need to convert these to RGB after load # Due to palletization, we'll need to convert these to RGB after load
FILE1_COMPARE = "Tests/images/zero_bb.png" FILE1_COMPARE = "Tests/images/eps/zero_bb.png"
FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png" FILE1_COMPARE_SCALE2 = "Tests/images/eps/zero_bb_scale2.png"
FILE2_COMPARE = "Tests/images/non_zero_bb.png" FILE2_COMPARE = "Tests/images/eps/non_zero_bb.png"
FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png"
# EPS test files with binary preview # EPS test files with binary preview
FILE3 = "Tests/images/binary_preview_map.eps" FILE3 = "Tests/images/eps/binary_preview_map.eps"
# Three unsigned 32bit little-endian values: # Three unsigned 32bit little-endian values:
# 0xC6D3D0C5 magic number # 0xC6D3D0C5 magic number
@ -126,6 +127,15 @@ def test_binary_header_only() -> None:
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_simple_eps_file(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file))
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_version_comment(prefix: bytes) -> None: def test_missing_version_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
@ -141,23 +151,21 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None:
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment(prefix: bytes) -> None: @pytest.mark.parametrize(
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) "file_lines",
(
simple_eps_file_with_invalid_boundingbox,
simple_eps_file_with_invalid_boundingbox_valid_imagedata,
),
)
def test_invalid_boundingbox_comment(
prefix: bytes, file_lines: tuple[bytes, ...]
) -> None:
data = io.BytesIO(prefix + b"\n".join(file_lines))
with pytest.raises(OSError, match="cannot determine EPS bounding box"): with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_ascii_comment_too_long(prefix: bytes) -> None: def test_ascii_comment_too_long(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
@ -177,7 +185,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img: with Image.open(data) as img:
img.load() img.load()
assert img.mode == "RGB" assert img.mode == "1"
assert img.size == (100, 100) assert img.size == (100, 100)
assert img.format == "EPS" assert img.format == "EPS"
@ -187,7 +195,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
) )
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk() -> None: def test_cmyk() -> None:
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK" assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100) assert cmyk_image.size == (100, 100)
assert cmyk_image.format == "EPS" assert cmyk_image.format == "EPS"
@ -204,8 +212,8 @@ def test_cmyk() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_showpage() -> None: def test_showpage() -> None:
# See https://github.com/python-pillow/Pillow/issues/2615 # See https://github.com/python-pillow/Pillow/issues/2615
with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/reqd_showpage.png") as target: with Image.open("Tests/images/eps/reqd_showpage.png") as target:
# should not crash/hang # should not crash/hang
plot_image.load() plot_image.load()
# fonts could be slightly different # fonts could be slightly different
@ -214,11 +222,11 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None: def test_transparency() -> None:
with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
plot_image.load(transparency=True) plot_image.load(transparency=True)
assert plot_image.mode == "RGBA" assert plot_image.mode == "RGBA"
with Image.open("Tests/images/reqd_showpage_transparency.png") as target: with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target:
# fonts could be slightly different # fonts could be slightly different
assert_image_similar(plot_image, target, 6) assert_image_similar(plot_image, target, 6)
@ -245,9 +253,19 @@ def test_bytesio_object() -> None:
assert_image_similar(img, image1_scale1_compare, 5) assert_image_similar(img, image1_scale1_compare, 5)
def test_1_mode() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
with Image.open("Tests/images/1.eps") as im: @pytest.mark.parametrize(
assert im.mode == "1" # These images have an "ImageData" descriptor.
"filename",
(
"Tests/images/eps/1.eps",
"Tests/images/eps/1_boundingbox_after_imagedata.eps",
"Tests/images/eps/1_second_imagedata.eps",
),
)
def test_1(filename: str) -> None:
with Image.open(filename) as im:
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
def test_image_mode_not_supported(tmp_path: Path) -> None: def test_image_mode_not_supported(tmp_path: Path) -> None:
@ -302,7 +320,9 @@ def test_render_scale2() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) @pytest.mark.parametrize(
"filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps")
)
def test_resize(filename: str) -> None: def test_resize(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
@ -344,10 +364,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"filename", "filename",
( (
"Tests/images/illu10_no_preview.eps", "Tests/images/eps/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps", "Tests/images/eps/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps", "Tests/images/eps/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps", "Tests/images/eps/illuCS6_preview.eps",
), ),
) )
def test_open_eps(filename: str) -> None: def test_open_eps(filename: str) -> None:
@ -359,7 +379,7 @@ def test_open_eps(filename: str) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_emptyline() -> None: def test_emptyline() -> None:
# Test file includes an empty line in the header data # Test file includes an empty line in the header data
emptyline_file = "Tests/images/zero_bb_emptyline.eps" emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps"
with Image.open(emptyline_file) as image: with Image.open(emptyline_file) as image:
image.load() image.load()
@ -371,7 +391,7 @@ def test_emptyline() -> None:
@pytest.mark.timeout(timeout=5) @pytest.mark.timeout(timeout=5)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
) )
def test_timeout(test_file: str) -> None: def test_timeout(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
@ -384,7 +404,7 @@ def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way # Check bounding boxes are parsed in the same way
# when specified in the header and the trailer # when specified in the header and the trailer
with ( with (
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image,
Image.open(FILE1) as header_image, Image.open(FILE1) as header_image,
): ):
assert trailer_image.size == header_image.size assert trailer_image.size == header_image.size
@ -392,12 +412,12 @@ def test_bounding_box_in_trailer() -> None:
def test_eof_before_bounding_box() -> None: def test_eof_before_bounding_box() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"):
pass pass
def test_invalid_data_after_eof() -> None: def test_invalid_data_after_eof() -> None:
with open("Tests/images/illuCS6_preview.eps", "rb") as f: with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f:
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255)) img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
with Image.open(img_bytes) as img: with Image.open(img_bytes) as img:

View File

@ -108,10 +108,6 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif") assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im: with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# The data type of this file's StripOffsets tag is LONG8,
# which is not yet supported for offset data when saving multiple frames.
del im.tag_v2[273]
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
@ -732,6 +728,20 @@ class TestFileTiff:
with Image.open(mp) as reread: with Image.open(mp) as reread:
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
# Neither short nor long
b.seek(0)
with pytest.raises(RuntimeError):
a.fixOffsets(1)
def test_saving_icc_profile(self, tmp_path: Path) -> None: def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set. # Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs # At the time of writing this will only work for non-compressed tiffs

View File

@ -35,16 +35,25 @@ from .helper import assert_image_equal, hopper
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
), ),
) )
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_sanity(
filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str
) -> None:
im = hopper(mode) im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): if mode[0] != "I" or (
callable(filter_to_apply)
and issubclass(filter_to_apply, ImageFilter.BuiltinFilter)
):
out = im.filter(filter_to_apply) out = im.filter(filter_to_apply)
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == im.size assert out.size == im.size
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_sanity_error(mode: str) -> None: def test_sanity_error(mode: str) -> None:
im = hopper(mode) im = hopper(mode)
with pytest.raises(TypeError): with pytest.raises(TypeError):
@ -145,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None:
ImageFilter.Kernel((3, 3), (0, 0)) ImageFilter.Kernel((3, 3), (0, 0))
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_consistency_3x3(mode: str) -> None: def test_consistency_3x3(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference: with Image.open("Tests/images/hopper_emboss.bmp") as reference:
@ -161,7 +172,9 @@ def test_consistency_3x3(mode: str) -> None:
assert_image_equal(source.filter(kernel), reference) assert_image_equal(source.filter(kernel), reference)
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize(
"mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK")
)
def test_consistency_5x5(mode: str) -> None: def test_consistency_5x5(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:

View File

@ -1,11 +1,19 @@
from __future__ import annotations from __future__ import annotations
import pytest
from .helper import hopper from .helper import hopper
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
type_repr = repr(type(im.getim()))
type_repr = repr(type(im.getim()))
assert "PyCapsule" in type_repr assert "PyCapsule" in type_repr
assert isinstance(im.im.id, int)
with pytest.warns(DeprecationWarning):
assert isinstance(im.im.id, int)
with pytest.warns(DeprecationWarning):
ptrs = dict(im.im.unsafe_ptrs)
assert ptrs.keys() == {"image8", "image32", "image"}

View File

@ -324,17 +324,17 @@ def test_set_lut() -> None:
def test_wrong_mode() -> None: def test_wrong_mode() -> None:
lut = ImageMorph.LutBuilder(op_name="corner").build_lut() lut = ImageMorph.LutBuilder(op_name="corner").build_lut()
imrgb = Image.new("RGB", (10, 10)) imrgb_ptr = Image.new("RGB", (10, 10)).getim()
iml = Image.new("L", (10, 10)) iml_ptr = Image.new("L", (10, 10)).getim()
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) _imagingmorph.apply(bytes(lut), imrgb_ptr, iml_ptr)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) _imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
_imagingmorph.match(bytes(lut), imrgb.im.id) _imagingmorph.match(bytes(lut), imrgb_ptr)
# Should not raise # Should not raise
_imagingmorph.match(bytes(lut), iml.im.id) _imagingmorph.match(bytes(lut), iml_ptr)

View File

@ -117,5 +117,5 @@ def test_ipythonviewer() -> None:
else: else:
pytest.fail("IPythonViewer not found") pytest.fail("IPythonViewer not found")
im = hopper() with hopper() as im:
assert test_viewer.show(im) == 1 assert test_viewer.show(im) == 1

View File

@ -56,10 +56,10 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
), ),
("Tests/images/hopper.tif", None), ("Tests/images/hopper.tif", None),
("Tests/images/test-card.png", None), ("Tests/images/test-card.png", None),
("Tests/images/zero_bb.png", None), ("Tests/images/eps/zero_bb.png", None),
("Tests/images/zero_bb_scale2.png", None), ("Tests/images/eps/zero_bb_scale2.png", None),
("Tests/images/non_zero_bb.png", None), ("Tests/images/eps/non_zero_bb.png", None),
("Tests/images/non_zero_bb_scale2.png", None), ("Tests/images/eps/non_zero_bb_scale2.png", None),
("Tests/images/p_trns_single.png", None), ("Tests/images/p_trns_single.png", None),
("Tests/images/pil123p.png", None), ("Tests/images/pil123p.png", None),
("Tests/images/itxt_chunks.png", None), ("Tests/images/itxt_chunks.png", None),

View File

@ -46,7 +46,7 @@ clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
install-sphinx: install-sphinx:
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph $(PYTHON) -m pip install -e ..[docs]
.PHONY: html .PHONY: html
html: html:

View File

@ -22,7 +22,7 @@ import PIL
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "7.3" needs_sphinx = "8.1"
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -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 = [("py:class", "_io.BytesIO")] nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
@ -338,8 +338,6 @@ linkcheck_allowed_redirects = {
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
_repo = "https://github.com/python-pillow/Pillow/" _repo = "https://github.com/python-pillow/Pillow/"
extlinks = { extlinks = {
"cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"), "issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"),
"pypi": ("https://pypi.org/project/%s/", "%s"), "pypi": ("https://pypi.org/project/%s/", "%s"),

View File

@ -165,6 +165,16 @@ Specific WebP Feature Checks
``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
12.0.0 (2025-10-15). 12.0.0 (2025-10-15).
Get internal pointers to objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
Removed features Removed features
---------------- ----------------

View File

@ -53,7 +53,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 | | Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 | x86 | | | 3.13 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 | | | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+

View File

@ -1,19 +1,6 @@
11.0.0 11.0.0
------ ------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes Backwards Incompatible Changes
============================== ==============================
@ -73,6 +60,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
Get internal pointers to objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been
deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
ICNS (width, height, scale) sizes ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -149,7 +146,7 @@ Python 3.13
Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help
others prepare for 3.13, and to ensure Pillow could be used immediately at the release others prepare for 3.13, and to ensure Pillow could be used immediately at the release
of 3.13.0 final (2024-10-01, :pep:`719`). of 3.13.0 final (2024-10-07, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.

View File

@ -14,14 +14,14 @@ readme = "README.md"
keywords = [ keywords = [
"Imaging", "Imaging",
] ]
license = { text = "HPND" } license = { text = "MIT-CMU" }
authors = [ authors = [
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, { name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
] ]
requires-python = ">=3.9" requires-python = ">=3.9"
classifiers = [ classifiers = [
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", "License :: OSI Approved :: CMU License (MIT-CMU)",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@ -43,7 +43,7 @@ dynamic = [
optional-dependencies.docs = [ optional-dependencies.docs = [
"furo", "furo",
"olefile", "olefile",
"sphinx>=7.3", "sphinx>=8.1",
"sphinx-copybutton", "sphinx-copybutton",
"sphinx-inline-tabs", "sphinx-inline-tabs",
"sphinxext-opengraph", "sphinxext-opengraph",

View File

@ -121,7 +121,13 @@ def Ghostscript(
lengthfile -= len(s) lengthfile -= len(s)
f.write(s) f.write(s)
device = "pngalpha" if transparency else "ppmraw" if transparency:
# "RGBA"
device = "pngalpha"
else:
# "pnmraw" automatically chooses between
# PBM ("1"), PGM ("L"), and PPM ("RGB").
device = "pnmraw"
# Build Ghostscript command # Build Ghostscript command
command = [ command = [
@ -151,8 +157,9 @@ def Ghostscript(
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, startupinfo=startupinfo) subprocess.check_call(command, startupinfo=startupinfo)
out_im = Image.open(outfile) with Image.open(outfile) as out_im:
out_im.load() out_im.load()
return out_im.im.copy()
finally: finally:
try: try:
os.unlink(outfile) os.unlink(outfile)
@ -161,10 +168,6 @@ def Ghostscript(
except OSError: except OSError:
pass pass
im = out_im.im.copy()
out_im.close()
return im
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
@ -191,6 +194,11 @@ class EpsImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
# When reading header comments, the first comment is used.
# When reading trailer comments, the last comment is used.
bounding_box: list[int] | None = None
imagedata_size: tuple[int, int] | None = None
byte_arr = bytearray(255) byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr) bytes_mv = memoryview(byte_arr)
bytes_read = 0 bytes_read = 0
@ -211,8 +219,8 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s: str) -> bool: def read_comment(s: str) -> bool:
nonlocal reading_trailer_comments nonlocal bounding_box, reading_trailer_comments
try: try:
m = split.match(s) m = split.match(s)
except re.error as e: except re.error as e:
@ -227,18 +235,12 @@ class EpsImageFile(ImageFile.ImageFile):
if k == "BoundingBox": if k == "BoundingBox":
if v == "(atend)": if v == "(atend)":
reading_trailer_comments = True reading_trailer_comments = True
elif not self.tile or (trailer_reached and reading_trailer_comments): elif not bounding_box or (trailer_reached and reading_trailer_comments):
try: try:
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] bounding_box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
ImageFile._Tile(
"eps", (0, 0) + self.size, offset, (length, box)
)
]
except Exception: except Exception:
pass pass
return True return True
@ -289,7 +291,7 @@ class EpsImageFile(ImageFile.ImageFile):
continue continue
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
if not _read_comment(s): if not read_comment(s):
m = field.match(s) m = field.match(s)
if m: if m:
k = m.group(1) k = m.group(1)
@ -308,6 +310,12 @@ class EpsImageFile(ImageFile.ImageFile):
# Check for an "ImageData" descriptor # Check for an "ImageData" descriptor
# https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096
# If we've already read an "ImageData" descriptor,
# don't read another one.
if imagedata_size:
bytes_read = 0
continue
# Values: # Values:
# columns # columns
# rows # rows
@ -333,22 +341,35 @@ class EpsImageFile(ImageFile.ImageFile):
else: else:
break break
self._size = columns, rows # Parse the columns and rows after checking the bit depth and mode
return # in case the bit depth and/or mode are invalid.
imagedata_size = columns, rows
elif bytes_mv[:5] == b"%%EOF": elif bytes_mv[:5] == b"%%EOF":
break break
elif trailer_reached and reading_trailer_comments: elif trailer_reached and reading_trailer_comments:
# Load EPS trailer # Load EPS trailer
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
_read_comment(s) read_comment(s)
elif bytes_mv[:9] == b"%%Trailer": elif bytes_mv[:9] == b"%%Trailer":
trailer_reached = True trailer_reached = True
bytes_read = 0 bytes_read = 0
if not self.tile: # A "BoundingBox" is always required,
# even if an "ImageData" descriptor size exists.
if not bounding_box:
msg = "cannot determine EPS bounding box" msg = "cannot determine EPS bounding box"
raise OSError(msg) raise OSError(msg)
# An "ImageData" size takes precedence over the "BoundingBox".
self._size = imagedata_size or (
bounding_box[2] - bounding_box[0],
bounding_box[3] - bounding_box[1],
)
self.tile = [
ImageFile._Tile("eps", (0, 0) + self.size, offset, (length, bounding_box))
]
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
s = fp.read(4) s = fp.read(4)

View File

@ -225,12 +225,7 @@ if TYPE_CHECKING:
from IPython.lib.pretty import PrettyPrinter from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import CapsuleType, 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,

View File

@ -31,6 +31,10 @@ from ._typing import SupportsRead
try: try:
from . import _imagingcms as core from . import _imagingcms as core
_CmsProfileCompatible = Union[
str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile"
]
except ImportError as ex: except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing # Allow error import for doc purposes, but error out when accessing
# anything in core. # anything in core.
@ -349,19 +353,17 @@ class ImageCmsTransform(Image.ImagePointHandler):
return self.apply(im) return self.apply(im)
def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
im.load()
if imOut is None: if imOut is None:
imOut = Image.new(self.output_mode, im.size, None) imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.im.id, imOut.im.id) self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes() imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut return imOut
def apply_in_place(self, im: Image.Image) -> Image.Image: def apply_in_place(self, im: Image.Image) -> Image.Image:
im.load()
if im.mode != self.output_mode: if im.mode != self.output_mode:
msg = "mode mismatch" msg = "mode mismatch"
raise ValueError(msg) # wrong output mode raise ValueError(msg) # wrong output mode
self.transform.apply(im.im.id, im.im.id) self.transform.apply(im.getim(), im.getim())
im.info["icc_profile"] = self.output_profile.tobytes() im.info["icc_profile"] = self.output_profile.tobytes()
return im return im
@ -391,10 +393,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile |
# pyCMS compatible layer # pyCMS compatible layer
# --------------------------------------------------------------------. # --------------------------------------------------------------------.
_CmsProfileCompatible = Union[
str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile
]
class PyCMSError(Exception): class PyCMSError(Exception):
"""(pyCMS) Exception class. """(pyCMS) Exception class.

View File

@ -59,13 +59,12 @@ class _Operand:
if im2 is None: if im2 is None:
# unary operation # unary operation
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
try: try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}") op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
_imagingmath.unop(op, out.im.id, im_1.im.id) _imagingmath.unop(op, out.getim(), im_1.getim())
else: else:
# binary operation # binary operation
im_2 = self.__fixup(im2) im_2 = self.__fixup(im2)
@ -86,14 +85,12 @@ class _Operand:
if im_2.size != size: if im_2.size != size:
im_2 = im_2.crop((0, 0) + size) im_2 = im_2.crop((0, 0) + size)
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
im_2.load()
try: try:
op = getattr(_imagingmath, f"{op}_{im_1.mode}") op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
_imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim())
return _Operand(out) return _Operand(out)
# unary operators # unary operators
@ -176,10 +173,10 @@ class _Operand:
return self.apply("rshift", self, other) return self.apply("rshift", self, other)
# logical # logical
def __eq__(self, other): def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
return self.apply("eq", self, other) return self.apply("eq", self, other)
def __ne__(self, other): def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
return self.apply("ne", self, other) return self.apply("ne", self, other)
def __lt__(self, other: _Operand | float) -> _Operand: def __lt__(self, other: _Operand | float) -> _Operand:

View File

@ -213,7 +213,7 @@ class MorphOp:
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
outimage = Image.new(image.mode, image.size, None) outimage = Image.new(image.mode, image.size, None)
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
return count, outimage return count, outimage
def match(self, image: Image.Image) -> list[tuple[int, int]]: def match(self, image: Image.Image) -> list[tuple[int, int]]:
@ -229,7 +229,7 @@ class MorphOp:
if image.mode != "L": if image.mode != "L":
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id) return _imagingmorph.match(bytes(self.lut), image.getim())
def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]: def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
"""Get a list of all turned on pixels in a binary image """Get a list of all turned on pixels in a binary image
@ -240,7 +240,7 @@ class MorphOp:
if image.mode != "L": if image.mode != "L":
msg = "Image mode must be L" msg = "Image mode must be L"
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id) return _imagingmorph.get_on_pixels(image.getim())
def load_lut(self, filename: str) -> None: def load_lut(self, filename: str) -> None:
"""Load an operator from an mrl file""" """Load an operator from an mrl file"""

View File

@ -32,23 +32,12 @@ from typing import TYPE_CHECKING, Any, cast
from . import Image, ImageFile from . import Image, ImageFile
if TYPE_CHECKING:
from ._typing import CapsuleType
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Check for Tkinter interface hooks # Check for Tkinter interface hooks
_pilbitmap_ok = None
def _pilbitmap_check() -> int:
global _pilbitmap_ok
if _pilbitmap_ok is None:
try:
im = Image.new("1", (1, 1))
tkinter.BitmapImage(data=f"PIL:{im.im.id}")
_pilbitmap_ok = 1
except tkinter.TclError:
_pilbitmap_ok = 0
return _pilbitmap_ok
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None source = None
@ -62,18 +51,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
def _pyimagingtkcall( def _pyimagingtkcall(
command: str, photo: PhotoImage | tkinter.PhotoImage, id: int command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
) -> None: ) -> None:
tk = photo.tk tk = photo.tk
try: try:
tk.call(command, photo, id) tk.call(command, photo, repr(ptr))
except tkinter.TclError: except tkinter.TclError:
# activate Tkinter hook # activate Tkinter hook
# may raise an error if it cannot attach to Tkinter # may raise an error if it cannot attach to Tkinter
from . import _imagingtk from . import _imagingtk
_imagingtk.tkinit(tk.interpaddr()) _imagingtk.tkinit(tk.interpaddr())
tk.call(command, photo, id) tk.call(command, photo, repr(ptr))
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -142,7 +131,10 @@ class PhotoImage:
self.paste(image) self.paste(image)
def __del__(self) -> None: def __del__(self) -> None:
name = self.__photo.name try:
name = self.__photo.name
except AttributeError:
return
self.__photo.name = None self.__photo.name = None
try: try:
self.__photo.tk.call("image", "delete", name) self.__photo.tk.call("image", "delete", name)
@ -185,15 +177,14 @@ class PhotoImage:
the bitmap image. the bitmap image.
""" """
# convert to blittable # convert to blittable
im.load() ptr = im.getim()
image = im.im image = im.im
if image.isblock() and im.mode == self.__mode: if not image.isblock() or im.mode != self.__mode:
block = image block = Image.core.new_block(self.__mode, im.size)
else:
block = image.new_block(self.__mode, im.size)
image.convert2(block, image) # convert directly between buffers image.convert2(block, image) # convert directly between buffers
ptr = block.ptr
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -225,18 +216,13 @@ class BitmapImage:
self.__mode = image.mode self.__mode = image.mode
self.__size = image.size self.__size = image.size
if _pilbitmap_check(): self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
# fast way (requires the pilbitmap booster patch)
image.load()
kw["data"] = f"PIL:{image.im.id}"
self.__im = image # must keep a reference
else:
# slow but safe way
kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw)
def __del__(self) -> None: def __del__(self) -> None:
name = self.__photo.name try:
name = self.__photo.name
except AttributeError:
return
self.__photo.name = None self.__photo.name = None
try: try:
self.__photo.tk.call("image", "delete", name) self.__photo.tk.call("image", "delete", name)
@ -273,9 +259,8 @@ class BitmapImage:
def getimage(photo: PhotoImage) -> Image.Image: def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory.""" """Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id) _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
return im return im

View File

@ -1063,6 +1063,12 @@ class PngImageFile(ImageFile.ImageFile):
"RGBA", self.info["transparency"] "RGBA", self.info["transparency"]
) )
else: else:
if self.im.mode == "P" and "transparency" in self.info:
t = self.info["transparency"]
if isinstance(t, bytes):
updated.putpalettealphas(t)
elif isinstance(t, int):
updated.putpalettealpha(t)
mask = updated.convert("RGBA") mask = updated.convert("RGBA")
self._prev_im.paste(updated, self.dispose_extent, mask) self._prev_im.paste(updated, self.dispose_extent, mask)
self.im = self._prev_im self.im = self._prev_im

View File

@ -1239,11 +1239,11 @@ class TiffImageFile(ImageFile.ImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self._seek(frame) self._seek(frame)
# Create a new core image object on second and if self._im is not None and (
# subsequent frames in the image. Image may be self.im.size != self._tile_size or self.im.mode != self.mode
# different size/mode. ):
Image._decompression_bomb_check(self._tile_size) # The core image will no longer be used
self.im = Image.core.new(self.mode, self._tile_size) self._im = None
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
@ -1325,6 +1325,7 @@ class TiffImageFile(ImageFile.ImageFile):
def load_prepare(self) -> None: def load_prepare(self) -> None:
if self._im is None: if self._im is None:
Image._decompression_bomb_check(self._tile_size)
self.im = Image.core.new(self.mode, self._tile_size) self.im = Image.core.new(self.mode, self._tile_size)
ImageFile.ImageFile.load_prepare(self) ImageFile.ImageFile.load_prepare(self)
@ -1905,7 +1906,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if hasattr(fp, "fileno"): if hasattr(fp, "fileno"):
try: try:
fp.seek(0) fp.seek(0)
_fp = os.dup(fp.fileno()) _fp = fp.fileno()
except io.UnsupportedOperation: except io.UnsupportedOperation:
pass pass
@ -1978,17 +1979,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
encoder.setimage(im.im, (0, 0) + im.size) encoder.setimage(im.im, (0, 0) + im.size)
while True: while True:
# undone, change to self.decodermaxblock: errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:]
errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp: if not _fp:
fp.write(data) fp.write(data)
if errcode: if errcode:
break break
if _fp:
try:
os.close(_fp)
except OSError:
pass
if errcode < 0: if errcode < 0:
msg = f"encoder error {errcode} when writing image file" msg = f"encoder error {errcode} when writing image file"
raise OSError(msg) raise OSError(msg)
@ -2162,13 +2157,24 @@ class AppendingTiffWriter(io.BytesIO):
def write(self, data: Buffer, /) -> int: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
def readShort(self) -> int: def _fmt(self, field_size: int) -> str:
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) try:
return {2: "H", 4: "L", 8: "Q"}[field_size]
except KeyError:
msg = "offset is not supported"
raise RuntimeError(msg)
def _read(self, field_size: int) -> int:
(value,) = struct.unpack(
self.endian + self._fmt(field_size), self.f.read(field_size)
)
return value return value
def readShort(self) -> int:
return self._read(2)
def readLong(self) -> int: def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) return self._read(4)
return value
@staticmethod @staticmethod
def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
@ -2181,15 +2187,18 @@ class AppendingTiffWriter(io.BytesIO):
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4) self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
self.f.seek(-field_size, os.SEEK_CUR)
bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value)
)
self._verify_bytes_written(bytes_written, field_size)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) return self._rewriteLast(value, 2)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
self._verify_bytes_written(bytes_written, 2)
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) return self._rewriteLast(value, 4)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
@ -2221,32 +2230,22 @@ class AppendingTiffWriter(io.BytesIO):
cur_pos = self.f.tell() cur_pos = self.f.tell()
if is_local: if is_local:
self.fixOffsets( self._fixOffsets(count, field_size)
count, isShort=(field_size == 2), isLong=(field_size == 4)
)
self.f.seek(cur_pos + 4) self.f.seek(cur_pos + 4)
else: else:
self.f.seek(offset) self.f.seek(offset)
self.fixOffsets( self._fixOffsets(count, field_size)
count, isShort=(field_size == 2), isLong=(field_size == 4)
)
self.f.seek(cur_pos) self.f.seek(cur_pos)
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)
def fixOffsets( def _fixOffsets(self, count: int, field_size: int) -> None:
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if not isShort and not isLong:
msg = "offset is neither short nor long"
raise RuntimeError(msg)
for i in range(count): for i in range(count):
offset = self.readShort() if isShort else self.readLong() offset = self._read(field_size)
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
if isShort and offset >= 65536: if field_size == 2 and offset >= 65536:
# offset is now too large - we must convert shorts to longs # offset is now too large - we must convert shorts to longs
if count != 1: if count != 1:
msg = "not implemented" msg = "not implemented"
@ -2258,10 +2257,19 @@ class AppendingTiffWriter(io.BytesIO):
self.f.seek(-10, os.SEEK_CUR) self.f.seek(-10, os.SEEK_CUR)
self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.writeShort(TiffTags.LONG) # rewrite the type to LONG
self.f.seek(8, os.SEEK_CUR) self.f.seek(8, os.SEEK_CUR)
elif isShort:
self.rewriteLastShort(offset)
else: else:
self.rewriteLastLong(offset) self._rewriteLast(offset, field_size)
def fixOffsets(
self, count: int, isShort: bool = False, isLong: bool = False
) -> None:
if isShort:
field_size = 2
elif isLong:
field_size = 4
else:
field_size = 0
return self._fixOffsets(count, field_size)
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -2,6 +2,8 @@ import datetime
import sys import sys
from typing import Literal, SupportsFloat, TypedDict from typing import Literal, SupportsFloat, TypedDict
from ._typing import CapsuleType
littlecms_version: str | None littlecms_version: str | None
_Tuple3f = tuple[float, float, float] _Tuple3f = tuple[float, float, float]
@ -108,7 +110,7 @@ class CmsProfile:
def is_intent_supported(self, intent: int, direction: int, /) -> int: ... def is_intent_supported(self, intent: int, direction: int, /) -> int: ...
class CmsTransform: class CmsTransform:
def apply(self, id_in: int, id_out: int) -> int: ... def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ...
def profile_open(profile: str, /) -> CmsProfile: ... def profile_open(profile: str, /) -> CmsProfile: ...
def profile_frombytes(profile: bytes, /) -> CmsProfile: ... def profile_frombytes(profile: bytes, /) -> CmsProfile: ...

View File

@ -15,6 +15,11 @@ if TYPE_CHECKING:
except (ImportError, AttributeError): except (ImportError, AttributeError):
pass pass
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
if sys.version_info >= (3, 12): if sys.version_info >= (3, 12):
from collections.abc import Buffer from collections.abc import Buffer
else: else:

View File

@ -146,10 +146,11 @@ def check_feature(feature: str) -> bool | None:
module, flag, ver = features[feature] module, flag, ver = features[feature]
if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
try: try:
imported_module = __import__(module, fromlist=["PIL"]) imported_module = __import__(module, fromlist=["PIL"])
if isinstance(flag, bool): if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
return flag return flag
return getattr(imported_module, flag) return getattr(imported_module, flag)
except ModuleNotFoundError: except ModuleNotFoundError:

View File

@ -56,19 +56,34 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
static Imaging static Imaging
ImagingFind(const char *name) { ImagingFind(const char *name) {
Py_ssize_t id; PyObject *capsule;
int direct_pointer = 0;
const char *expected = "capsule object \"" IMAGING_MAGIC "\" at 0x";
/* FIXME: use CObject instead? */ if (name[0] == '<') {
#if defined(_WIN64) name++;
id = _atoi64(name); } else {
#else // Special case for PyPy, where the string representation of a Capsule
id = atol(name); // refers directly to the pointer itself, not to the PyCapsule object.
#endif direct_pointer = 1;
if (!id) { }
if (strncmp(name, expected, strlen(expected))) {
return NULL; return NULL;
} }
return (Imaging)id; capsule = (PyObject *)strtoull(name + strlen(expected), NULL, 16);
if (direct_pointer) {
return (Imaging)capsule;
}
if (!PyCapsule_IsValid(capsule, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
return (Imaging)PyCapsule_GetPointer(capsule, IMAGING_MAGIC);
} }
static int static int

View File

@ -3717,15 +3717,12 @@ static struct PyMethodDef methods[] = {
/* Unsharpmask extension */ /* Unsharpmask extension */
{"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS},
{"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS},
{"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, {"box_blur", (PyCFunction)_box_blur, METH_VARARGS},
/* Special effects */ /* Special effects */
{"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS},
/* Misc. */ /* Misc. */
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
@ -3750,16 +3747,40 @@ _getattr_bands(ImagingObject *self, void *closure) {
static PyObject * static PyObject *
_getattr_id(ImagingObject *self, void *closure) { _getattr_id(ImagingObject *self, void *closure) {
if (PyErr_WarnEx(
PyExc_DeprecationWarning,
"id property is deprecated and will be removed in Pillow 12 (2025-10-15)",
1
) < 0) {
return NULL;
}
return PyLong_FromSsize_t((Py_ssize_t)self->image); return PyLong_FromSsize_t((Py_ssize_t)self->image);
} }
static void
_ptr_destructor(PyObject *capsule) {
PyObject *self = (PyObject *)PyCapsule_GetContext(capsule);
Py_DECREF(self);
}
static PyObject * static PyObject *
_getattr_ptr(ImagingObject *self, void *closure) { _getattr_ptr(ImagingObject *self, void *closure) {
return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor);
Py_INCREF(self);
PyCapsule_SetContext(capsule, self);
return capsule;
} }
static PyObject * static PyObject *
_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
if (PyErr_WarnEx(
PyExc_DeprecationWarning,
"unsafe_ptrs property is deprecated and will be removed in Pillow 12 "
"(2025-10-15)",
1
) < 0) {
return NULL;
}
return Py_BuildValue( return Py_BuildValue(
"(sn)(sn)(sn)", "(sn)(sn)(sn)",
"image8", "image8",
@ -4241,6 +4262,7 @@ static PyMethodDef functions[] = {
{"blend", (PyCFunction)_blend, METH_VARARGS}, {"blend", (PyCFunction)_blend, METH_VARARGS},
{"fill", (PyCFunction)_fill, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS},
{"new", (PyCFunction)_new, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS},
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"merge", (PyCFunction)_merge, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS},
/* Functions */ /* Functions */

View File

@ -531,23 +531,24 @@ buildProofTransform(PyObject *self, PyObject *args) {
static PyObject * static PyObject *
cms_transform_apply(CmsTransformObject *self, PyObject *args) { cms_transform_apply(CmsTransformObject *self, PyObject *args) {
Py_ssize_t idIn; PyObject *i0, *i1;
Py_ssize_t idOut;
Imaging im; Imaging im;
Imaging imOut; Imaging imOut;
int result; if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) {
if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) {
return NULL; return NULL;
} }
im = (Imaging)idIn; if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) ||
imOut = (Imaging)idOut; !PyCapsule_IsValid(i1, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
result = pyCMSdoTransform(im, imOut, self->transform); im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
return Py_BuildValue("i", result); return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform));
} }
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */

View File

@ -830,7 +830,6 @@ font_render(FontObject *self, PyObject *args) {
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
PyObject *image; PyObject *image;
Imaging im; Imaging im;
Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */
float stroke_width = 0; float stroke_width = 0;
@ -922,17 +921,13 @@ font_render(FontObject *self, PyObject *args) {
width += ceil(stroke_width * 2 + x_start); width += ceil(stroke_width * 2 + x_start);
height += ceil(stroke_width * 2 + y_start); height += ceil(stroke_width * 2 + y_start);
image = PyObject_CallFunction(fill, "ii", width, height); image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) { if (image == NULL) {
PyMem_Del(glyph_info);
return Py_BuildValue("N(ii)", image, 0, 0);
} else if (image == NULL) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return NULL; return NULL;
} }
PyObject *imageId = PyObject_GetAttrString(image, "id"); PyObject *imagePtr = PyObject_GetAttrString(image, "ptr");
id = PyLong_AsSsize_t(imageId); im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC);
Py_XDECREF(imageId); Py_XDECREF(imagePtr);
im = (Imaging)id;
x_offset = round(x_offset - stroke_width); x_offset = round(x_offset - stroke_width);
y_offset = round(y_offset - stroke_width); y_offset = round(y_offset - stroke_width);

View File

@ -23,6 +23,9 @@
#define MAX_INT32 2147483647.0 #define MAX_INT32 2147483647.0
#define MIN_INT32 -2147483648.0 #define MIN_INT32 -2147483648.0
#define MATH_FUNC_UNOP_MAGIC "Pillow Math unary func"
#define MATH_FUNC_BINOP_MAGIC "Pillow Math binary func"
#define UNOP(name, op, type) \ #define UNOP(name, op, type) \
void name(Imaging out, Imaging im1) { \ void name(Imaging out, Imaging im1) { \
int x, y; \ int x, y; \
@ -168,15 +171,24 @@ _unop(PyObject *self, PyObject *args) {
Imaging im1; Imaging im1;
void (*unop)(Imaging, Imaging); void (*unop)(Imaging, Imaging);
Py_ssize_t op, i0, i1; PyObject *op, *i0, *i1;
if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) {
return NULL; return NULL;
} }
out = (Imaging)i0; if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) {
im1 = (Imaging)i1; PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_UNOP_MAGIC);
return NULL;
}
if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) ||
!PyCapsule_IsValid(i1, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
unop = (void *)op; unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC);
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
unop(out, im1); unop(out, im1);
@ -191,16 +203,26 @@ _binop(PyObject *self, PyObject *args) {
Imaging im2; Imaging im2;
void (*binop)(Imaging, Imaging, Imaging); void (*binop)(Imaging, Imaging, Imaging);
Py_ssize_t op, i0, i1, i2; PyObject *op, *i0, *i1, *i2;
if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) {
return NULL; return NULL;
} }
out = (Imaging)i0; if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) {
im1 = (Imaging)i1; PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC);
im2 = (Imaging)i2; return NULL;
}
if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) ||
!PyCapsule_IsValid(i1, IMAGING_MAGIC) ||
!PyCapsule_IsValid(i2, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
binop = (void *)op; binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC);
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC);
binop(out, im1, im2); binop(out, im1, im2);
@ -213,8 +235,17 @@ static PyMethodDef _functions[] = {
}; };
static void static void
install(PyObject *d, char *name, void *value) { install_unary(PyObject *d, char *name, void *func) {
PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); PyObject *v = PyCapsule_New(func, MATH_FUNC_UNOP_MAGIC, NULL);
if (!v || PyDict_SetItemString(d, name, v)) {
PyErr_Clear();
}
Py_XDECREF(v);
}
static void
install_binary(PyObject *d, char *name, void *func) {
PyObject *v = PyCapsule_New(func, MATH_FUNC_BINOP_MAGIC, NULL);
if (!v || PyDict_SetItemString(d, name, v)) { if (!v || PyDict_SetItemString(d, name, v)) {
PyErr_Clear(); PyErr_Clear();
} }
@ -225,50 +256,50 @@ static int
setup_module(PyObject *m) { setup_module(PyObject *m) {
PyObject *d = PyModule_GetDict(m); PyObject *d = PyModule_GetDict(m);
install(d, "abs_I", abs_I); install_unary(d, "abs_I", abs_I);
install(d, "neg_I", neg_I); install_unary(d, "neg_I", neg_I);
install(d, "add_I", add_I); install_binary(d, "add_I", add_I);
install(d, "sub_I", sub_I); install_binary(d, "sub_I", sub_I);
install(d, "diff_I", diff_I); install_binary(d, "diff_I", diff_I);
install(d, "mul_I", mul_I); install_binary(d, "mul_I", mul_I);
install(d, "div_I", div_I); install_binary(d, "div_I", div_I);
install(d, "mod_I", mod_I); install_binary(d, "mod_I", mod_I);
install(d, "min_I", min_I); install_binary(d, "min_I", min_I);
install(d, "max_I", max_I); install_binary(d, "max_I", max_I);
install(d, "pow_I", pow_I); install_binary(d, "pow_I", pow_I);
install(d, "invert_I", invert_I); install_unary(d, "invert_I", invert_I);
install(d, "and_I", and_I); install_binary(d, "and_I", and_I);
install(d, "or_I", or_I); install_binary(d, "or_I", or_I);
install(d, "xor_I", xor_I); install_binary(d, "xor_I", xor_I);
install(d, "lshift_I", lshift_I); install_binary(d, "lshift_I", lshift_I);
install(d, "rshift_I", rshift_I); install_binary(d, "rshift_I", rshift_I);
install(d, "eq_I", eq_I); install_binary(d, "eq_I", eq_I);
install(d, "ne_I", ne_I); install_binary(d, "ne_I", ne_I);
install(d, "lt_I", lt_I); install_binary(d, "lt_I", lt_I);
install(d, "le_I", le_I); install_binary(d, "le_I", le_I);
install(d, "gt_I", gt_I); install_binary(d, "gt_I", gt_I);
install(d, "ge_I", ge_I); install_binary(d, "ge_I", ge_I);
install(d, "abs_F", abs_F); install_unary(d, "abs_F", abs_F);
install(d, "neg_F", neg_F); install_unary(d, "neg_F", neg_F);
install(d, "add_F", add_F); install_binary(d, "add_F", add_F);
install(d, "sub_F", sub_F); install_binary(d, "sub_F", sub_F);
install(d, "diff_F", diff_F); install_binary(d, "diff_F", diff_F);
install(d, "mul_F", mul_F); install_binary(d, "mul_F", mul_F);
install(d, "div_F", div_F); install_binary(d, "div_F", div_F);
install(d, "mod_F", mod_F); install_binary(d, "mod_F", mod_F);
install(d, "min_F", min_F); install_binary(d, "min_F", min_F);
install(d, "max_F", max_F); install_binary(d, "max_F", max_F);
install(d, "pow_F", pow_F); install_binary(d, "pow_F", pow_F);
install(d, "eq_F", eq_F); install_binary(d, "eq_F", eq_F);
install(d, "ne_F", ne_F); install_binary(d, "ne_F", ne_F);
install(d, "lt_F", lt_F); install_binary(d, "lt_F", lt_F);
install(d, "le_F", le_F); install_binary(d, "le_F", le_F);
install(d, "gt_F", gt_F); install_binary(d, "gt_F", gt_F);
install(d, "ge_F", ge_F); install_binary(d, "ge_F", ge_F);
return 0; return 0;
} }

View File

@ -11,7 +11,6 @@
* See the README file for information on usage and redistribution. * See the README file for information on usage and redistribution.
*/ */
#include "Python.h"
#include "libImaging/Imaging.h" #include "libImaging/Imaging.h"
#define LUT_SIZE (1 << 9) #define LUT_SIZE (1 << 9)
@ -30,43 +29,36 @@
static PyObject * static PyObject *
apply(PyObject *self, PyObject *args) { apply(PyObject *self, PyObject *args) {
const char *lut; const char *lut;
PyObject *py_lut; Py_ssize_t lut_len;
Py_ssize_t lut_len, i0, i1; PyObject *i0, *i1;
Imaging imgin, imgout; Imaging imgin, imgout;
int width, height; int width, height;
int row_idx, col_idx; int row_idx, col_idx;
UINT8 **inrows, **outrows; UINT8 **inrows, **outrows;
int num_changed_pixels = 0; int num_changed_pixels = 0;
if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) {
PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem");
return NULL; return NULL;
} }
if (!PyBytes_Check(py_lut)) {
PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object");
return NULL;
}
lut_len = PyBytes_Size(py_lut);
if (lut_len < LUT_SIZE) { if (lut_len < LUT_SIZE) {
PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size");
return NULL; return NULL;
} }
lut = PyBytes_AsString(py_lut); if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) ||
!PyCapsule_IsValid(i1, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
imgin = (Imaging)i0; imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
imgout = (Imaging)i1; imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
width = imgin->xsize; width = imgin->xsize;
height = imgin->ysize; height = imgin->ysize;
if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1 ||
PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) {
return NULL;
}
if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) {
PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type");
return NULL; return NULL;
} }
@ -129,46 +121,39 @@ apply(PyObject *self, PyObject *args) {
static PyObject * static PyObject *
match(PyObject *self, PyObject *args) { match(PyObject *self, PyObject *args) {
const char *lut; const char *lut;
PyObject *py_lut; Py_ssize_t lut_len;
Py_ssize_t lut_len, i0; PyObject *i0;
Imaging imgin; Imaging imgin;
int width, height; int width, height;
int row_idx, col_idx; int row_idx, col_idx;
UINT8 **inrows; UINT8 **inrows;
PyObject *ret = PyList_New(0);
if (ret == NULL) { if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) {
return NULL; return NULL;
} }
if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) {
Py_DECREF(ret);
PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem");
return NULL;
}
if (!PyBytes_Check(py_lut)) {
Py_DECREF(ret);
PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object");
return NULL;
}
lut_len = PyBytes_Size(py_lut);
if (lut_len < LUT_SIZE) { if (lut_len < LUT_SIZE) {
Py_DECREF(ret);
PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size");
return NULL; return NULL;
} }
lut = PyBytes_AsString(py_lut); if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) {
imgin = (Imaging)i0; PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) {
Py_DECREF(ret);
PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type");
return NULL; return NULL;
} }
PyObject *ret = PyList_New(0);
if (ret == NULL) {
return NULL;
}
inrows = imgin->image8; inrows = imgin->image8;
width = imgin->xsize; width = imgin->xsize;
height = imgin->ysize; height = imgin->ysize;
@ -215,26 +200,31 @@ match(PyObject *self, PyObject *args) {
*/ */
static PyObject * static PyObject *
get_on_pixels(PyObject *self, PyObject *args) { get_on_pixels(PyObject *self, PyObject *args) {
Py_ssize_t i0; PyObject *i0;
Imaging img; Imaging img;
UINT8 **rows; UINT8 **rows;
int row_idx, col_idx; int row_idx, col_idx;
int width, height; int width, height;
if (!PyArg_ParseTuple(args, "O", &i0)) {
return NULL;
}
if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) {
PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", IMAGING_MAGIC);
return NULL;
}
img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
rows = img->image8;
width = img->xsize;
height = img->ysize;
PyObject *ret = PyList_New(0); PyObject *ret = PyList_New(0);
if (ret == NULL) { if (ret == NULL) {
return NULL; return NULL;
} }
if (!PyArg_ParseTuple(args, "n", &i0)) {
Py_DECREF(ret);
PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem");
return NULL;
}
img = (Imaging)i0;
rows = img->image8;
width = img->xsize;
height = img->ysize;
for (row_idx = 0; row_idx < height; row_idx++) { for (row_idx = 0; row_idx < height; row_idx++) {
UINT8 *row = rows[row_idx]; UINT8 *row = rows[row_idx];
for (col_idx = 0; col_idx < width; col_idx++) { for (col_idx = 0; col_idx < width; col_idx++) {

View File

@ -26,6 +26,8 @@
#include "Imaging.h" #include "Imaging.h"
#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F))
static inline UINT8 static inline UINT8
clip8(float in) { clip8(float in) {
if (in <= 0.0) { if (in <= 0.0) {
@ -106,6 +108,22 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
return imOut; return imOut;
} }
float
kernel_i16(int size, UINT8 *in0, int x, const float *kernel, int bigendian) {
int i;
float result = 0;
int half_size = (size - 1) / 2;
for (i = 0; i < size; i++) {
int x1 = x + i - half_size;
result += _i2f(
in0[x1 * 2 + (bigendian ? 1 : 0)] +
(in0[x1 * 2 + (bigendian ? 0 : 1)] >> 8)
) *
kernel[i];
}
return result;
}
void void
ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x3(in0, x, kernel, d) \ #define KERNEL1x3(in0, x, kernel, d) \
@ -136,6 +154,16 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
out[x] = in0[x]; out[x] = in0[x];
} }
} else { } else {
int bigendian = 0;
if (im->type == IMAGING_TYPE_SPECIAL) {
if (strcmp(im->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(im->mode, "I;16N") == 0
#endif
) {
bigendian = 1;
}
}
for (y = 1; y < im->ysize - 1; y++) { for (y = 1; y < im->ysize - 1; y++) {
UINT8 *in_1 = (UINT8 *)im->image[y - 1]; UINT8 *in_1 = (UINT8 *)im->image[y - 1];
UINT8 *in0 = (UINT8 *)im->image[y]; UINT8 *in0 = (UINT8 *)im->image[y];
@ -143,14 +171,31 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
UINT8 *out = (UINT8 *)imOut->image[y]; UINT8 *out = (UINT8 *)imOut->image[y];
out[0] = in0[0]; out[0] = in0[0];
if (im->type == IMAGING_TYPE_SPECIAL) {
out[1] = in0[1];
}
for (x = 1; x < im->xsize - 1; x++) { for (x = 1; x < im->xsize - 1; x++) {
float ss = offset; float ss = offset;
ss += KERNEL1x3(in1, x, &kernel[0], 1); if (im->type == IMAGING_TYPE_SPECIAL) {
ss += KERNEL1x3(in0, x, &kernel[3], 1); ss += kernel_i16(3, in1, x, &kernel[0], bigendian);
ss += KERNEL1x3(in_1, x, &kernel[6], 1); ss += kernel_i16(3, in0, x, &kernel[3], bigendian);
out[x] = clip8(ss); ss += kernel_i16(3, in_1, x, &kernel[6], bigendian);
int ss_int = ROUND_UP(ss);
out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256);
out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8);
} else {
ss += KERNEL1x3(in1, x, &kernel[0], 1);
ss += KERNEL1x3(in0, x, &kernel[3], 1);
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
out[x] = clip8(ss);
}
}
if (im->type == IMAGING_TYPE_SPECIAL) {
out[x * 2] = in0[x * 2];
out[x * 2 + 1] = in0[x * 2 + 1];
} else {
out[x] = in0[x];
} }
out[x] = in0[x];
} }
} }
} else { } else {
@ -262,6 +307,16 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
out[x + 1] = in0[x + 1]; out[x + 1] = in0[x + 1];
} }
} else { } else {
int bigendian = 0;
if (im->type == IMAGING_TYPE_SPECIAL) {
if (strcmp(im->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(im->mode, "I;16N") == 0
#endif
) {
bigendian = 1;
}
}
for (y = 2; y < im->ysize - 2; y++) { for (y = 2; y < im->ysize - 2; y++) {
UINT8 *in_2 = (UINT8 *)im->image[y - 2]; UINT8 *in_2 = (UINT8 *)im->image[y - 2];
UINT8 *in_1 = (UINT8 *)im->image[y - 1]; UINT8 *in_1 = (UINT8 *)im->image[y - 1];
@ -272,17 +327,39 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
out[0] = in0[0]; out[0] = in0[0];
out[1] = in0[1]; out[1] = in0[1];
if (im->type == IMAGING_TYPE_SPECIAL) {
out[2] = in0[2];
out[3] = in0[3];
}
for (x = 2; x < im->xsize - 2; x++) { for (x = 2; x < im->xsize - 2; x++) {
float ss = offset; float ss = offset;
ss += KERNEL1x5(in2, x, &kernel[0], 1); if (im->type == IMAGING_TYPE_SPECIAL) {
ss += KERNEL1x5(in1, x, &kernel[5], 1); ss += kernel_i16(5, in2, x, &kernel[0], bigendian);
ss += KERNEL1x5(in0, x, &kernel[10], 1); ss += kernel_i16(5, in1, x, &kernel[5], bigendian);
ss += KERNEL1x5(in_1, x, &kernel[15], 1); ss += kernel_i16(5, in0, x, &kernel[10], bigendian);
ss += KERNEL1x5(in_2, x, &kernel[20], 1); ss += kernel_i16(5, in_1, x, &kernel[15], bigendian);
out[x] = clip8(ss); ss += kernel_i16(5, in_2, x, &kernel[20], bigendian);
int ss_int = ROUND_UP(ss);
out[x * 2 + (bigendian ? 1 : 0)] = clip8(ss_int % 256);
out[x * 2 + (bigendian ? 0 : 1)] = clip8(ss_int >> 8);
} else {
ss += KERNEL1x5(in2, x, &kernel[0], 1);
ss += KERNEL1x5(in1, x, &kernel[5], 1);
ss += KERNEL1x5(in0, x, &kernel[10], 1);
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
out[x] = clip8(ss);
}
}
if (im->type == IMAGING_TYPE_SPECIAL) {
out[x * 2 + 0] = in0[x * 2 + 0];
out[x * 2 + 1] = in0[x * 2 + 1];
out[x * 2 + 2] = in0[x * 2 + 2];
out[x * 2 + 3] = in0[x * 2 + 3];
} else {
out[x + 0] = in0[x + 0];
out[x + 1] = in0[x + 1];
} }
out[x + 0] = in0[x + 0];
out[x + 1] = in0[x + 1];
} }
} }
} else { } else {
@ -384,7 +461,8 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o
Imaging imOut; Imaging imOut;
ImagingSectionCookie cookie; ImagingSectionCookie cookie;
if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { if (im->type == IMAGING_TYPE_FLOAT32 ||
(im->type == IMAGING_TYPE_SPECIAL && im->bands != 1)) {
return (Imaging)ImagingError_ModeError(); return (Imaging)ImagingError_ModeError();
} }

View File

@ -7,6 +7,7 @@
* Copyright (c) Fredrik Lundh 1995-2003. * Copyright (c) Fredrik Lundh 1995-2003.
*/ */
#define PY_SSIZE_T_CLEAN
#include "Python.h" #include "Python.h"
/* Check that we have an ANSI compliant compiler */ /* Check that we have an ANSI compliant compiler */

View File

@ -70,8 +70,8 @@ typedef struct ImagingHistogramInstance *ImagingHistogram;
typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingOutlineInstance *ImagingOutline;
typedef struct ImagingPaletteInstance *ImagingPalette; typedef struct ImagingPaletteInstance *ImagingPalette;
/* handle magics (used with PyCObject). */ /* handle magics (used with PyCapsule). */
#define IMAGING_MAGIC "PIL Imaging" #define IMAGING_MAGIC "Pillow Imaging"
/* pixel types */ /* pixel types */
#define IMAGING_TYPE_UINT8 0 #define IMAGING_TYPE_UINT8 0

View File

@ -188,7 +188,7 @@ create_sorted_color_palette(const ColorCube cube) {
buckets, buckets,
cube->size, cube->size,
sizeof(struct _ColorBucket), sizeof(struct _ColorBucket),
(int (*)(void const *, void const *)) & compare_bucket_count (int (*)(void const *, void const *))&compare_bucket_count
); );
return buckets; return buckets;

View File

@ -780,7 +780,7 @@ ImagingLibTiffDecode(
decode_err: decode_err:
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) { if (clientstate->fp) {
// Pillow will manage the closing of the file rather than libtiff // Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup // So only call TIFFCleanup
TIFFCleanup(tiff); TIFFCleanup(tiff);
} else { } else {
@ -1008,7 +1008,17 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
) == -1) { ) == -1) {
TRACE(("Encode Error, row %d\n", state->y)); TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN; state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) {
// Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup
TIFFCleanup(tiff);
} else {
// When tif_closeproc refers to our custom _tiffCloseProc though,
// that is fine, as it does not close the file
TIFFClose(tiff);
}
if (!clientstate->fp) { if (!clientstate->fp) {
free(clientstate->data); free(clientstate->data);
} }
@ -1025,14 +1035,22 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Error flushing the tiff")); TRACE(("Error flushing the tiff"));
// likely reason is memory. // likely reason is memory.
state->errcode = IMAGING_CODEC_MEMORY; state->errcode = IMAGING_CODEC_MEMORY;
TIFFClose(tiff); if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
if (!clientstate->fp) { if (!clientstate->fp) {
free(clientstate->data); free(clientstate->data);
} }
return -1; return -1;
} }
TRACE(("Closing \n")); TRACE(("Closing \n"));
TIFFClose(tiff); if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
// reset the clientstate metadata to use it to read out the buffer. // reset the clientstate metadata to use it to read out the buffer.
clientstate->loc = 0; clientstate->loc = 0;
clientstate->size = clientstate->eof; // redundant? clientstate->size = clientstate->eof; // redundant?