mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 07:57:27 +03:00 
			
		
		
		
	Merge pull request #8182 from hugovk/rm-deprecated-psfile
Remove PSFile, PyAccess and USE_CFFI_ACCESS
This commit is contained in:
		
						commit
						585bd6ad48
					
				|  | @ -28,8 +28,6 @@ fi | |||
| 
 | ||||
| python3 -m pip install --upgrade pip | ||||
| python3 -m pip install --upgrade wheel | ||||
| # TODO Update condition when cffi supports 3.13 | ||||
| if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi | ||||
| python3 -m pip install coverage | ||||
| python3 -m pip install defusedxml | ||||
| python3 -m pip install olefile | ||||
|  |  | |||
|  | @ -19,6 +19,5 @@ exclude_also = | |||
| [run] | ||||
| omit = | ||||
|     Tests/32bit_segfault_check.py | ||||
|     Tests/bench_cffi_access.py | ||||
|     Tests/check_*.py | ||||
|     Tests/createfontdatachunk.py | ||||
|  |  | |||
							
								
								
									
										3
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -18,9 +18,6 @@ else | |||
| fi | ||||
| export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | ||||
| 
 | ||||
| # TODO Update condition when cffi supports 3.13 | ||||
| if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi | ||||
| 
 | ||||
| python3 -m pip install coverage | ||||
| python3 -m pip install defusedxml | ||||
| python3 -m pip install olefile | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -72,7 +72,6 @@ jobs: | |||
|             make | ||||
|             netpbm | ||||
|             perl | ||||
|             python3${{ matrix.python-minor-version }}-cffi | ||||
|             python3${{ matrix.python-minor-version }}-cython | ||||
|             python3${{ matrix.python-minor-version }}-devel | ||||
|             python3${{ matrix.python-minor-version }}-numpy | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -64,7 +64,6 @@ jobs: | |||
|               mingw-w64-x86_64-libtiff \ | ||||
|               mingw-w64-x86_64-libwebp \ | ||||
|               mingw-w64-x86_64-openjpeg2 \ | ||||
|               mingw-w64-x86_64-python3-cffi \ | ||||
|               mingw-w64-x86_64-python3-numpy \ | ||||
|               mingw-w64-x86_64-python3-olefile \ | ||||
|               mingw-w64-x86_64-python3-setuptools \ | ||||
|  |  | |||
|  | @ -1,54 +0,0 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import time | ||||
| 
 | ||||
| from PIL import PyAccess | ||||
| 
 | ||||
| from .helper import hopper | ||||
| 
 | ||||
| # Not running this test by default. No DOS against CI. | ||||
| 
 | ||||
| 
 | ||||
| def iterate_get(size, access) -> None: | ||||
|     (w, h) = size | ||||
|     for x in range(w): | ||||
|         for y in range(h): | ||||
|             access[(x, y)] | ||||
| 
 | ||||
| 
 | ||||
| def iterate_set(size, access) -> None: | ||||
|     (w, h) = size | ||||
|     for x in range(w): | ||||
|         for y in range(h): | ||||
|             access[(x, y)] = (x % 256, y % 256, 0) | ||||
| 
 | ||||
| 
 | ||||
| def timer(func, label, *args) -> None: | ||||
|     iterations = 5000 | ||||
|     starttime = time.time() | ||||
|     for x in range(iterations): | ||||
|         func(*args) | ||||
|         if time.time() - starttime > 10: | ||||
|             break | ||||
|     endtime = time.time() | ||||
|     print( | ||||
|         f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, " | ||||
|         f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_direct() -> None: | ||||
|     im = hopper() | ||||
|     im.load() | ||||
|     # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) | ||||
|     caccess = im.im.pixel_access(False) | ||||
|     access = PyAccess.new(im, False) | ||||
| 
 | ||||
|     assert access is not None | ||||
|     assert caccess[(0, 0)] == access[(0, 0)] | ||||
| 
 | ||||
|     print(f"Size: {im.width}x{im.height}") | ||||
|     timer(iterate_get, "PyAccess - get", im.size, access) | ||||
|     timer(iterate_set, "PyAccess - set", im.size, access) | ||||
|     timer(iterate_get, "C-api - get", im.size, caccess) | ||||
|     timer(iterate_set, "C-api - set", im.size, caccess) | ||||
|  | @ -9,9 +9,9 @@ from PIL import _deprecate | |||
|     "version, expected", | ||||
|     [ | ||||
|         ( | ||||
|             11, | ||||
|             "Old thing is deprecated and will be removed in Pillow 11 " | ||||
|             r"\(2024-10-15\)\. Use new thing instead\.", | ||||
|             12, | ||||
|             "Old thing is deprecated and will be removed in Pillow 12 " | ||||
|             r"\(2025-10-15\)\. Use new thing instead\.", | ||||
|         ), | ||||
|         ( | ||||
|             None, | ||||
|  | @ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None: | |||
| 
 | ||||
| def test_plural() -> None: | ||||
|     expected = ( | ||||
|         r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " | ||||
|         r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " | ||||
|         r"Use new thing instead\." | ||||
|     ) | ||||
|     with pytest.warns(DeprecationWarning, match=expected): | ||||
|         _deprecate.deprecate("Old things", 11, "new thing", plural=True) | ||||
|         _deprecate.deprecate("Old things", 12, "new thing", plural=True) | ||||
| 
 | ||||
| 
 | ||||
| def test_replacement_and_action() -> None: | ||||
|     expected = "Use only one of 'replacement' and 'action'" | ||||
|     with pytest.raises(ValueError, match=expected): | ||||
|         _deprecate.deprecate( | ||||
|             "Old thing", 11, replacement="new thing", action="Upgrade to new thing" | ||||
|             "Old thing", 12, replacement="new thing", action="Upgrade to new thing" | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -78,16 +78,16 @@ def test_replacement_and_action() -> None: | |||
| ) | ||||
| def test_action(action: str) -> None: | ||||
|     expected = ( | ||||
|         r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " | ||||
|         r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " | ||||
|         r"Upgrade to new thing\." | ||||
|     ) | ||||
|     with pytest.warns(DeprecationWarning, match=expected): | ||||
|         _deprecate.deprecate("Old thing", 11, action=action) | ||||
|         _deprecate.deprecate("Old thing", 12, action=action) | ||||
| 
 | ||||
| 
 | ||||
| def test_no_replacement_or_action() -> None: | ||||
|     expected = ( | ||||
|         r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" | ||||
|         r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)" | ||||
|     ) | ||||
|     with pytest.warns(DeprecationWarning, match=expected): | ||||
|         _deprecate.deprecate("Old thing", 11) | ||||
|         _deprecate.deprecate("Old thing", 12) | ||||
|  |  | |||
|  | @ -329,46 +329,6 @@ def test_read_binary_preview() -> None: | |||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| def test_readline_psfile(tmp_path: Path) -> None: | ||||
|     # check all the freaking line endings possible from the spec | ||||
|     # test_string = u'something\r\nelse\n\rbaz\rbif\n' | ||||
|     line_endings = ["\r\n", "\n", "\n\r", "\r"] | ||||
|     strings = ["something", "else", "baz", "bif"] | ||||
| 
 | ||||
|     def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: | ||||
|         ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}" | ||||
|         assert t.readline().strip("\r\n") == "something", ending | ||||
|         assert t.readline().strip("\r\n") == "else", ending | ||||
|         assert t.readline().strip("\r\n") == "baz", ending | ||||
|         assert t.readline().strip("\r\n") == "bif", ending | ||||
| 
 | ||||
|     def _test_readline_io_psfile(test_string: str, ending: str) -> None: | ||||
|         f = io.BytesIO(test_string.encode("latin-1")) | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             t = EpsImagePlugin.PSFile(f) | ||||
|         _test_readline(t, ending) | ||||
| 
 | ||||
|     def _test_readline_file_psfile(test_string: str, ending: str) -> None: | ||||
|         f = str(tmp_path / "temp.txt") | ||||
|         with open(f, "wb") as w: | ||||
|             w.write(test_string.encode("latin-1")) | ||||
| 
 | ||||
|         with open(f, "rb") as r: | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 t = EpsImagePlugin.PSFile(r) | ||||
|             _test_readline(t, ending) | ||||
| 
 | ||||
|     for ending in line_endings: | ||||
|         s = ending.join(strings) | ||||
|         _test_readline_io_psfile(s, ending) | ||||
|         _test_readline_file_psfile(s, ending) | ||||
| 
 | ||||
| 
 | ||||
| def test_psfile_deprecation() -> None: | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         EpsImagePlugin.PSFile(None) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) | ||||
| @pytest.mark.parametrize( | ||||
|     "line_ending", | ||||
|  |  | |||
|  | @ -12,19 +12,6 @@ from PIL import Image | |||
| 
 | ||||
| from .helper import assert_image_equal, hopper, is_win32 | ||||
| 
 | ||||
| # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 | ||||
| # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 | ||||
| cffi: ModuleType | None | ||||
| if os.environ.get("PYTHONOPTIMIZE") == "2": | ||||
|     cffi = None | ||||
| else: | ||||
|     try: | ||||
|         import cffi | ||||
| 
 | ||||
|         from PIL import PyAccess | ||||
|     except ImportError: | ||||
|         cffi = None | ||||
| 
 | ||||
| numpy: ModuleType | None | ||||
| try: | ||||
|     import numpy | ||||
|  | @ -32,21 +19,7 @@ except ImportError: | |||
|     numpy = None | ||||
| 
 | ||||
| 
 | ||||
| class AccessTest: | ||||
|     # Initial value | ||||
|     _init_cffi_access = Image.USE_CFFI_ACCESS | ||||
|     _need_cffi_access = False | ||||
| 
 | ||||
|     @classmethod | ||||
|     def setup_class(cls) -> None: | ||||
|         Image.USE_CFFI_ACCESS = cls._need_cffi_access | ||||
| 
 | ||||
|     @classmethod | ||||
|     def teardown_class(cls) -> None: | ||||
|         Image.USE_CFFI_ACCESS = cls._init_cffi_access | ||||
| 
 | ||||
| 
 | ||||
| class TestImagePutPixel(AccessTest): | ||||
| class TestImagePutPixel: | ||||
|     def test_sanity(self) -> None: | ||||
|         im1 = hopper() | ||||
|         im2 = Image.new(im1.mode, im1.size, 0) | ||||
|  | @ -131,7 +104,7 @@ class TestImagePutPixel(AccessTest): | |||
|         assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) | ||||
| 
 | ||||
| 
 | ||||
| class TestImageGetPixel(AccessTest): | ||||
| class TestImageGetPixel: | ||||
|     @staticmethod | ||||
|     def color(mode: str) -> int | tuple[int, ...]: | ||||
|         bands = Image.getmodebands(mode) | ||||
|  | @ -144,9 +117,6 @@ class TestImageGetPixel(AccessTest): | |||
|         return tuple(range(1, bands + 1)) | ||||
| 
 | ||||
|     def check(self, mode: str, expected_color_int: int | None = None) -> None: | ||||
|         if self._need_cffi_access and mode.startswith("BGR;"): | ||||
|             pytest.skip("Support not added to deprecated module for BGR;* modes") | ||||
| 
 | ||||
|         expected_color = ( | ||||
|             self.color(mode) if expected_color_int is None else expected_color_int | ||||
|         ) | ||||
|  | @ -171,15 +141,14 @@ class TestImageGetPixel(AccessTest): | |||
|         # Check 0x0 image with None initial color | ||||
|         im = Image.new(mode, (0, 0), None) | ||||
|         assert im.load() is not None | ||||
|         error = ValueError if self._need_cffi_access else IndexError | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.putpixel((0, 0), expected_color) | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.getpixel((0, 0)) | ||||
|         # Check negative index | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.putpixel((-1, -1), expected_color) | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.getpixel((-1, -1)) | ||||
| 
 | ||||
|         # Check initial color | ||||
|  | @ -199,10 +168,10 @@ class TestImageGetPixel(AccessTest): | |||
| 
 | ||||
|         # Check 0x0 image with initial color | ||||
|         im = Image.new(mode, (0, 0), expected_color) | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.getpixel((0, 0)) | ||||
|         # Check negative index | ||||
|         with pytest.raises(error): | ||||
|         with pytest.raises(IndexError): | ||||
|             im.getpixel((-1, -1)) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", Image.MODES) | ||||
|  | @ -235,126 +204,7 @@ class TestImageGetPixel(AccessTest): | |||
|         assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.filterwarnings("ignore::DeprecationWarning") | ||||
| @pytest.mark.skipif(cffi is None, reason="No CFFI") | ||||
| class TestCffiPutPixel(TestImagePutPixel): | ||||
|     _need_cffi_access = True | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.filterwarnings("ignore::DeprecationWarning") | ||||
| @pytest.mark.skipif(cffi is None, reason="No CFFI") | ||||
| class TestCffiGetPixel(TestImageGetPixel): | ||||
|     _need_cffi_access = True | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(cffi is None, reason="No CFFI") | ||||
| class TestCffi(AccessTest): | ||||
|     _need_cffi_access = True | ||||
| 
 | ||||
|     def _test_get_access(self, im: Image.Image) -> None: | ||||
|         """Do we get the same thing as the old pixel access | ||||
| 
 | ||||
|         Using private interfaces, forcing a capi access and | ||||
|         a pyaccess for the same image""" | ||||
|         caccess = im.im.pixel_access(False) | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             access = PyAccess.new(im, False) | ||||
|         assert access is not None | ||||
| 
 | ||||
|         w, h = im.size | ||||
|         for x in range(0, w, 10): | ||||
|             for y in range(0, h, 10): | ||||
|                 assert access[(x, y)] == caccess[(x, y)] | ||||
| 
 | ||||
|         # Access an out-of-range pixel | ||||
|         with pytest.raises(ValueError): | ||||
|             access[(access.xsize + 1, access.ysize + 1)] | ||||
| 
 | ||||
|     def test_get_vs_c(self) -> None: | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             rgb = hopper("RGB") | ||||
|             rgb.load() | ||||
|             self._test_get_access(rgb) | ||||
|             for mode in ("RGBA", "L", "LA", "1", "P", "F"): | ||||
|                 self._test_get_access(hopper(mode)) | ||||
| 
 | ||||
|             for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): | ||||
|                 im = Image.new(mode, (10, 10), 40000) | ||||
|                 self._test_get_access(im) | ||||
| 
 | ||||
|     def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: | ||||
|         """Are we writing the correct bits into the image? | ||||
| 
 | ||||
|         Using private interfaces, forcing a capi access and | ||||
|         a pyaccess for the same image""" | ||||
|         caccess = im.im.pixel_access(False) | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             access = PyAccess.new(im, False) | ||||
|         assert access is not None | ||||
| 
 | ||||
|         w, h = im.size | ||||
|         for x in range(0, w, 10): | ||||
|             for y in range(0, h, 10): | ||||
|                 access[(x, y)] = color | ||||
|                 assert color == caccess[(x, y)] | ||||
| 
 | ||||
|         # Attempt to set the value on a read-only image | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             access = PyAccess.new(im, True) | ||||
|         assert access is not None | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             access[(0, 0)] = color | ||||
| 
 | ||||
|     def test_set_vs_c(self) -> None: | ||||
|         rgb = hopper("RGB") | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             rgb.load() | ||||
|         self._test_set_access(rgb, (255, 128, 0)) | ||||
|         self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) | ||||
|         self._test_set_access(hopper("L"), 128) | ||||
|         self._test_set_access(hopper("LA"), (128, 128)) | ||||
|         self._test_set_access(hopper("1"), 255) | ||||
|         self._test_set_access(hopper("P"), 128) | ||||
|         self._test_set_access(hopper("PA"), (128, 128)) | ||||
|         self._test_set_access(hopper("F"), 1024.0) | ||||
| 
 | ||||
|         for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): | ||||
|             im = Image.new(mode, (10, 10), 40000) | ||||
|             self._test_set_access(im, 45000) | ||||
| 
 | ||||
|     @pytest.mark.filterwarnings("ignore::DeprecationWarning") | ||||
|     def test_not_implemented(self) -> None: | ||||
|         assert PyAccess.new(hopper("BGR;15")) is None | ||||
| 
 | ||||
|     # Ref https://github.com/python-pillow/Pillow/pull/2009 | ||||
|     def test_reference_counting(self) -> None: | ||||
|         size = 10 | ||||
| 
 | ||||
|         for _ in range(10): | ||||
|             # Do not save references to the image, only to the access object | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 px = Image.new("L", (size, 1), 0).load() | ||||
|             for i in range(size): | ||||
|                 # Pixels can contain garbage if image is released | ||||
|                 assert px[i, 0] == 0 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
|     def test_p_putpixel_rgb_rgba(self, mode: str) -> None: | ||||
|         for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): | ||||
|             im = Image.new(mode, (1, 1)) | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 access = PyAccess.new(im, False) | ||||
|                 assert access is not None | ||||
| 
 | ||||
|                 access.putpixel((0, 0), color) | ||||
| 
 | ||||
|                 if len(color) == 3: | ||||
|                     color += (255,) | ||||
|                 assert im.convert("RGBA").getpixel((0, 0)) == color | ||||
| 
 | ||||
| 
 | ||||
| class TestImagePutPixelError(AccessTest): | ||||
| class TestImagePutPixelError: | ||||
|     IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] | ||||
|     IMAGE_MODES2 = ["L", "I", "I;16"] | ||||
|     INVALID_TYPES = ["foo", 1.0, None] | ||||
|  |  | |||
|  | @ -17,6 +17,5 @@ coverage: | |||
| # Matches 'omit:' in .coveragerc | ||||
| ignore: | ||||
|   - "Tests/32bit_segfault_check.py" | ||||
|   - "Tests/bench_cffi_access.py" | ||||
|   - "Tests/check_*.py" | ||||
|   - "Tests/createfontdatachunk.py" | ||||
|  |  | |||
|  | @ -12,28 +12,6 @@ Deprecated features | |||
| Below are features which are considered deprecated. Where appropriate, | ||||
| a :py:exc:`DeprecationWarning` is issued. | ||||
| 
 | ||||
| PSFile | ||||
| ~~~~~~ | ||||
| 
 | ||||
| .. deprecated:: 9.5.0 | ||||
| 
 | ||||
| The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will | ||||
| be removed in Pillow 11 (2024-10-15). This class was only made as a helper to | ||||
| be used internally, so there is no replacement. If you need this functionality | ||||
| though, it is a very short class that can easily be recreated in your own code. | ||||
| 
 | ||||
| PyAccess and Image.USE_CFFI_ACCESS | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. deprecated:: 10.0.0 | ||||
| 
 | ||||
| Since Pillow's C API is now faster than PyAccess on PyPy, | ||||
| :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow | ||||
| 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. | ||||
| 
 | ||||
| ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is | ||||
| similarly deprecated. | ||||
| 
 | ||||
| ImageFile.raise_oserror | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
|  | @ -137,6 +115,29 @@ Removed features | |||
| Deprecated features are only removed in major releases after an appropriate | ||||
| period of deprecation has passed. | ||||
| 
 | ||||
| PSFile | ||||
| ~~~~~~ | ||||
| 
 | ||||
| .. deprecated:: 9.5.0 | ||||
| .. versionremoved:: 11.0.0 | ||||
| 
 | ||||
| The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15). | ||||
| This class was only made as a helper to be used internally, | ||||
| so there is no replacement. If you need this functionality though, | ||||
| it is a very short class that can easily be recreated in your own code. | ||||
| 
 | ||||
| PyAccess and Image.USE_CFFI_ACCESS | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. deprecated:: 10.0.0 | ||||
| .. versionremoved:: 11.0.0 | ||||
| 
 | ||||
| Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been | ||||
| removed. Pillow's C API will now be used on PyPy instead. | ||||
| 
 | ||||
| ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was | ||||
| similarly removed. | ||||
| 
 | ||||
| Tk/Tcl 8.4 | ||||
| ~~~~~~~~~~ | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,47 +0,0 @@ | |||
| .. py:module:: PIL.PyAccess | ||||
| .. py:currentmodule:: PIL.PyAccess | ||||
| 
 | ||||
| :py:mod:`~PIL.PyAccess` Module | ||||
| ============================== | ||||
| 
 | ||||
| The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. | ||||
| 
 | ||||
| .. note:: Accessing individual pixels is fairly slow. If you are | ||||
|           looping over all of the pixels in an image, there is likely | ||||
|           a faster way using other parts of the Pillow API. | ||||
| 
 | ||||
|           :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` | ||||
|           have methods for many standard operations. If you wish to perform | ||||
|           a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. | ||||
| 
 | ||||
| Example | ||||
| ------- | ||||
| 
 | ||||
| The following script loads an image, accesses one pixel from it, then changes it. :: | ||||
| 
 | ||||
|     from PIL import Image | ||||
| 
 | ||||
|     with Image.open("hopper.jpg") as im: | ||||
|         px = im.load() | ||||
|     print(px[4, 4]) | ||||
|     px[4, 4] = (0, 0, 0) | ||||
|     print(px[4, 4]) | ||||
| 
 | ||||
| Results in the following:: | ||||
| 
 | ||||
|     (23, 24, 68) | ||||
|     (0, 0, 0) | ||||
| 
 | ||||
| Access using negative indexes is also possible. :: | ||||
| 
 | ||||
|     px[-1, -1] = (0, 0, 0) | ||||
|     print(px[-1, -1]) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| :py:class:`PyAccess` Class | ||||
| -------------------------- | ||||
| 
 | ||||
| .. autoclass:: PIL.PyAccess.PyAccess() | ||||
|     :members: | ||||
|     :special-members: __getitem__, __setitem__ | ||||
|  | @ -32,7 +32,6 @@ Reference | |||
|    JpegPresets | ||||
|    PSDraw | ||||
|    PixelAccess | ||||
|    PyAccess | ||||
|    features | ||||
|    ../PIL | ||||
|    plugins | ||||
|  |  | |||
|  | @ -158,7 +158,7 @@ PyAccess and Image.USE_CFFI_ACCESS | |||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Since Pillow's C API is now faster than PyAccess on PyPy, | ||||
| :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow | ||||
| :py:mod:`!PyAccess` has been deprecated and will be removed in Pillow | ||||
| 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. | ||||
| 
 | ||||
| ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is | ||||
|  |  | |||
							
								
								
									
										67
									
								
								docs/releasenotes/11.0.0.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								docs/releasenotes/11.0.0.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | |||
| 11.0.0 | ||||
| ------ | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| :cve:`YYYY-XXXXX`: TODO | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Backwards Incompatible Changes | ||||
| ============================== | ||||
| 
 | ||||
| PSFile | ||||
| ^^^^^^ | ||||
| 
 | ||||
| The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15). | ||||
| This class was only made as a helper to be used internally, | ||||
| so there is no replacement. If you need this functionality though, | ||||
| it is a very short class that can easily be recreated in your own code. | ||||
| 
 | ||||
| PyAccess and Image.USE_CFFI_ACCESS | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been | ||||
| removed. Pillow's C API will now be used on PyPy instead. | ||||
| 
 | ||||
| ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was | ||||
| similarly removed. | ||||
| 
 | ||||
| Deprecations | ||||
| ============ | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| API Changes | ||||
| =========== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| API Additions | ||||
| ============= | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Other Changes | ||||
| ============= | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
|  | @ -32,7 +32,7 @@ Deprecations | |||
| PSFile | ||||
| ^^^^^^ | ||||
| 
 | ||||
| The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will | ||||
| The :py:class:`!PSFile` class has been deprecated and will | ||||
| be removed in Pillow 11 (2024-10-15). This class was only made as a helper to | ||||
| be used internally, so there is no replacement. If you need this functionality | ||||
| though, it is a very short class that can easily be recreated in your own code. | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ expected to be backported to earlier versions. | |||
| .. toctree:: | ||||
|   :maxdepth: 2 | ||||
| 
 | ||||
|   11.0.0 | ||||
|   10.4.0 | ||||
|   10.3.0 | ||||
|   10.2.0 | ||||
|  |  | |||
|  | @ -31,7 +31,6 @@ from typing import IO | |||
| 
 | ||||
| from . import Image, ImageFile | ||||
| from ._binary import i32le as i32 | ||||
| from ._deprecate import deprecate | ||||
| 
 | ||||
| # -------------------------------------------------------------------- | ||||
| 
 | ||||
|  | @ -159,43 +158,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): | |||
|     return im | ||||
| 
 | ||||
| 
 | ||||
| class PSFile: | ||||
|     """ | ||||
|     Wrapper for bytesio object that treats either CR or LF as end of line. | ||||
|     This class is no longer used internally, but kept for backwards compatibility. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, fp): | ||||
|         deprecate( | ||||
|             "PSFile", | ||||
|             11, | ||||
|             action="If you need the functionality of this class " | ||||
|             "you will need to implement it yourself.", | ||||
|         ) | ||||
|         self.fp = fp | ||||
|         self.char = None | ||||
| 
 | ||||
|     def seek(self, offset, whence=io.SEEK_SET): | ||||
|         self.char = None | ||||
|         self.fp.seek(offset, whence) | ||||
| 
 | ||||
|     def readline(self) -> str: | ||||
|         s = [self.char or b""] | ||||
|         self.char = None | ||||
| 
 | ||||
|         c = self.fp.read(1) | ||||
|         while (c not in b"\r\n") and len(c): | ||||
|             s.append(c) | ||||
|             c = self.fp.read(1) | ||||
| 
 | ||||
|         self.char = self.fp.read(1) | ||||
|         # line endings can be 1 or 2 of \r \n, in either order | ||||
|         if self.char in b"\r\n": | ||||
|             self.char = None | ||||
| 
 | ||||
|         return b"".join(s).decode("latin-1") | ||||
| 
 | ||||
| 
 | ||||
| def _accept(prefix: bytes) -> bool: | ||||
|     return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) | ||||
| 
 | ||||
|  |  | |||
|  | @ -331,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile): | |||
|                     LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY | ||||
|                     or palette | ||||
|                 ): | ||||
|                     self.pyaccess = None | ||||
|                     if "transparency" in self.info: | ||||
|                         self.im.putpalettealpha(self.info["transparency"], 0) | ||||
|                         self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) | ||||
|  |  | |||
|  | @ -329,7 +329,6 @@ class IcoImageFile(ImageFile.ImageFile): | |||
|         # if tile is PNG, it won't really be loaded yet | ||||
|         im.load() | ||||
|         self.im = im.im | ||||
|         self.pyaccess = None | ||||
|         self._mode = im.mode | ||||
|         if im.palette: | ||||
|             self.palette = im.palette | ||||
|  |  | |||
|  | @ -125,14 +125,6 @@ except ImportError as v: | |||
|     raise | ||||
| 
 | ||||
| 
 | ||||
| USE_CFFI_ACCESS = False | ||||
| cffi: ModuleType | None | ||||
| try: | ||||
|     import cffi | ||||
| except ImportError: | ||||
|     cffi = None | ||||
| 
 | ||||
| 
 | ||||
| def isImageType(t: Any) -> TypeGuard[Image]: | ||||
|     """ | ||||
|     Checks if an object is an image object. | ||||
|  | @ -229,7 +221,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): | |||
| # Registries | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import ImageFile, PyAccess | ||||
|     from . import ImageFile | ||||
| ID: list[str] = [] | ||||
| OPEN: dict[ | ||||
|     str, | ||||
|  | @ -549,7 +541,6 @@ class Image: | |||
|         self.palette = None | ||||
|         self.info = {} | ||||
|         self.readonly = 0 | ||||
|         self.pyaccess = None | ||||
|         self._exif = None | ||||
| 
 | ||||
|     @property | ||||
|  | @ -631,7 +622,6 @@ class Image: | |||
|     def _copy(self) -> None: | ||||
|         self.load() | ||||
|         self.im = self.im.copy() | ||||
|         self.pyaccess = None | ||||
|         self.readonly = 0 | ||||
| 
 | ||||
|     def _ensure_mutable(self) -> None: | ||||
|  | @ -882,7 +872,7 @@ class Image: | |||
|             msg = "cannot decode image data" | ||||
|             raise ValueError(msg) | ||||
| 
 | ||||
|     def load(self) -> core.PixelAccess | PyAccess.PyAccess | None: | ||||
|     def load(self) -> core.PixelAccess | None: | ||||
|         """ | ||||
|         Allocates storage for the image and loads the pixel data.  In | ||||
|         normal cases, you don't need to call this method, since the | ||||
|  | @ -895,7 +885,7 @@ class Image: | |||
|         operations. See :ref:`file-handling` for more information. | ||||
| 
 | ||||
|         :returns: An image access object. | ||||
|         :rtype: :py:class:`.PixelAccess` or :py:class:`.PyAccess` | ||||
|         :rtype: :py:class:`.PixelAccess` | ||||
|         """ | ||||
|         if self.im is not None and self.palette and self.palette.dirty: | ||||
|             # realize palette | ||||
|  | @ -915,14 +905,6 @@ class Image: | |||
|                 ) | ||||
| 
 | ||||
|         if self.im is not None: | ||||
|             if cffi and USE_CFFI_ACCESS: | ||||
|                 if self.pyaccess: | ||||
|                     return self.pyaccess | ||||
|                 from . import PyAccess | ||||
| 
 | ||||
|                 self.pyaccess = PyAccess.new(self, self.readonly) | ||||
|                 if self.pyaccess: | ||||
|                     return self.pyaccess | ||||
|             return self.im.pixel_access(self.readonly) | ||||
|         return None | ||||
| 
 | ||||
|  | @ -1685,8 +1667,6 @@ class Image: | |||
|         """ | ||||
| 
 | ||||
|         self.load() | ||||
|         if self.pyaccess: | ||||
|             return self.pyaccess.getpixel(xy) | ||||
|         return self.im.getpixel(tuple(xy)) | ||||
| 
 | ||||
|     def getprojection(self) -> tuple[list[int], list[int]]: | ||||
|  | @ -1983,7 +1963,6 @@ class Image: | |||
|                         msg = "alpha channel could not be added" | ||||
|                         raise ValueError(msg) from e  # sanity check | ||||
|                     self.im = im | ||||
|                 self.pyaccess = None | ||||
|                 self._mode = self.im.mode | ||||
|             except KeyError as e: | ||||
|                 msg = "illegal image mode" | ||||
|  | @ -2101,9 +2080,6 @@ class Image: | |||
|             self._copy() | ||||
|         self.load() | ||||
| 
 | ||||
|         if self.pyaccess: | ||||
|             return self.pyaccess.putpixel(xy, value) | ||||
| 
 | ||||
|         if ( | ||||
|             self.mode in ("P", "PA") | ||||
|             and isinstance(value, (list, tuple)) | ||||
|  | @ -2768,7 +2744,6 @@ class Image: | |||
|             self._mode = self.im.mode | ||||
| 
 | ||||
|         self.readonly = 0 | ||||
|         self.pyaccess = None | ||||
| 
 | ||||
|     # FIXME: the different transform methods need further explanation | ||||
|     # instead of bloating the method docs, add a separate chapter. | ||||
|  |  | |||
|  | @ -698,7 +698,6 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | |||
|         transposed_image = image.transpose(method) | ||||
|         if in_place: | ||||
|             image.im = transposed_image.im | ||||
|             image.pyaccess = None | ||||
|             image._size = transposed_image._size | ||||
|         exif_image = image if in_place else transposed_image | ||||
| 
 | ||||
|  |  | |||
|  | @ -851,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile): | |||
|                 self.png.rewind() | ||||
|                 self.__prepare_idat = self.__rewind_idat | ||||
|                 self.im = None | ||||
|                 if self.pyaccess: | ||||
|                     self.pyaccess = None | ||||
|                 self.info = self.png.im_info | ||||
|                 self.tile = self.png.im_tile | ||||
|                 self.fp = self._fp | ||||
|  | @ -1039,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile): | |||
|                     mask = updated.convert("RGBA") | ||||
|                 self._prev_im.paste(updated, self.dispose_extent, mask) | ||||
|                 self.im = self._prev_im | ||||
|                 if self.pyaccess: | ||||
|                     self.pyaccess = None | ||||
| 
 | ||||
|     def _getexif(self) -> dict[str, Any] | None: | ||||
|         if "exif" not in self.info: | ||||
|  |  | |||
|  | @ -1,381 +0,0 @@ | |||
| # | ||||
| # The Python Imaging Library | ||||
| # Pillow fork | ||||
| # | ||||
| # Python implementation of the PixelAccess Object | ||||
| # | ||||
| # Copyright (c) 1997-2009 by Secret Labs AB.  All rights reserved. | ||||
| # Copyright (c) 1995-2009 by Fredrik Lundh. | ||||
| # Copyright (c) 2013 Eric Soroos | ||||
| # | ||||
| # See the README file for information on usage and redistribution | ||||
| # | ||||
| 
 | ||||
| # Notes: | ||||
| # | ||||
| #  * Implements the pixel access object following Access.c | ||||
| #  * Taking only the tuple form, which is used from python. | ||||
| #    * Fill.c uses the integer form, but it's still going to use the old | ||||
| #      Access.c implementation. | ||||
| # | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| import logging | ||||
| import sys | ||||
| from typing import TYPE_CHECKING | ||||
| 
 | ||||
| from ._deprecate import deprecate | ||||
| 
 | ||||
| FFI: type | ||||
| try: | ||||
|     from cffi import FFI | ||||
| 
 | ||||
|     defs = """ | ||||
|     struct Pixel_RGBA { | ||||
|         unsigned char r,g,b,a; | ||||
|     }; | ||||
|     struct Pixel_I16 { | ||||
|         unsigned char l,r; | ||||
|     }; | ||||
|     """ | ||||
|     ffi = FFI() | ||||
|     ffi.cdef(defs) | ||||
| except ImportError as ex: | ||||
|     # Allow error import for doc purposes, but error out when accessing | ||||
|     # anything in core. | ||||
|     from ._util import DeferredError | ||||
| 
 | ||||
|     FFI = ffi = DeferredError.new(ex) | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import Image | ||||
| 
 | ||||
| 
 | ||||
| class PyAccess: | ||||
|     def __init__(self, img: Image.Image, readonly: bool = False) -> None: | ||||
|         deprecate("PyAccess", 11) | ||||
|         vals = dict(img.im.unsafe_ptrs) | ||||
|         self.readonly = readonly | ||||
|         self.image8 = ffi.cast("unsigned char **", vals["image8"]) | ||||
|         self.image32 = ffi.cast("int **", vals["image32"]) | ||||
|         self.image = ffi.cast("unsigned char **", vals["image"]) | ||||
|         self.xsize, self.ysize = img.im.size | ||||
|         self._img = img | ||||
| 
 | ||||
|         # Keep pointer to im object to prevent dereferencing. | ||||
|         self._im = img.im | ||||
|         if self._im.mode in ("P", "PA"): | ||||
|             self._palette = img.palette | ||||
| 
 | ||||
|         # Debugging is polluting test traces, only useful here | ||||
|         # when hacking on PyAccess | ||||
|         # logger.debug("%s", vals) | ||||
|         self._post_init() | ||||
| 
 | ||||
|     def _post_init(self) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def __setitem__( | ||||
|         self, | ||||
|         xy: tuple[int, int] | list[int], | ||||
|         color: float | tuple[int, ...] | list[int], | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Modifies the pixel at x,y. The color is given as a single | ||||
|         numerical value for single band images, and a tuple for | ||||
|         multi-band images. In addition to this, RGB and RGBA tuples | ||||
|         are accepted for P and PA images. | ||||
| 
 | ||||
|         :param xy: The pixel coordinate, given as (x, y). See | ||||
|            :ref:`coordinate-system`. | ||||
|         :param color: The pixel value. | ||||
|         """ | ||||
|         if self.readonly: | ||||
|             msg = "Attempt to putpixel a read only image" | ||||
|             raise ValueError(msg) | ||||
|         (x, y) = xy | ||||
|         if x < 0: | ||||
|             x = self.xsize + x | ||||
|         if y < 0: | ||||
|             y = self.ysize + y | ||||
|         (x, y) = self.check_xy((x, y)) | ||||
| 
 | ||||
|         if ( | ||||
|             self._im.mode in ("P", "PA") | ||||
|             and isinstance(color, (list, tuple)) | ||||
|             and len(color) in [3, 4] | ||||
|         ): | ||||
|             # RGB or RGBA value for a P or PA image | ||||
|             if self._im.mode == "PA": | ||||
|                 alpha = color[3] if len(color) == 4 else 255 | ||||
|                 color = color[:3] | ||||
|             palette_index = self._palette.getcolor(color, self._img) | ||||
|             color = (palette_index, alpha) if self._im.mode == "PA" else palette_index | ||||
| 
 | ||||
|         return self.set_pixel(x, y, color) | ||||
| 
 | ||||
|     def __getitem__(self, xy: tuple[int, int] | list[int]) -> float | tuple[int, ...]: | ||||
|         """ | ||||
|         Returns the pixel at x,y. The pixel is returned as a single | ||||
|         value for single band images or a tuple for multiple band | ||||
|         images | ||||
| 
 | ||||
|         :param xy: The pixel coordinate, given as (x, y). See | ||||
|           :ref:`coordinate-system`. | ||||
|         :returns: a pixel value for single band images, a tuple of | ||||
|           pixel values for multiband images. | ||||
|         """ | ||||
|         (x, y) = xy | ||||
|         if x < 0: | ||||
|             x = self.xsize + x | ||||
|         if y < 0: | ||||
|             y = self.ysize + y | ||||
|         (x, y) = self.check_xy((x, y)) | ||||
|         return self.get_pixel(x, y) | ||||
| 
 | ||||
|     putpixel = __setitem__ | ||||
|     getpixel = __getitem__ | ||||
| 
 | ||||
|     def check_xy(self, xy: tuple[int, int]) -> tuple[int, int]: | ||||
|         (x, y) = xy | ||||
|         if not (0 <= x < self.xsize and 0 <= y < self.ysize): | ||||
|             msg = "pixel location out of range" | ||||
|             raise ValueError(msg) | ||||
|         return xy | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> float | tuple[int, ...]: | ||||
|         raise NotImplementedError() | ||||
| 
 | ||||
|     def set_pixel( | ||||
|         self, x: int, y: int, color: float | tuple[int, ...] | list[int] | ||||
|     ) -> None: | ||||
|         raise NotImplementedError() | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccess32_2(PyAccess): | ||||
|     """PA, LA, stored in first and last bytes of a 32 bit word""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> tuple[int, int]: | ||||
|         pixel = self.pixels[y][x] | ||||
|         return pixel.r, pixel.a | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         pixel = self.pixels[y][x] | ||||
|         # tuple | ||||
|         pixel.r = min(color[0], 255) | ||||
|         pixel.a = min(color[1], 255) | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccess32_3(PyAccess): | ||||
|     """RGB and friends, stored in the first three bytes of a 32 bit word""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: | ||||
|         pixel = self.pixels[y][x] | ||||
|         return pixel.r, pixel.g, pixel.b | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         pixel = self.pixels[y][x] | ||||
|         # tuple | ||||
|         pixel.r = min(color[0], 255) | ||||
|         pixel.g = min(color[1], 255) | ||||
|         pixel.b = min(color[2], 255) | ||||
|         pixel.a = 255 | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccess32_4(PyAccess): | ||||
|     """RGBA etc, all 4 bytes of a 32 bit word""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> tuple[int, int, int, int]: | ||||
|         pixel = self.pixels[y][x] | ||||
|         return pixel.r, pixel.g, pixel.b, pixel.a | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         pixel = self.pixels[y][x] | ||||
|         # tuple | ||||
|         pixel.r = min(color[0], 255) | ||||
|         pixel.g = min(color[1], 255) | ||||
|         pixel.b = min(color[2], 255) | ||||
|         pixel.a = min(color[3], 255) | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccess8(PyAccess): | ||||
|     """1, L, P, 8 bit images stored as uint8""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = self.image8 | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         return self.pixels[y][x] | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         try: | ||||
|             # integer | ||||
|             self.pixels[y][x] = min(color, 255) | ||||
|         except TypeError: | ||||
|             # tuple | ||||
|             self.pixels[y][x] = min(color[0], 255) | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessI16_N(PyAccess): | ||||
|     """I;16 access, native bitendian without conversion""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("unsigned short **", self.image) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         return self.pixels[y][x] | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         try: | ||||
|             # integer | ||||
|             self.pixels[y][x] = min(color, 65535) | ||||
|         except TypeError: | ||||
|             # tuple | ||||
|             self.pixels[y][x] = min(color[0], 65535) | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessI16_L(PyAccess): | ||||
|     """I;16L access, with conversion""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("struct Pixel_I16 **", self.image) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         pixel = self.pixels[y][x] | ||||
|         return pixel.l + pixel.r * 256 | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         pixel = self.pixels[y][x] | ||||
|         try: | ||||
|             color = min(color, 65535) | ||||
|         except TypeError: | ||||
|             color = min(color[0], 65535) | ||||
| 
 | ||||
|         pixel.l = color & 0xFF | ||||
|         pixel.r = color >> 8 | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessI16_B(PyAccess): | ||||
|     """I;16B access, with conversion""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("struct Pixel_I16 **", self.image) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         pixel = self.pixels[y][x] | ||||
|         return pixel.l * 256 + pixel.r | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         pixel = self.pixels[y][x] | ||||
|         try: | ||||
|             color = min(color, 65535) | ||||
|         except Exception: | ||||
|             color = min(color[0], 65535) | ||||
| 
 | ||||
|         pixel.l = color >> 8 | ||||
|         pixel.r = color & 0xFF | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessI32_N(PyAccess): | ||||
|     """Signed Int32 access, native endian""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = self.image32 | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         return self.pixels[y][x] | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         self.pixels[y][x] = color | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessI32_Swap(PyAccess): | ||||
|     """I;32L/B access, with byteswapping conversion""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = self.image32 | ||||
| 
 | ||||
|     def reverse(self, i): | ||||
|         orig = ffi.new("int *", i) | ||||
|         chars = ffi.cast("unsigned char *", orig) | ||||
|         chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0] | ||||
|         return ffi.cast("int *", chars)[0] | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> int: | ||||
|         return self.reverse(self.pixels[y][x]) | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         self.pixels[y][x] = self.reverse(color) | ||||
| 
 | ||||
| 
 | ||||
| class _PyAccessF(PyAccess): | ||||
|     """32 bit float access""" | ||||
| 
 | ||||
|     def _post_init(self, *args, **kwargs): | ||||
|         self.pixels = ffi.cast("float **", self.image32) | ||||
| 
 | ||||
|     def get_pixel(self, x: int, y: int) -> float: | ||||
|         return self.pixels[y][x] | ||||
| 
 | ||||
|     def set_pixel(self, x, y, color): | ||||
|         try: | ||||
|             # not a tuple | ||||
|             self.pixels[y][x] = color | ||||
|         except TypeError: | ||||
|             # tuple | ||||
|             self.pixels[y][x] = color[0] | ||||
| 
 | ||||
| 
 | ||||
| mode_map = { | ||||
|     "1": _PyAccess8, | ||||
|     "L": _PyAccess8, | ||||
|     "P": _PyAccess8, | ||||
|     "I;16N": _PyAccessI16_N, | ||||
|     "LA": _PyAccess32_2, | ||||
|     "La": _PyAccess32_2, | ||||
|     "PA": _PyAccess32_2, | ||||
|     "RGB": _PyAccess32_3, | ||||
|     "LAB": _PyAccess32_3, | ||||
|     "HSV": _PyAccess32_3, | ||||
|     "YCbCr": _PyAccess32_3, | ||||
|     "RGBA": _PyAccess32_4, | ||||
|     "RGBa": _PyAccess32_4, | ||||
|     "RGBX": _PyAccess32_4, | ||||
|     "CMYK": _PyAccess32_4, | ||||
|     "F": _PyAccessF, | ||||
|     "I": _PyAccessI32_N, | ||||
| } | ||||
| 
 | ||||
| if sys.byteorder == "little": | ||||
|     mode_map["I;16"] = _PyAccessI16_N | ||||
|     mode_map["I;16L"] = _PyAccessI16_N | ||||
|     mode_map["I;16B"] = _PyAccessI16_B | ||||
| 
 | ||||
|     mode_map["I;32L"] = _PyAccessI32_N | ||||
|     mode_map["I;32B"] = _PyAccessI32_Swap | ||||
| else: | ||||
|     mode_map["I;16"] = _PyAccessI16_L | ||||
|     mode_map["I;16L"] = _PyAccessI16_L | ||||
|     mode_map["I;16B"] = _PyAccessI16_N | ||||
| 
 | ||||
|     mode_map["I;32L"] = _PyAccessI32_Swap | ||||
|     mode_map["I;32B"] = _PyAccessI32_N | ||||
| 
 | ||||
| 
 | ||||
| def new(img: Image.Image, readonly: bool = False) -> PyAccess | None: | ||||
|     access_type = mode_map.get(img.mode, None) | ||||
|     if not access_type: | ||||
|         logger.debug("PyAccess Not Implemented: %s", img.mode) | ||||
|         return None | ||||
|     return access_type(img, readonly) | ||||
|  | @ -45,8 +45,6 @@ def deprecate( | |||
|     elif when <= int(__version__.split(".")[0]): | ||||
|         msg = f"{deprecated} {is_} deprecated and should be removed." | ||||
|         raise RuntimeError(msg) | ||||
|     elif when == 11: | ||||
|         removed = "Pillow 11 (2024-10-15)" | ||||
|     elif when == 12: | ||||
|         removed = "Pillow 12 (2025-10-15)" | ||||
|     else: | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user