diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6b0535fc1..a0dcb92d2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.9.0 +mypy==1.10.0 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 70426d7b5..8f4a4d090 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,8 +47,8 @@ jobs: debian-11-bullseye-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-38-amd64, fedora-39-amd64, + fedora-40-amd64, gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, diff --git a/.readthedocs.yml b/.readthedocs.yml index 0c8f935d5..b83ba05b1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,10 @@ build: os: ubuntu-22.04 tools: python: "3" + jobs: + post_checkout: + - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks + - git fetch upstream --tags python: install: diff --git a/Tests/helper.py b/Tests/helper.py index 213d99427..8510a05f0 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -29,6 +29,33 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" +modes = ( + "1", + "L", + "LA", + "La", + "P", + "PA", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "BGR;15", + "BGR;16", + "BGR;24", + "CMYK", + "YCbCr", + "HSV", + "LAB", +) + + def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": # local img.show for errors. diff --git a/Tests/test_image.py b/Tests/test_image.py index ed80be503..2d09b6f86 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,43 +28,16 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, + is_big_endian, is_win32, mark_if_feature_version, + modes, skip_unless_feature, ) -# name, pixel size -image_modes = ( - ("1", 1), - ("L", 1), - ("LA", 4), - ("La", 4), - ("P", 1), - ("PA", 4), - ("F", 4), - ("I", 4), - ("I;16", 2), - ("I;16L", 2), - ("I;16B", 2), - ("I;16N", 2), - ("RGB", 4), - ("RGBA", 4), - ("RGBa", 4), - ("RGBX", 4), - ("BGR;15", 2), - ("BGR;16", 2), - ("BGR;24", 3), - ("CMYK", 4), - ("YCbCr", 4), - ("HSV", 4), - ("LAB", 4), -) - -image_mode_names = [name for name, _ in image_modes] - class TestImage: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_image_modes_success(self, mode: str) -> None: if mode.startswith("BGR;"): with pytest.warns(DeprecationWarning): @@ -1049,7 +1022,7 @@ class TestImage: class TestImageBytes: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1061,7 +1034,7 @@ class TestImageBytes: reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1074,16 +1047,11 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) - def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning): - im = Image.new(mode, (2, 2)) - else: - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixelsize)) - im.frombytes(source_bytes) - + @pytest.mark.parametrize("mode", modes) + def test_getdata_putdata(self, mode: str) -> None: + if is_big_endian and mode == "BGR;15": + pytest.xfail("Known failure of BGR;15 on big-endian") + im = hopper(mode) if mode.startswith("BGR;"): with pytest.warns(DeprecationWarning): reloaded = Image.new(mode, im.size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8bb90710a..02c75073a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, is_win32 +from .helper import assert_image_equal, hopper, is_win32, modes # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -33,7 +33,7 @@ except ImportError: class AccessTest: - # initial value + # Initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest): if bands == 1: return 1 if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band - # So (1, 2, 3) cannot be roundtripped + # These modes have less than 8 bits per band, + # so (1, 2, 3) cannot be roundtripped. return (16, 32, 49) return tuple(range(1, bands + 1)) @@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest): self.color(mode) if expected_color_int is None else expected_color_int ) - # check putpixel + # Check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), expected_color) actual_color = im.getpixel((0, 0)) @@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check putpixel negative index + # Check putpixel negative index im.putpixel((-1, -1), expected_color) actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( @@ -168,22 +168,21 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # 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): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # Check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): im.getpixel((-1, -1)) - # check initial color + # Check initial color im = Image.new(mode, (1, 1), expected_color) actual_color = im.getpixel((0, 0)) assert actual_color == expected_color, ( @@ -191,43 +190,22 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check initial color negative index + # Check initial color negative index actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( f"initial color failed with negative index for mode {mode}, " f"expected {expected_color} got {actual_color}" ) - # Check 0 + # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # Check negative index with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize( - "mode", - ( - "1", - "L", - "LA", - "I", - "I;16", - "I;16B", - "F", - "P", - "PA", - "BGR;15", - "BGR;16", - "BGR;24", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - ), - ) + @pytest.mark.parametrize("mode", modes) def test_basic(self, mode: str) -> None: if mode.startswith("BGR;"): with pytest.warns(DeprecationWarning): @@ -242,7 +220,7 @@ class TestImageGetPixel(AccessTest): @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode: str, expected_color: int) -> None: - # see https://github.com/python-pillow/Pillow/issues/452 + # See https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @@ -302,13 +280,6 @@ class TestCffi(AccessTest): im = Image.new(mode, (10, 10), 40000) self._test_get_access(im) - # These don't actually appear to be modes that I can actually make, - # as unpack sets them directly into the I mode. - # im = Image.new('I;32L', (10, 10), -2**10) - # self._test_get_access(im) - # im = Image.new('I;32B', (10, 10), 2**10) - # 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? @@ -340,23 +311,18 @@ class TestCffi(AccessTest): 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(i, (128, 128)) #PA -- undone how to make + 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) - # im = Image.new('I;32L', (10, 10), -(2**10)) - # self._test_set_access(im, -(2**13)+1) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_set_access(im, 2**13-1) - @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 + # Ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self) -> None: size = 10 @@ -365,7 +331,7 @@ class TestCffi(AccessTest): 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 + # Pixels can contain garbage if image is released assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -482,7 +448,7 @@ int main(int argc, char* argv[]) env = os.environ.copy() env["PATH"] = sys.prefix + ";" + env["PATH"] - # do not display the Windows Error Reporting dialog + # Do not display the Windows Error Reporting dialog getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index f80c5b78c..b4a300d0c 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -216,7 +216,10 @@ class TestLibPack: ) def test_I16(self) -> None: - self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + if sys.byteorder == "little": + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) diff --git a/docs/conf.py b/docs/conf.py index 392cf317e..f12b30e65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,6 +28,7 @@ needs_sphinx = "7.3" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "dater", "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", diff --git a/docs/dater.py b/docs/dater.py new file mode 100644 index 000000000..f9fb0c1da --- /dev/null +++ b/docs/dater.py @@ -0,0 +1,48 @@ +""" +Sphinx extension to add timestamps to release notes based on Git versions. + +Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. +""" + +from __future__ import annotations + +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") +VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") + + +def get_date_for(git_version: str) -> str | None: + cmd = ["git", "log", "-1", "--format=%ai", git_version] + try: + out = subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" + ) + except subprocess.CalledProcessError: + return None + return out.split()[0] + + +def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: + if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): + old_title = m.group(1) + + if tag_date := get_date_for(old_title): + new_title = f"{old_title} ({tag_date})" + else: + new_title = f"{old_title} (unreleased)" + + new_underline = "-" * len(new_title) + + result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) + source[0] = result + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("source-read", add_date) + return {"parallel_read_safe": True} diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index af205a4e8..c08a53a43 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,10 +31,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 38 | 3.11 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 39 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 40 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9 | x86-64 | diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f18d55a57..63d78f84d 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) { static cmsUInt32Number findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0) { + if ( + strcmp(PILmode, "RGB") == 0 || + strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0 + ) { return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA;16B") == 0) { + } + if (strcmp(PILmode, "RGBA;16B") == 0) { return TYPE_RGBA_16; - } else if (strcmp(PILmode, "CMYK") == 0) { + } + if (strcmp(PILmode, "CMYK") == 0) { return TYPE_CMYK_8; - } else if (strcmp(PILmode, "L") == 0) { - return TYPE_GRAY_8; - } else if (strcmp(PILmode, "L;16") == 0) { + } + if (strcmp(PILmode, "L;16") == 0) { return TYPE_GRAY_16; - } else if (strcmp(PILmode, "L;16B") == 0) { + } + if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; - } else if (strcmp(PILmode, "YCCA") == 0) { + } + if ( + strcmp(PILmode, "YCCA") == 0 || + strcmp(PILmode, "YCC") == 0 + ) { return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "YCC") == 0) { - return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "LAB") == 0) { + } + if (strcmp(PILmode, "LAB") == 0) { // LabX equivalent like ALab, but not reversed -- no #define in lcms2 return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); } - else { - /* take a wild guess... */ - return TYPE_GRAY_8; - } + /* presume "L" by default */ + return TYPE_GRAY_8; } #define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 091c84e18..04618df09 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; - memcpy(color, in, sizeof(UINT16)); -} - static void get_pixel_BGR15(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; @@ -207,7 +201,11 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); - ADD("I;16N", get_pixel_16, put_pixel_16L); +#ifdef WORDS_BIGENDIAN + ADD("I;16N", get_pixel_16B, put_pixel_16B); +#else + ADD("I;16N", get_pixel_16L, put_pixel_16L); +#endif ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32);