mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 16:07:30 +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 pip | ||||||
| python3 -m pip install --upgrade wheel | 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 coverage | ||||||
| python3 -m pip install defusedxml | python3 -m pip install defusedxml | ||||||
| python3 -m pip install olefile | python3 -m pip install olefile | ||||||
|  |  | ||||||
|  | @ -19,6 +19,5 @@ exclude_also = | ||||||
| [run] | [run] | ||||||
| omit = | omit = | ||||||
|     Tests/32bit_segfault_check.py |     Tests/32bit_segfault_check.py | ||||||
|     Tests/bench_cffi_access.py |  | ||||||
|     Tests/check_*.py |     Tests/check_*.py | ||||||
|     Tests/createfontdatachunk.py |     Tests/createfontdatachunk.py | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -18,9 +18,6 @@ else | ||||||
| fi | fi | ||||||
| export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | 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 coverage | ||||||
| python3 -m pip install defusedxml | python3 -m pip install defusedxml | ||||||
| python3 -m pip install olefile | 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 |             make | ||||||
|             netpbm |             netpbm | ||||||
|             perl |             perl | ||||||
|             python3${{ matrix.python-minor-version }}-cffi |  | ||||||
|             python3${{ matrix.python-minor-version }}-cython |             python3${{ matrix.python-minor-version }}-cython | ||||||
|             python3${{ matrix.python-minor-version }}-devel |             python3${{ matrix.python-minor-version }}-devel | ||||||
|             python3${{ matrix.python-minor-version }}-numpy |             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-libtiff \ | ||||||
|               mingw-w64-x86_64-libwebp \ |               mingw-w64-x86_64-libwebp \ | ||||||
|               mingw-w64-x86_64-openjpeg2 \ |               mingw-w64-x86_64-openjpeg2 \ | ||||||
|               mingw-w64-x86_64-python3-cffi \ |  | ||||||
|               mingw-w64-x86_64-python3-numpy \ |               mingw-w64-x86_64-python3-numpy \ | ||||||
|               mingw-w64-x86_64-python3-olefile \ |               mingw-w64-x86_64-python3-olefile \ | ||||||
|               mingw-w64-x86_64-python3-setuptools \ |               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", |     "version, expected", | ||||||
|     [ |     [ | ||||||
|         ( |         ( | ||||||
|             11, |             12, | ||||||
|             "Old thing is deprecated and will be removed in Pillow 11 " |             "Old thing is deprecated and will be removed in Pillow 12 " | ||||||
|             r"\(2024-10-15\)\. Use new thing instead\.", |             r"\(2025-10-15\)\. Use new thing instead\.", | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
|             None, |             None, | ||||||
|  | @ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_plural() -> None: | def test_plural() -> None: | ||||||
|     expected = ( |     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\." |         r"Use new thing instead\." | ||||||
|     ) |     ) | ||||||
|     with pytest.warns(DeprecationWarning, match=expected): |     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: | def test_replacement_and_action() -> None: | ||||||
|     expected = "Use only one of 'replacement' and 'action'" |     expected = "Use only one of 'replacement' and 'action'" | ||||||
|     with pytest.raises(ValueError, match=expected): |     with pytest.raises(ValueError, match=expected): | ||||||
|         _deprecate.deprecate( |         _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: | def test_action(action: str) -> None: | ||||||
|     expected = ( |     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\." |         r"Upgrade to new thing\." | ||||||
|     ) |     ) | ||||||
|     with pytest.warns(DeprecationWarning, match=expected): |     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: | def test_no_replacement_or_action() -> None: | ||||||
|     expected = ( |     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): |     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 |         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("prefix", (b"", simple_binary_header)) | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "line_ending", |     "line_ending", | ||||||
|  |  | ||||||
|  | @ -12,19 +12,6 @@ from PIL import Image | ||||||
| 
 | 
 | ||||||
| from .helper import assert_image_equal, hopper, is_win32 | 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 | numpy: ModuleType | None | ||||||
| try: | try: | ||||||
|     import numpy |     import numpy | ||||||
|  | @ -32,21 +19,7 @@ except ImportError: | ||||||
|     numpy = None |     numpy = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AccessTest: | class TestImagePutPixel: | ||||||
|     # 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): |  | ||||||
|     def test_sanity(self) -> None: |     def test_sanity(self) -> None: | ||||||
|         im1 = hopper() |         im1 = hopper() | ||||||
|         im2 = Image.new(im1.mode, im1.size, 0) |         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) |         assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestImageGetPixel(AccessTest): | class TestImageGetPixel: | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def color(mode: str) -> int | tuple[int, ...]: |     def color(mode: str) -> int | tuple[int, ...]: | ||||||
|         bands = Image.getmodebands(mode) |         bands = Image.getmodebands(mode) | ||||||
|  | @ -144,9 +117,6 @@ class TestImageGetPixel(AccessTest): | ||||||
|         return tuple(range(1, bands + 1)) |         return tuple(range(1, bands + 1)) | ||||||
| 
 | 
 | ||||||
|     def check(self, mode: str, expected_color_int: int | None = None) -> None: |     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 = ( |         expected_color = ( | ||||||
|             self.color(mode) if expected_color_int is None else expected_color_int |             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 |         # Check 0x0 image with None initial color | ||||||
|         im = Image.new(mode, (0, 0), None) |         im = Image.new(mode, (0, 0), None) | ||||||
|         assert im.load() is not None |         assert im.load() is not None | ||||||
|         error = ValueError if self._need_cffi_access else IndexError |         with pytest.raises(IndexError): | ||||||
|         with pytest.raises(error): |  | ||||||
|             im.putpixel((0, 0), expected_color) |             im.putpixel((0, 0), expected_color) | ||||||
|         with pytest.raises(error): |         with pytest.raises(IndexError): | ||||||
|             im.getpixel((0, 0)) |             im.getpixel((0, 0)) | ||||||
|         # Check negative index |         # Check negative index | ||||||
|         with pytest.raises(error): |         with pytest.raises(IndexError): | ||||||
|             im.putpixel((-1, -1), expected_color) |             im.putpixel((-1, -1), expected_color) | ||||||
|         with pytest.raises(error): |         with pytest.raises(IndexError): | ||||||
|             im.getpixel((-1, -1)) |             im.getpixel((-1, -1)) | ||||||
| 
 | 
 | ||||||
|         # Check initial color |         # Check initial color | ||||||
|  | @ -199,10 +168,10 @@ class TestImageGetPixel(AccessTest): | ||||||
| 
 | 
 | ||||||
|         # Check 0x0 image with initial color |         # Check 0x0 image with initial color | ||||||
|         im = Image.new(mode, (0, 0), expected_color) |         im = Image.new(mode, (0, 0), expected_color) | ||||||
|         with pytest.raises(error): |         with pytest.raises(IndexError): | ||||||
|             im.getpixel((0, 0)) |             im.getpixel((0, 0)) | ||||||
|         # Check negative index |         # Check negative index | ||||||
|         with pytest.raises(error): |         with pytest.raises(IndexError): | ||||||
|             im.getpixel((-1, -1)) |             im.getpixel((-1, -1)) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", Image.MODES) |     @pytest.mark.parametrize("mode", Image.MODES) | ||||||
|  | @ -235,126 +204,7 @@ class TestImageGetPixel(AccessTest): | ||||||
|         assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) |         assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.filterwarnings("ignore::DeprecationWarning") | class TestImagePutPixelError: | ||||||
| @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): |  | ||||||
|     IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] |     IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] | ||||||
|     IMAGE_MODES2 = ["L", "I", "I;16"] |     IMAGE_MODES2 = ["L", "I", "I;16"] | ||||||
|     INVALID_TYPES = ["foo", 1.0, None] |     INVALID_TYPES = ["foo", 1.0, None] | ||||||
|  |  | ||||||
|  | @ -17,6 +17,5 @@ coverage: | ||||||
| # Matches 'omit:' in .coveragerc | # Matches 'omit:' in .coveragerc | ||||||
| ignore: | ignore: | ||||||
|   - "Tests/32bit_segfault_check.py" |   - "Tests/32bit_segfault_check.py" | ||||||
|   - "Tests/bench_cffi_access.py" |  | ||||||
|   - "Tests/check_*.py" |   - "Tests/check_*.py" | ||||||
|   - "Tests/createfontdatachunk.py" |   - "Tests/createfontdatachunk.py" | ||||||
|  |  | ||||||
|  | @ -12,28 +12,6 @@ Deprecated features | ||||||
| Below are features which are considered deprecated. Where appropriate, | Below are features which are considered deprecated. Where appropriate, | ||||||
| a :py:exc:`DeprecationWarning` is issued. | 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 | ImageFile.raise_oserror | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
| 
 | 
 | ||||||
|  | @ -137,6 +115,29 @@ Removed features | ||||||
| Deprecated features are only removed in major releases after an appropriate | Deprecated features are only removed in major releases after an appropriate | ||||||
| period of deprecation has passed. | period of deprecation has passed. | ||||||
| 
 | 
 | ||||||
|  | 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 | 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 |    JpegPresets | ||||||
|    PSDraw |    PSDraw | ||||||
|    PixelAccess |    PixelAccess | ||||||
|    PyAccess |  | ||||||
|    features |    features | ||||||
|    ../PIL |    ../PIL | ||||||
|    plugins |    plugins | ||||||
|  |  | ||||||
|  | @ -158,7 +158,7 @@ PyAccess and Image.USE_CFFI_ACCESS | ||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| Since Pillow's C API is now faster than PyAccess on PyPy, | 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. | 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 | ``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 | 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 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 | 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. | 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:: | .. toctree:: | ||||||
|   :maxdepth: 2 |   :maxdepth: 2 | ||||||
| 
 | 
 | ||||||
|  |   11.0.0 | ||||||
|   10.4.0 |   10.4.0 | ||||||
|   10.3.0 |   10.3.0 | ||||||
|   10.2.0 |   10.2.0 | ||||||
|  |  | ||||||
|  | @ -31,7 +31,6 @@ from typing import IO | ||||||
| 
 | 
 | ||||||
| from . import Image, ImageFile | from . import Image, ImageFile | ||||||
| from ._binary import i32le as i32 | 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 |     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: | def _accept(prefix: bytes) -> bool: | ||||||
|     return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) |     return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -331,7 +331,6 @@ class GifImageFile(ImageFile.ImageFile): | ||||||
|                     LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY |                     LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY | ||||||
|                     or palette |                     or palette | ||||||
|                 ): |                 ): | ||||||
|                     self.pyaccess = None |  | ||||||
|                     if "transparency" in self.info: |                     if "transparency" in self.info: | ||||||
|                         self.im.putpalettealpha(self.info["transparency"], 0) |                         self.im.putpalettealpha(self.info["transparency"], 0) | ||||||
|                         self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) |                         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 |         # if tile is PNG, it won't really be loaded yet | ||||||
|         im.load() |         im.load() | ||||||
|         self.im = im.im |         self.im = im.im | ||||||
|         self.pyaccess = None |  | ||||||
|         self._mode = im.mode |         self._mode = im.mode | ||||||
|         if im.palette: |         if im.palette: | ||||||
|             self.palette = im.palette |             self.palette = im.palette | ||||||
|  |  | ||||||
|  | @ -125,14 +125,6 @@ except ImportError as v: | ||||||
|     raise |     raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| USE_CFFI_ACCESS = False |  | ||||||
| cffi: ModuleType | None |  | ||||||
| try: |  | ||||||
|     import cffi |  | ||||||
| except ImportError: |  | ||||||
|     cffi = None |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def isImageType(t: Any) -> TypeGuard[Image]: | def isImageType(t: Any) -> TypeGuard[Image]: | ||||||
|     """ |     """ | ||||||
|     Checks if an object is an image object. |     Checks if an object is an image object. | ||||||
|  | @ -229,7 +221,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): | ||||||
| # Registries | # Registries | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from . import ImageFile, PyAccess |     from . import ImageFile | ||||||
| ID: list[str] = [] | ID: list[str] = [] | ||||||
| OPEN: dict[ | OPEN: dict[ | ||||||
|     str, |     str, | ||||||
|  | @ -549,7 +541,6 @@ class Image: | ||||||
|         self.palette = None |         self.palette = None | ||||||
|         self.info = {} |         self.info = {} | ||||||
|         self.readonly = 0 |         self.readonly = 0 | ||||||
|         self.pyaccess = None |  | ||||||
|         self._exif = None |         self._exif = None | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | @ -631,7 +622,6 @@ class Image: | ||||||
|     def _copy(self) -> None: |     def _copy(self) -> None: | ||||||
|         self.load() |         self.load() | ||||||
|         self.im = self.im.copy() |         self.im = self.im.copy() | ||||||
|         self.pyaccess = None |  | ||||||
|         self.readonly = 0 |         self.readonly = 0 | ||||||
| 
 | 
 | ||||||
|     def _ensure_mutable(self) -> None: |     def _ensure_mutable(self) -> None: | ||||||
|  | @ -882,7 +872,7 @@ class Image: | ||||||
|             msg = "cannot decode image data" |             msg = "cannot decode image data" | ||||||
|             raise ValueError(msg) |             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 |         Allocates storage for the image and loads the pixel data.  In | ||||||
|         normal cases, you don't need to call this method, since the |         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. |         operations. See :ref:`file-handling` for more information. | ||||||
| 
 | 
 | ||||||
|         :returns: An image access object. |         :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: |         if self.im is not None and self.palette and self.palette.dirty: | ||||||
|             # realize palette |             # realize palette | ||||||
|  | @ -915,14 +905,6 @@ class Image: | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|         if self.im is not None: |         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 self.im.pixel_access(self.readonly) | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|  | @ -1685,8 +1667,6 @@ class Image: | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         self.load() |         self.load() | ||||||
|         if self.pyaccess: |  | ||||||
|             return self.pyaccess.getpixel(xy) |  | ||||||
|         return self.im.getpixel(tuple(xy)) |         return self.im.getpixel(tuple(xy)) | ||||||
| 
 | 
 | ||||||
|     def getprojection(self) -> tuple[list[int], list[int]]: |     def getprojection(self) -> tuple[list[int], list[int]]: | ||||||
|  | @ -1983,7 +1963,6 @@ class Image: | ||||||
|                         msg = "alpha channel could not be added" |                         msg = "alpha channel could not be added" | ||||||
|                         raise ValueError(msg) from e  # sanity check |                         raise ValueError(msg) from e  # sanity check | ||||||
|                     self.im = im |                     self.im = im | ||||||
|                 self.pyaccess = None |  | ||||||
|                 self._mode = self.im.mode |                 self._mode = self.im.mode | ||||||
|             except KeyError as e: |             except KeyError as e: | ||||||
|                 msg = "illegal image mode" |                 msg = "illegal image mode" | ||||||
|  | @ -2101,9 +2080,6 @@ class Image: | ||||||
|             self._copy() |             self._copy() | ||||||
|         self.load() |         self.load() | ||||||
| 
 | 
 | ||||||
|         if self.pyaccess: |  | ||||||
|             return self.pyaccess.putpixel(xy, value) |  | ||||||
| 
 |  | ||||||
|         if ( |         if ( | ||||||
|             self.mode in ("P", "PA") |             self.mode in ("P", "PA") | ||||||
|             and isinstance(value, (list, tuple)) |             and isinstance(value, (list, tuple)) | ||||||
|  | @ -2768,7 +2744,6 @@ class Image: | ||||||
|             self._mode = self.im.mode |             self._mode = self.im.mode | ||||||
| 
 | 
 | ||||||
|         self.readonly = 0 |         self.readonly = 0 | ||||||
|         self.pyaccess = None |  | ||||||
| 
 | 
 | ||||||
|     # FIXME: the different transform methods need further explanation |     # FIXME: the different transform methods need further explanation | ||||||
|     # instead of bloating the method docs, add a separate chapter. |     # 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) |         transposed_image = image.transpose(method) | ||||||
|         if in_place: |         if in_place: | ||||||
|             image.im = transposed_image.im |             image.im = transposed_image.im | ||||||
|             image.pyaccess = None |  | ||||||
|             image._size = transposed_image._size |             image._size = transposed_image._size | ||||||
|         exif_image = image if in_place else transposed_image |         exif_image = image if in_place else transposed_image | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -851,8 +851,6 @@ class PngImageFile(ImageFile.ImageFile): | ||||||
|                 self.png.rewind() |                 self.png.rewind() | ||||||
|                 self.__prepare_idat = self.__rewind_idat |                 self.__prepare_idat = self.__rewind_idat | ||||||
|                 self.im = None |                 self.im = None | ||||||
|                 if self.pyaccess: |  | ||||||
|                     self.pyaccess = None |  | ||||||
|                 self.info = self.png.im_info |                 self.info = self.png.im_info | ||||||
|                 self.tile = self.png.im_tile |                 self.tile = self.png.im_tile | ||||||
|                 self.fp = self._fp |                 self.fp = self._fp | ||||||
|  | @ -1039,8 +1037,6 @@ class PngImageFile(ImageFile.ImageFile): | ||||||
|                     mask = updated.convert("RGBA") |                     mask = updated.convert("RGBA") | ||||||
|                 self._prev_im.paste(updated, self.dispose_extent, mask) |                 self._prev_im.paste(updated, self.dispose_extent, mask) | ||||||
|                 self.im = self._prev_im |                 self.im = self._prev_im | ||||||
|                 if self.pyaccess: |  | ||||||
|                     self.pyaccess = None |  | ||||||
| 
 | 
 | ||||||
|     def _getexif(self) -> dict[str, Any] | None: |     def _getexif(self) -> dict[str, Any] | None: | ||||||
|         if "exif" not in self.info: |         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]): |     elif when <= int(__version__.split(".")[0]): | ||||||
|         msg = f"{deprecated} {is_} deprecated and should be removed." |         msg = f"{deprecated} {is_} deprecated and should be removed." | ||||||
|         raise RuntimeError(msg) |         raise RuntimeError(msg) | ||||||
|     elif when == 11: |  | ||||||
|         removed = "Pillow 11 (2024-10-15)" |  | ||||||
|     elif when == 12: |     elif when == 12: | ||||||
|         removed = "Pillow 12 (2025-10-15)" |         removed = "Pillow 12 (2025-10-15)" | ||||||
|     else: |     else: | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user