diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index a1424ecc0..c4511439c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.2 +cibuildwheel==2.21.3 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a4c86cd7..837e5ea91 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -41,7 +41,7 @@ env: jobs: 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 }} runs-on: ubuntu-latest strategy: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14d75c689..bdc335f32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.6.9 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.9 + rev: 1.7.10 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.8 + rev: v19.1.1 hooks: - id: clang-format types: [c] @@ -36,7 +36,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -50,29 +50,29 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.2 + rev: 0.29.3 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.9.1 + rev: v1.0.0 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.1 + rev: 2.2.4 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.20.2 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.1 + rev: 1.4.1 hooks: - id: tox-ini-fmt diff --git a/CHANGES.rst b/CHANGES.rst index cc891ed1d..c965d60a6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- 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] diff --git a/Makefile b/Makefile index ec4159627..53164b08a 100644 --- a/Makefile +++ b/Makefile @@ -17,12 +17,10 @@ coverage: .PHONY: doc .PHONY: html doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html .PHONY: htmlview htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs htmlview .PHONY: doccheck diff --git a/Tests/test_features.py b/Tests/test_features.py index 807782847..ed7929973 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -56,17 +56,17 @@ def test_version() -> None: def test_webp_transparency() -> None: 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: 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: 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") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 47632b8fa..509e48879 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -116,10 +116,6 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) - # 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") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) @@ -767,6 +763,20 @@ class TestFileTiff: assert isinstance(reread, TiffImagePlugin.TiffImageFile) 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: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 412ab44c3..e566cd055 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -35,16 +35,25 @@ from .helper import assert_image_equal, hopper ImageFilter.UnsharpMask(10), ), ) -@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: +@pytest.mark.parametrize( + "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) - 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) assert out.mode == im.mode 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: im = hopper(mode) with pytest.raises(TypeError): @@ -145,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None: 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: with Image.open("Tests/images/hopper.bmp") as source: 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) -@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: with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 9afa02b0a..fa58492fc 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,11 +1,19 @@ from __future__ import annotations +import pytest + from .helper import hopper def test_sanity() -> None: im = hopper() - type_repr = repr(type(im.getim())) + type_repr = repr(type(im.getim())) 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"} diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 80d8c3815..6180a7b5d 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -324,17 +324,17 @@ def test_set_lut() -> None: def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) + imrgb_ptr = Image.new("RGB", (10, 10)).getim() + iml_ptr = Image.new("L", (10, 10)).getim() 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): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + _imagingmorph.apply(bytes(lut), iml_ptr, imrgb_ptr) with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + _imagingmorph.match(bytes(lut), imrgb_ptr) # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + _imagingmorph.match(bytes(lut), iml_ptr) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 67c32783f..7a2f58767 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -117,5 +117,5 @@ def test_ipythonviewer() -> None: else: pytest.fail("IPythonViewer not found") - im = hopper() - assert test_viewer.show(im) == 1 + with hopper() as im: + assert test_viewer.show(im) == 1 diff --git a/docs/Makefile b/docs/Makefile index 8f13f1aea..e90af0519 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,7 +46,7 @@ clean: -rm -rf $(BUILDDIR)/* 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 html: diff --git a/docs/conf.py b/docs/conf.py index a0f5867d7..b81d86c6c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import PIL # -- General configuration ------------------------------------------------ # 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 # 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 # if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [("py:class", "_io.BytesIO")] +nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")] # -- Options for HTML output ---------------------------------------------- @@ -338,8 +338,6 @@ linkcheck_allowed_redirects = { # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html _repo = "https://github.com/python-pillow/Pillow/" 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"), "pr": (_repo + "pull/%s", "#%s"), "pypi": ("https://pypi.org/project/%s/", "%s"), diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 366360dbe..25607e27c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -165,6 +165,16 @@ Specific WebP Feature Checks ``True`` if the WebP module is installed, until they are removed in Pillow 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 ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1218c5014..d5f5934f4 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -73,6 +73,16 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index 0d0a6f170..132030c99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dynamic = [ optional-dependencies.docs = [ "furo", "olefile", - "sphinx>=7.3", + "sphinx>=8.1", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph", diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d056a5052..73b84b344 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -225,12 +225,7 @@ if TYPE_CHECKING: from IPython.lib.pretty import PrettyPrinter from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import NumpyArray, StrOrBytesPath, TypeGuard - - if sys.version_info >= (3, 13): - from types import CapsuleType - else: - CapsuleType = object + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ str, diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index ec10230f1..fdfbee789 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -31,6 +31,10 @@ from ._typing import SupportsRead try: from . import _imagingcms as core + + _CmsProfileCompatible = Union[ + str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" + ] except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -349,19 +353,17 @@ class ImageCmsTransform(Image.ImagePointHandler): return self.apply(im) def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image: - im.load() if imOut is 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() return imOut def apply_in_place(self, im: Image.Image) -> Image.Image: - im.load() if im.mode != self.output_mode: msg = "mode mismatch" 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() return im @@ -391,10 +393,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | # pyCMS compatible layer # --------------------------------------------------------------------. -_CmsProfileCompatible = Union[ - str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile -] - class PyCMSError(Exception): """(pyCMS) Exception class. diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191ca8524..15464947d 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -59,13 +59,12 @@ class _Operand: if im2 is None: # unary operation out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im_1.im.id) + _imagingmath.unop(op, out.getim(), im_1.getim()) else: # binary operation im_2 = self.__fixup(im2) @@ -86,14 +85,12 @@ class _Operand: if im_2.size != size: im_2 = im_2.crop((0, 0) + size) out = Image.new(mode or im_1.mode, im_1.size, None) - im_1.load() - im_2.load() try: op = getattr(_imagingmath, f"{op}_{im_1.mode}") except AttributeError as e: msg = f"bad operand type for '{op}'" 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) # unary operators diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 6a43983d3..f0a066b5b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -213,7 +213,7 @@ class MorphOp: msg = "Image mode must be L" raise ValueError(msg) 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 def match(self, image: Image.Image) -> list[tuple[int, int]]: @@ -229,7 +229,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" 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]]: """Get a list of all turned on pixels in a binary image @@ -240,7 +240,7 @@ class MorphOp: if image.mode != "L": msg = "Image mode must be L" 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: """Load an operator from an mrl file""" diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index a788c9d1f..bf29fdba5 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -32,23 +32,12 @@ from typing import TYPE_CHECKING, Any, cast from . import Image, ImageFile +if TYPE_CHECKING: + from ._typing import CapsuleType + # -------------------------------------------------------------------- # 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: source = None @@ -62,18 +51,18 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: def _pyimagingtkcall( - command: str, photo: PhotoImage | tkinter.PhotoImage, id: int + command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType ) -> None: tk = photo.tk try: - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) except tkinter.TclError: # activate Tkinter hook # may raise an error if it cannot attach to Tkinter from . import _imagingtk _imagingtk.tkinit(tk.interpaddr()) - tk.call(command, photo, id) + tk.call(command, photo, repr(ptr)) # -------------------------------------------------------------------- @@ -142,7 +131,10 @@ class PhotoImage: self.paste(image) def __del__(self) -> None: - name = self.__photo.name + try: + name = self.__photo.name + except AttributeError: + return self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) @@ -185,15 +177,14 @@ class PhotoImage: the bitmap image. """ # convert to blittable - im.load() + ptr = im.getim() image = im.im - if image.isblock() and im.mode == self.__mode: - block = image - else: - block = image.new_block(self.__mode, im.size) + if not image.isblock() or im.mode != self.__mode: + block = Image.core.new_block(self.__mode, im.size) 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.__size = image.size - if _pilbitmap_check(): - # 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) + self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw) def __del__(self) -> None: - name = self.__photo.name + try: + name = self.__photo.name + except AttributeError: + return self.__photo.name = None try: self.__photo.tk.call("image", "delete", name) @@ -273,9 +259,8 @@ class BitmapImage: def getimage(photo: PhotoImage) -> Image.Image: """Copies the contents of a PhotoImage to a PIL image memory.""" im = Image.new("RGBA", (photo.width(), photo.height())) - block = im.im - _pyimagingtkcall("PyImagingPhotoGet", photo, block.id) + _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim()) return im diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0b7dc68eb..5c50e2efd 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1869,7 +1869,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if hasattr(fp, "fileno"): try: fp.seek(0) - _fp = os.dup(fp.fileno()) + _fp = fp.fileno() except io.UnsupportedOperation: pass @@ -1942,17 +1942,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder.setimage(im.im, (0, 0) + im.size) while True: - # undone, change to self.decodermaxblock: - errcode, data = encoder.encode(16 * 1024)[1:] + errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:] if not _fp: fp.write(data) if errcode: break - if _fp: - try: - os.close(_fp) - except OSError: - pass if errcode < 0: msg = f"encoder error {errcode} when writing image file" raise OSError(msg) @@ -2126,13 +2120,24 @@ class AppendingTiffWriter(io.BytesIO): def write(self, data: Buffer, /) -> int: return self.f.write(data) - def readShort(self) -> int: - (value,) = struct.unpack(self.shortFmt, self.f.read(2)) + def _fmt(self, field_size: int) -> str: + 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 + def readShort(self) -> int: + return self._read(2) + def readLong(self) -> int: - (value,) = struct.unpack(self.longFmt, self.f.read(4)) - return value + return self._read(4) @staticmethod def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: @@ -2145,15 +2150,18 @@ class AppendingTiffWriter(io.BytesIO): bytes_written = self.f.write(struct.pack(self.longFmt, value)) 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: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - self._verify_bytes_written(bytes_written, 2) + return self._rewriteLast(value, 2) def rewriteLastLong(self, value: int) -> None: - self.f.seek(-4, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + return self._rewriteLast(value, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) @@ -2185,32 +2193,22 @@ class AppendingTiffWriter(io.BytesIO): cur_pos = self.f.tell() if is_local: - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos + 4) else: self.f.seek(offset) - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos) elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) - def fixOffsets( - 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) - + def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): - offset = self.readShort() if isShort else self.readLong() + offset = self._read(field_size) 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 if count != 1: msg = "not implemented" @@ -2222,10 +2220,19 @@ class AppendingTiffWriter(io.BytesIO): self.f.seek(-10, os.SEEK_CUR) self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.f.seek(8, os.SEEK_CUR) - elif isShort: - self.rewriteLastShort(offset) 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: diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index 2abd6d0f7..ddcf93ab1 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -2,6 +2,8 @@ import datetime import sys from typing import Literal, SupportsFloat, TypedDict +from ._typing import CapsuleType + littlecms_version: str | None _Tuple3f = tuple[float, float, float] @@ -108,7 +110,7 @@ class CmsProfile: def is_intent_supported(self, intent: int, direction: int, /) -> int: ... 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_frombytes(profile: bytes, /) -> CmsProfile: ... diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index da49f7c5d..0a7d87cc2 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -15,6 +15,11 @@ if TYPE_CHECKING: except (ImportError, AttributeError): pass +if sys.version_info >= (3, 13): + from types import CapsuleType +else: + CapsuleType = object + if sys.version_info >= (3, 12): from collections.abc import Buffer else: diff --git a/src/PIL/features.py b/src/PIL/features.py index 24c5ee978..75d59e01c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -146,10 +146,11 @@ def check_feature(feature: str) -> bool | None: module, flag, ver = features[feature] + if isinstance(flag, bool): + deprecate(f'check_feature("{feature}")', 12) try: imported_module = __import__(module, fromlist=["PIL"]) if isinstance(flag, bool): - deprecate(f'check_feature("{feature}")', 12) return flag return getattr(imported_module, flag) except ModuleNotFoundError: diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 727ee6bed..a36c3e0bd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -56,19 +56,34 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging 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 defined(_WIN64) - id = _atoi64(name); -#else - id = atol(name); -#endif - if (!id) { + if (name[0] == '<') { + name++; + } else { + // Special case for PyPy, where the string representation of a Capsule + // refers directly to the pointer itself, not to the PyCapsule object. + direct_pointer = 1; + } + + if (strncmp(name, expected, strlen(expected))) { 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 diff --git a/src/_imaging.c b/src/_imaging.c index 426036335..2db4486b2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3670,15 +3670,12 @@ static struct PyMethodDef methods[] = { /* Unsharpmask extension */ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, - {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, /* Misc. */ - {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ @@ -3703,16 +3700,40 @@ _getattr_bands(ImagingObject *self, void *closure) { static PyObject * _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); } +static void +_ptr_destructor(PyObject *capsule) { + PyObject *self = (PyObject *)PyCapsule_GetContext(capsule); + Py_DECREF(self); +} + static PyObject * _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 * _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( "(sn)(sn)(sn)", "image8", @@ -4194,6 +4215,7 @@ static PyMethodDef functions[] = { {"blend", (PyCFunction)_blend, METH_VARARGS}, {"fill", (PyCFunction)_fill, METH_VARARGS}, {"new", (PyCFunction)_new, METH_VARARGS}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ diff --git a/src/_imagingcms.c b/src/_imagingcms.c index bafe787a7..1823bcf03 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -531,23 +531,24 @@ buildProofTransform(PyObject *self, PyObject *args) { static PyObject * cms_transform_apply(CmsTransformObject *self, PyObject *args) { - Py_ssize_t idIn; - Py_ssize_t idOut; + PyObject *i0, *i1; Imaging im; Imaging imOut; - int result; - - if (!PyArg_ParseTuple(args, "nn:apply", &idIn, &idOut)) { + if (!PyArg_ParseTuple(args, "OO:apply", &i0, &i1)) { return NULL; } - im = (Imaging)idIn; - imOut = (Imaging)idOut; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC) || + !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)); } /* -------------------------------------------------------------------- */ diff --git a/src/_imagingft.c b/src/_imagingft.c index 8bef45baf..7f2464124 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -830,7 +830,6 @@ font_render(FontObject *self, PyObject *args) { unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ PyObject *image; Imaging im; - Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ float stroke_width = 0; @@ -922,17 +921,13 @@ font_render(FontObject *self, PyObject *args) { width += ceil(stroke_width * 2 + x_start); height += ceil(stroke_width * 2 + y_start); image = PyObject_CallFunction(fill, "ii", width, height); - if (image == Py_None) { - PyMem_Del(glyph_info); - return Py_BuildValue("N(ii)", image, 0, 0); - } else if (image == NULL) { + if (image == NULL) { PyMem_Del(glyph_info); return NULL; } - PyObject *imageId = PyObject_GetAttrString(image, "id"); - id = PyLong_AsSsize_t(imageId); - Py_XDECREF(imageId); - im = (Imaging)id; + PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); + Py_XDECREF(imagePtr); x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 550a10903..dbe636707 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -23,6 +23,9 @@ #define MAX_INT32 2147483647.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) \ void name(Imaging out, Imaging im1) { \ int x, y; \ @@ -168,15 +171,24 @@ _unop(PyObject *self, PyObject *args) { Imaging im1; void (*unop)(Imaging, Imaging); - Py_ssize_t op, i0, i1; - if (!PyArg_ParseTuple(args, "nnn", &op, &i0, &i1)) { + PyObject *op, *i0, *i1; + if (!PyArg_ParseTuple(args, "OOO", &op, &i0, &i1)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; + if (!PyCapsule_IsValid(op, MATH_FUNC_UNOP_MAGIC)) { + 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); @@ -191,16 +203,26 @@ _binop(PyObject *self, PyObject *args) { Imaging im2; void (*binop)(Imaging, Imaging, Imaging); - Py_ssize_t op, i0, i1, i2; - if (!PyArg_ParseTuple(args, "nnnn", &op, &i0, &i1, &i2)) { + PyObject *op, *i0, *i1, *i2; + if (!PyArg_ParseTuple(args, "OOOO", &op, &i0, &i1, &i2)) { return NULL; } - out = (Imaging)i0; - im1 = (Imaging)i1; - im2 = (Imaging)i2; + if (!PyCapsule_IsValid(op, MATH_FUNC_BINOP_MAGIC)) { + PyErr_Format(PyExc_TypeError, "Expected '%s' Capsule", MATH_FUNC_BINOP_MAGIC); + 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); @@ -213,8 +235,17 @@ static PyMethodDef _functions[] = { }; static void -install(PyObject *d, char *name, void *value) { - PyObject *v = PyLong_FromSsize_t((Py_ssize_t)value); +install_unary(PyObject *d, char *name, void *func) { + 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)) { PyErr_Clear(); } @@ -225,50 +256,50 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - install(d, "abs_I", abs_I); - install(d, "neg_I", neg_I); - install(d, "add_I", add_I); - install(d, "sub_I", sub_I); - install(d, "diff_I", diff_I); - install(d, "mul_I", mul_I); - install(d, "div_I", div_I); - install(d, "mod_I", mod_I); - install(d, "min_I", min_I); - install(d, "max_I", max_I); - install(d, "pow_I", pow_I); + install_unary(d, "abs_I", abs_I); + install_unary(d, "neg_I", neg_I); + install_binary(d, "add_I", add_I); + install_binary(d, "sub_I", sub_I); + install_binary(d, "diff_I", diff_I); + install_binary(d, "mul_I", mul_I); + install_binary(d, "div_I", div_I); + install_binary(d, "mod_I", mod_I); + install_binary(d, "min_I", min_I); + install_binary(d, "max_I", max_I); + install_binary(d, "pow_I", pow_I); - install(d, "invert_I", invert_I); - install(d, "and_I", and_I); - install(d, "or_I", or_I); - install(d, "xor_I", xor_I); - install(d, "lshift_I", lshift_I); - install(d, "rshift_I", rshift_I); + install_unary(d, "invert_I", invert_I); + install_binary(d, "and_I", and_I); + install_binary(d, "or_I", or_I); + install_binary(d, "xor_I", xor_I); + install_binary(d, "lshift_I", lshift_I); + install_binary(d, "rshift_I", rshift_I); - install(d, "eq_I", eq_I); - install(d, "ne_I", ne_I); - install(d, "lt_I", lt_I); - install(d, "le_I", le_I); - install(d, "gt_I", gt_I); - install(d, "ge_I", ge_I); + install_binary(d, "eq_I", eq_I); + install_binary(d, "ne_I", ne_I); + install_binary(d, "lt_I", lt_I); + install_binary(d, "le_I", le_I); + install_binary(d, "gt_I", gt_I); + install_binary(d, "ge_I", ge_I); - install(d, "abs_F", abs_F); - install(d, "neg_F", neg_F); - install(d, "add_F", add_F); - install(d, "sub_F", sub_F); - install(d, "diff_F", diff_F); - install(d, "mul_F", mul_F); - install(d, "div_F", div_F); - install(d, "mod_F", mod_F); - install(d, "min_F", min_F); - install(d, "max_F", max_F); - install(d, "pow_F", pow_F); + install_unary(d, "abs_F", abs_F); + install_unary(d, "neg_F", neg_F); + install_binary(d, "add_F", add_F); + install_binary(d, "sub_F", sub_F); + install_binary(d, "diff_F", diff_F); + install_binary(d, "mul_F", mul_F); + install_binary(d, "div_F", div_F); + install_binary(d, "mod_F", mod_F); + install_binary(d, "min_F", min_F); + install_binary(d, "max_F", max_F); + install_binary(d, "pow_F", pow_F); - install(d, "eq_F", eq_F); - install(d, "ne_F", ne_F); - install(d, "lt_F", lt_F); - install(d, "le_F", le_F); - install(d, "gt_F", gt_F); - install(d, "ge_F", ge_F); + install_binary(d, "eq_F", eq_F); + install_binary(d, "ne_F", ne_F); + install_binary(d, "lt_F", lt_F); + install_binary(d, "le_F", le_F); + install_binary(d, "gt_F", gt_F); + install_binary(d, "ge_F", ge_F); return 0; } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 614dfbe7f..b763e3a6f 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -11,7 +11,6 @@ * See the README file for information on usage and redistribution. */ -#include "Python.h" #include "libImaging/Imaging.h" #define LUT_SIZE (1 << 9) @@ -30,43 +29,36 @@ static PyObject * apply(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0, i1; + Py_ssize_t lut_len; + PyObject *i0, *i1; Imaging imgin, imgout; int width, height; int row_idx, col_idx; UINT8 **inrows, **outrows; int num_changed_pixels = 0; - if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { - PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + if (!PyArg_ParseTuple(args, "s#OO", &lut, &lut_len, &i0, &i1)) { 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) { PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); 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; - imgout = (Imaging)i1; + imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); width = imgin->xsize; height = imgin->ysize; - if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { - PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); - return NULL; - } - if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1 || + imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -129,46 +121,39 @@ apply(PyObject *self, PyObject *args) { static PyObject * match(PyObject *self, PyObject *args) { const char *lut; - PyObject *py_lut; - Py_ssize_t lut_len, i0; + Py_ssize_t lut_len; + PyObject *i0; Imaging imgin; int width, height; int row_idx, col_idx; UINT8 **inrows; - PyObject *ret = PyList_New(0); - if (ret == NULL) { + + if (!PyArg_ParseTuple(args, "s#O", &lut, &lut_len, &i0)) { 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) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } - lut = PyBytes_AsString(py_lut); - imgin = (Imaging)i0; + if (!PyCapsule_IsValid(i0, IMAGING_MAGIC)) { + 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) { - Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } + PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } + inrows = imgin->image8; width = imgin->xsize; height = imgin->ysize; @@ -215,26 +200,31 @@ match(PyObject *self, PyObject *args) { */ static PyObject * get_on_pixels(PyObject *self, PyObject *args) { - Py_ssize_t i0; + PyObject *i0; Imaging img; UINT8 **rows; int row_idx, col_idx; 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); if (ret == 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++) { UINT8 *row = rows[row_idx]; for (col_idx = 0; col_idx < width; col_idx++) { diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index fbd6b425f..7b7b2e429 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -26,6 +26,8 @@ #include "Imaging.h" +#define ROUND_UP(f) ((int)((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + static inline UINT8 clip8(float in) { if (in <= 0.0) { @@ -105,6 +107,22 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { 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 ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ @@ -135,6 +153,16 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x] = in0[x]; } } 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++) { UINT8 *in_1 = (UINT8 *)im->image[y - 1]; UINT8 *in0 = (UINT8 *)im->image[y]; @@ -142,14 +170,31 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { UINT8 *out = (UINT8 *)imOut->image[y]; out[0] = in0[0]; + if (im->type == IMAGING_TYPE_SPECIAL) { + out[1] = in0[1]; + } for (x = 1; x < im->xsize - 1; x++) { float ss = offset; - 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) { + ss += kernel_i16(3, in1, x, &kernel[0], bigendian); + ss += kernel_i16(3, in0, x, &kernel[3], bigendian); + 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 { @@ -261,6 +306,16 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[x + 1] = in0[x + 1]; } } 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++) { UINT8 *in_2 = (UINT8 *)im->image[y - 2]; UINT8 *in_1 = (UINT8 *)im->image[y - 1]; @@ -271,17 +326,39 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { out[0] = in0[0]; 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++) { float ss = offset; - 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) { + ss += kernel_i16(5, in2, x, &kernel[0], bigendian); + ss += kernel_i16(5, in1, x, &kernel[5], bigendian); + ss += kernel_i16(5, in0, x, &kernel[10], bigendian); + ss += kernel_i16(5, in_1, x, &kernel[15], bigendian); + 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 { @@ -383,7 +460,8 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; 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(); } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f6e7fb6b9..c9b7e43b4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -7,6 +7,7 @@ * Copyright (c) Fredrik Lundh 1995-2003. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" /* Check that we have an ANSI compliant compiler */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 321dce988..31052c68a 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -60,8 +60,8 @@ typedef struct ImagingHistogramInstance *ImagingHistogram; typedef struct ImagingOutlineInstance *ImagingOutline; typedef struct ImagingPaletteInstance *ImagingPalette; -/* handle magics (used with PyCObject). */ -#define IMAGING_MAGIC "PIL Imaging" +/* handle magics (used with PyCapsule). */ +#define IMAGING_MAGIC "Pillow Imaging" /* pixel types */ #define IMAGING_TYPE_UINT8 0 diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index 7e02ebf65..ec60b214e 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -188,7 +188,7 @@ create_sorted_color_palette(const ColorCube cube) { buckets, cube->size, sizeof(struct _ColorBucket), - (int (*)(void const *, void const *)) & compare_bucket_count + (int (*)(void const *, void const *))&compare_bucket_count ); return buckets; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 18a54f633..e4da9162d 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -780,7 +780,7 @@ ImagingLibTiffDecode( decode_err: // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup 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 TIFFCleanup(tiff); } else { @@ -1008,7 +1008,17 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt ) == -1) { TRACE(("Encode Error, row %d\n", state->y)); 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) { free(clientstate->data); } @@ -1025,14 +1035,22 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - TIFFClose(tiff); + if (clientstate->fp) { + TIFFCleanup(tiff); + } else { + TIFFClose(tiff); + } if (!clientstate->fp) { free(clientstate->data); } return -1; } 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. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant?