From ce4059171cc5696e7c7d8c5dc74990d5e874bf58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 18:41:05 +1100 Subject: [PATCH 01/35] Skip failing records when rendering --- Tests/test_file_wmf.py | 12 +++++++++++- src/display.c | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..d730a049a 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path from typing import IO @@ -7,7 +8,7 @@ import pytest from PIL import Image, ImageFile, WmfImagePlugin -from .helper import assert_image_similar_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper def test_load_raw() -> None: @@ -34,6 +35,15 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_render() -> None: + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:808] + b"\x00" + data[809:]) + with Image.open(b) as im: + if hasattr(Image.core, "drawwmf"): + assert_image_equal_tofile(im, "Tests/images/drawing.emf") + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/src/display.c b/src/display.c index b4e2e3899..03b9316c3 100644 --- a/src/display.c +++ b/src/display.c @@ -716,6 +716,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] +BOOL +enhMetaFileProc( + HDC hdc, HANDLETABLE FAR *lpht, CONST ENHMETARECORD *lpmr, int nHandles, LPARAM data +) { + PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles); + return TRUE; +} + PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args) { HBITMAP bitmap; @@ -796,10 +804,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); - if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_OSError, "cannot render metafile"); - goto error; - } + EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); /* step 4: extract bits from bitmap */ From b4ba4665410c70ad8976b092ee9b1ad89625c0ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 27 Oct 2024 07:03:35 +1100 Subject: [PATCH 02/35] Do not skip failing records on 32-bit --- Tests/test_file_wmf.py | 2 ++ src/display.c | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 2adf38d48..e60a5b64e 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from io import BytesIO from pathlib import Path from typing import IO @@ -35,6 +36,7 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() diff --git a/src/display.c b/src/display.c index 03b9316c3..fe5801fc0 100644 --- a/src/display.c +++ b/src/display.c @@ -716,12 +716,12 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] -BOOL +int enhMetaFileProc( - HDC hdc, HANDLETABLE FAR *lpht, CONST ENHMETARECORD *lpmr, int nHandles, LPARAM data + HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data ) { PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles); - return TRUE; + return 1; } PyObject * @@ -804,7 +804,14 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); +#ifdef _WIN64 EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); +#else + if (!PlayEnhMetaFile(dc, meta, &rect)) { + PyErr_SetString(PyExc_OSError, "cannot render metafile"); + goto error; + } +#endif /* step 4: extract bits from bitmap */ From 2ea3ea94a117772532e6f04ae7284c5842392af4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 26 Dec 2024 21:41:51 +0100 Subject: [PATCH 03/35] Skip failing WMF records on 32-bit Windows --- Tests/test_file_wmf.py | 2 -- src/display.c | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 9322bd0c5..d3cad7ca4 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from io import BytesIO from pathlib import Path from typing import IO @@ -43,7 +42,6 @@ def test_load_zero_inch() -> None: pass -@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() diff --git a/src/display.c b/src/display.c index fe5801fc0..b5e9c2a3d 100644 --- a/src/display.c +++ b/src/display.c @@ -716,7 +716,7 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { #define GET32(p, o) ((DWORD *)(p + o))[0] -int +static int CALLBACK enhMetaFileProc( HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data ) { @@ -804,14 +804,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) { /* FIXME: make background transparent? configurable? */ FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); -#ifdef _WIN64 EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect); -#else - if (!PlayEnhMetaFile(dc, meta, &rect)) { - PyErr_SetString(PyExc_OSError, "cannot render metafile"); - goto error; - } -#endif /* step 4: extract bits from bitmap */ From 6c7917d7a6031ae22e1d9eaccc2e536123ea25c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Mar 2025 07:54:47 +1100 Subject: [PATCH 04/35] Revert to zlib on macOS < 10.15 --- .github/workflows/wheels-dependencies.sh | 7 ++++++- Tests/check_wheel.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e9c54536e..e0c18d6c2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,6 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.4 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 +ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -106,7 +107,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_zlib_ng + if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 563be0b74..8ba40ba3f 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,9 +1,12 @@ from __future__ import annotations +import platform import sys from PIL import features +from .helper import is_pypy + def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} @@ -40,5 +43,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") + elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 6cc5f1f0adee0ed79bd6a4595b90933939926645 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Mar 2025 20:58:40 +1100 Subject: [PATCH 05/35] Simplified code --- Tests/check_j2k_overflow.py | 2 +- Tests/check_large_memory.py | 2 +- Tests/check_large_memory_numpy.py | 2 +- Tests/helper.py | 8 ++- Tests/test_file_apng.py | 20 +++---- Tests/test_file_blp.py | 4 +- Tests/test_file_bmp.py | 12 ++-- Tests/test_file_bufrstub.py | 4 +- Tests/test_file_dds.py | 18 +++--- Tests/test_file_eps.py | 4 +- Tests/test_file_gif.py | 98 +++++++++++++++---------------- Tests/test_file_gribstub.py | 4 +- Tests/test_file_hdf5stub.py | 2 +- Tests/test_file_icns.py | 4 +- Tests/test_file_ico.py | 24 ++++---- Tests/test_file_im.py | 8 +-- Tests/test_file_jpeg.py | 38 ++++++------ Tests/test_file_jpeg2k.py | 6 +- Tests/test_file_libtiff.py | 66 ++++++++++----------- Tests/test_file_msp.py | 4 +- Tests/test_file_palm.py | 4 +- Tests/test_file_pcx.py | 4 +- Tests/test_file_pdf.py | 12 ++-- Tests/test_file_png.py | 30 +++++----- Tests/test_file_ppm.py | 36 ++++++------ Tests/test_file_sgi.py | 8 +-- Tests/test_file_spider.py | 2 +- Tests/test_file_tga.py | 18 +++--- Tests/test_file_tiff.py | 48 +++++++-------- Tests/test_file_tiff_metadata.py | 38 ++++++------ Tests/test_file_webp_alpha.py | 12 ++-- Tests/test_file_webp_animated.py | 18 +++--- Tests/test_file_webp_lossless.py | 2 +- Tests/test_file_webp_metadata.py | 2 +- Tests/test_file_wmf.py | 4 +- Tests/test_file_xbm.py | 4 +- Tests/test_image.py | 22 +++---- Tests/test_image_convert.py | 6 +- Tests/test_image_resize.py | 2 +- Tests/test_image_split.py | 4 +- Tests/test_imagefont.py | 2 +- Tests/test_imagesequence.py | 2 +- Tests/test_imagewin_pointers.py | 2 +- Tests/test_mode_i16.py | 2 +- Tests/test_pickle.py | 6 +- Tests/test_psdraw.py | 2 +- Tests/test_shell_injection.py | 2 +- Tests/test_tiff_ifdrational.py | 2 +- 48 files changed, 315 insertions(+), 311 deletions(-) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index dbdd5a4f5..58566c4b2 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -9,6 +9,6 @@ from PIL import Image def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) - target = str(tmp_path / "temp.jpc") + target = tmp_path / "temp.jpc" with pytest.raises(OSError): im.save(target) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index a9ce79e57..c9feda3b1 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.new("L", (xdim, ydim), 0) im.save(f) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index f4ca8d0aa..458b0ab72 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im = Image.fromarray(a, "L") im.save(f) diff --git a/Tests/helper.py b/Tests/helper.py index 764935f87..909fff879 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,6 +13,7 @@ import tempfile from collections.abc import Sequence from functools import lru_cache from io import BytesIO +from pathlib import Path from typing import Any, Callable import pytest @@ -95,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - def assert_image_equal_tofile( - a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None + a: Image.Image, + filename: str | Path, + msg: str | None = None, + mode: str | None = None, ) -> None: with Image.open(filename) as img: if mode: @@ -136,7 +140,7 @@ def assert_image_similar( def assert_image_similar_tofile( a: Image.Image, - filename: str, + filename: str | Path, epsilon: float, msg: str | None = None, ) -> None: diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index b9a036173..abd7d510b 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -345,7 +345,7 @@ def test_apng_sequence_errors(test_file: str) -> None: def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, save_all=True) with Image.open(test_file) as im: @@ -375,7 +375,7 @@ def test_apng_save(tmp_path: Path) -> None: def test_apng_save_alpha(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) @@ -393,7 +393,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple # data chunks) - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/old-style-jpeg-compression.png") as im: frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] im.save( @@ -408,7 +408,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: def test_apng_save_duration_loop(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/delay.png") as im: frames = [] durations = [] @@ -471,7 +471,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: def test_apng_save_disposal(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) @@ -572,7 +572,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None: def test_apng_save_disposal_previous(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -594,7 +594,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None: def test_apng_save_blend(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) @@ -662,7 +662,7 @@ def test_apng_save_blend(tmp_path: Path) -> None: def test_apng_save_size(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (100, 100)) im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) @@ -686,7 +686,7 @@ def test_seek_after_close() -> None: def test_different_modes_in_later_frames( mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im = Image.new("L", (1, 1)) im.save( @@ -700,7 +700,7 @@ def test_different_modes_in_later_frames( def test_different_durations(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/apng/different_durations.png") as im: for _ in range(3): diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 9f2de8f98..9f50df22d 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -46,7 +46,7 @@ def test_invalid_file() -> None: def test_save(tmp_path: Path) -> None: - f = str(tmp_path / "temp.blp") + f = tmp_path / "temp.blp" for version in ("BLP1", "BLP2"): im = hopper("P") @@ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None: assert_image_equal(im.convert("RGB"), reloaded) with Image.open("Tests/images/transparent.png") as im: - f = str(tmp_path / "temp.blp") + f = tmp_path / "temp.blp" im.convert("P").save(f, blp_version=version) with Image.open(f) as reloaded: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2ff4160bd..64d2acaf5 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -17,7 +17,7 @@ from .helper import ( def test_sanity(tmp_path: Path) -> None: def roundtrip(im: Image.Image) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" im.save(outfile, "BMP") @@ -66,7 +66,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) - out = str(tmp_path / "temp.bmp") + out = tmp_path / "temp.bmp" im.save(out) with Image.open(out) as reloaded: @@ -74,7 +74,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_too_large(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) with pytest.raises(ValueError): @@ -96,7 +96,7 @@ def test_dpi() -> None: def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.bmp") as im: assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) @@ -112,7 +112,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None: def test_save_float_dpi(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.bmp") + outfile = tmp_path / "temp.bmp" with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) with Image.open(outfile) as reloaded: @@ -152,7 +152,7 @@ def test_dib_header_size(header_size: int, path: str) -> None: def test_save_dib(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.dib") + outfile = tmp_path / "temp.dib" with Image.open("Tests/images/clipboard.dib") as im: im.save(outfile) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index fc8920317..362578c56 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.bufr") + tmpfile = tmp_path / "temp.bufr" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.bufr") + temp_file = tmp_path / "temp.bufr" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 9a6042660..3388fce16 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -116,7 +116,7 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: def test_dx10_bc2(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT3) as im: im.save(out, pixel_format="BC2") @@ -129,7 +129,7 @@ def test_dx10_bc2(tmp_path: Path) -> None: def test_dx10_bc3(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT5) as im: im.save(out, pixel_format="BC3") @@ -400,7 +400,7 @@ def test_not_implemented(test_file: str) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" im = hopper("HSV") with pytest.raises(OSError, match="cannot write mode HSV as DDS"): im.save(out) @@ -416,7 +416,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: ], ) def test_save(mode: str, test_file: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(test_file) as im: assert im.mode == mode im.save(out) @@ -425,7 +425,7 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: def test_save_unsupported_pixel_format(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" im = hopper() with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): im.save(out, pixel_format="UNKNOWN") @@ -433,7 +433,7 @@ def test_save_unsupported_pixel_format(tmp_path: Path) -> None: def test_save_dxt1(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT1") assert_image_similar_tofile(im, out, 1.84) @@ -458,7 +458,7 @@ def test_save_dxt1(tmp_path: Path) -> None: def test_save_dxt3(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT3) as im: im_rgb = im.convert("RGB") im_rgb.save(out, pixel_format="DXT3") @@ -481,7 +481,7 @@ def test_save_dxt3(tmp_path: Path) -> None: def test_save_dxt5(tmp_path: Path) -> None: # RGB - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT5") assert_image_similar_tofile(im, out, 1.84) @@ -503,7 +503,7 @@ def test_save_dxt5(tmp_path: Path) -> None: def test_save_dx10_bc5(tmp_path: Path) -> None: - out = str(tmp_path / "temp.dds") + out = tmp_path / "temp.dds" with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: im.save(out, pixel_format="BC5") assert_image_similar_tofile(im, out, 9.56) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index a0c2f9216..f5acc532c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -239,7 +239,7 @@ def test_transparency() -> None: def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: - with open(str(tmp_path / "temp.eps"), "wb") as fh: + with open(tmp_path / "temp.eps", "wb") as fh: image1.save(fh, "EPS") @@ -274,7 +274,7 @@ def test_1(filename: str) -> None: def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") - tmpfile = str(tmp_path / "temp.eps") + tmpfile = tmp_path / "temp.eps" with pytest.raises(ValueError): im.save(tmpfile) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index dbbffc675..fb1a636ed 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: def test_full_palette_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 256)) full_palette_im = Image.new("P", (1, 256)) @@ -249,7 +249,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None: def test_roundtrip(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out) with Image.open(out) as reread: @@ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None: def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open(TEST_GIF) as im: im2 = im.copy() im2.save(out) @@ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None: def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: @@ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: @@ -284,7 +284,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: def test_roundtrip_save_all_1(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) im.save(out, save_all=True, append_images=[im2]) @@ -329,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: info = im.info.copy() - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, save_all=True) with Image.open(out) as reread: for header in important_headers: @@ -345,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None: im = im.resize((100, 100), Image.Resampling.LANCZOS) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) - f = str(tmp_path / "temp.gif") + f = tmp_path / "temp.gif" im2.save(f, optimize=True) with Image.open(f) as reloaded: @@ -356,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.copy().save(out, "GIF", **kwargs) reloaded = Image.open(out) @@ -599,7 +599,7 @@ def test_previous_frame_loaded() -> None: def test_save_dispose(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#111"), @@ -627,7 +627,7 @@ def test_save_dispose(tmp_path: Path) -> None: def test_dispose2_palette(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Four colors: white, gray, black, red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] @@ -661,7 +661,7 @@ def test_dispose2_palette(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # 4 frames: red/blue, red/red, blue/blue, red/blue circles = [ @@ -703,7 +703,7 @@ def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [] @@ -729,7 +729,7 @@ def test_dispose2_background(tmp_path: Path) -> None: def test_dispose2_background_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [Image.new("RGBA", (1, 20))] @@ -747,7 +747,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: def test_dispose2_previous_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (100, 100)) im.info["transparency"] = 0 @@ -766,7 +766,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: def test_dispose2_without_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (100, 100)) @@ -781,7 +781,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None: def test_transparency_in_second_frame(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -811,7 +811,7 @@ def test_no_transparency_in_second_frame() -> None: def test_remapped_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("P", (1, 2)) im2 = im.copy() @@ -829,7 +829,7 @@ def test_remapped_transparency(tmp_path: Path) -> None: def test_duration(tmp_path: Path) -> None: duration = 1000 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") # Check that the argument has priority over the info settings @@ -843,7 +843,7 @@ def test_duration(tmp_path: Path) -> None: def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#111"), @@ -878,7 +878,7 @@ def test_multiple_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/transparent_dispose.gif") as im: assert [ frame.info["duration"] for frame in ImageSequence.Iterator(im) @@ -893,7 +893,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ 1000, @@ -911,7 +911,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -944,7 +944,7 @@ def test_identical_frames(tmp_path: Path) -> None: def test_identical_frames_to_single_frame( duration: int | list[int], tmp_path: Path ) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_list = [ Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"), @@ -961,7 +961,7 @@ def test_identical_frames_to_single_frame( def test_loop_none(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) with Image.open(out) as reread: @@ -971,7 +971,7 @@ def test_loop_none(tmp_path: Path) -> None: def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.save(out, loop=number_of_loops) with Image.open(out) as reread: @@ -987,7 +987,7 @@ def test_number_of_loops(tmp_path: Path) -> None: def test_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 im.save(out) @@ -996,7 +996,7 @@ def test_background(tmp_path: Path) -> None: def test_webp_background(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test opaque WebP background if features.check("webp"): @@ -1014,7 +1014,7 @@ def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") im.info["comment"] = b"Test comment text" im.save(out) @@ -1031,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None: def test_comment_over_255(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" while len(comment) < 256: @@ -1057,7 +1057,7 @@ def test_read_multiple_comment_blocks() -> None: def test_empty_string_comment(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1091,7 +1091,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: assert "comment" not in im.info # Test that a saved image keeps the comment - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/dispose_prev.gif") as im: im.save(out, save_all=True, comment="Test") @@ -1101,7 +1101,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) @@ -1131,7 +1131,7 @@ def test_version(tmp_path: Path) -> None: def test_append_images(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Test appending single frame images im = Image.new("RGB", (100, 100), "#f00") @@ -1160,7 +1160,7 @@ def test_append_images(tmp_path: Path) -> None: def test_append_different_size_image(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (100, 100)) bigger_im = Image.new("RGB", (200, 200), "#f00") @@ -1187,7 +1187,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: im.frombytes(data) im.putpalette(palette) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, transparency=im.getpixel((252, 0))) with Image.open(out) as reloaded: @@ -1195,7 +1195,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: def test_removed_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (256, 1)) for x in range(256): @@ -1210,7 +1210,7 @@ def test_removed_transparency(tmp_path: Path) -> None: def test_rgb_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" # Single frame im = Image.new("RGB", (1, 1)) @@ -1232,7 +1232,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: def test_rgba_transparency(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = hopper("P") im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) @@ -1249,7 +1249,7 @@ def test_background_outside_palettte(tmp_path: Path) -> None: def test_bbox(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGB", (100, 100), "#fff") ims = [Image.new("RGB", (100, 100), "#000")] @@ -1260,7 +1260,7 @@ def test_bbox(tmp_path: Path) -> None: def test_bbox_alpha(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) im.putpixel((0, 1), (255, 0, 0, 0)) @@ -1279,7 +1279,7 @@ def test_palette_save_L(tmp_path: Path) -> None: palette = im.getpalette() assert palette is not None - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im_l.save(out, palette=bytes(palette)) with Image.open(out) as reloaded: @@ -1290,7 +1290,7 @@ def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) with Image.open(out) as reloaded: @@ -1306,7 +1306,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im.putpalette((0, 0, 0, 0, 0, 0)) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) with Image.open(out) as reloaded: @@ -1321,7 +1321,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: frame.putpalette(color) frames.append(frame) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" frames[0].save( out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] ) @@ -1344,7 +1344,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None: im = hopper("P") palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out, palette=palette) with Image.open(out) as reloaded: @@ -1357,7 +1357,7 @@ def test_save_I(tmp_path: Path) -> None: im = hopper("I") - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im.save(out) with Image.open(out) as reloaded: @@ -1441,7 +1441,7 @@ def test_missing_background() -> None: def test_saving_rgba(tmp_path: Path) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" with Image.open("Tests/images/transparent.png") as im: im.save(out) @@ -1452,7 +1452,7 @@ def test_saving_rgba(tmp_path: Path) -> None: @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: - out = str(tmp_path / "temp.gif") + out = tmp_path / "temp.gif" im1 = Image.new("P", (100, 100)) d = ImageDraw.Draw(im1) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 02e464ff1..960e5f4be 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: # Arrange im = hopper() - tmpfile = str(tmp_path / "temp.grib") + tmpfile = tmp_path / "temp.grib" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): @@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.grib") + temp_file = tmp_path / "temp.grib" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 024be9e80..50864009f 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None: im.load() assert handler.is_loaded() - temp_file = str(tmp_path / "temp.h5") + temp_file = tmp_path / "temp.h5" im.save(temp_file) assert handler.saved diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 94f16aeec..b6dc4bc19 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -43,7 +43,7 @@ def test_load() -> None: def test_save(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" with Image.open(TEST_FILE) as im: im.save(temp_file) @@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None: def test_save_append_images(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.icns") + temp_file = tmp_path / "temp.icns" provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) with Image.open(TEST_FILE) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 2f5e4ca5a..37bfd3f1f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -41,7 +41,7 @@ def test_black_and_white() -> None: def test_palette(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = Image.new("P", (16, 16)) im.save(temp_file) @@ -88,7 +88,7 @@ def test_save_to_bytes() -> None: def test_getpixel(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") + temp_file = tmp_path / "temp.ico" im = hopper() im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) @@ -101,8 +101,8 @@ def test_getpixel(tmp_path: Path) -> None: def test_no_duplicates(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() sizes = [(32, 32), (64, 64)] @@ -115,8 +115,8 @@ def test_no_duplicates(tmp_path: Path) -> None: def test_different_bit_depths(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.ico") - temp_file2 = str(tmp_path / "temp2.ico") + temp_file = tmp_path / "temp.ico" + temp_file2 = tmp_path / "temp2.ico" im = hopper() im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) @@ -132,8 +132,8 @@ def test_different_bit_depths(tmp_path: Path) -> None: assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) # Test that only matching sizes of different bit depths are saved - temp_file3 = str(tmp_path / "temp3.ico") - temp_file4 = str(tmp_path / "temp4.ico") + temp_file3 = tmp_path / "temp3.ico" + temp_file4 = tmp_path / "temp4.ico" im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save( @@ -186,7 +186,7 @@ def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: - outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") + outfile = tmp_path / "temp_saved_hopper_256x256.ico" # Act im.save(outfile) @@ -202,7 +202,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: """ # Arrange with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 - outfile = str(tmp_path / "temp_saved_python.ico") + outfile = tmp_path / "temp_saved_python.ico" # Act im.save(outfile) @@ -215,7 +215,7 @@ def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) - outfile = str(tmp_path / "temp_saved_multi_icon.ico") + outfile = tmp_path / "temp_saved_multi_icon.ico" im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: @@ -235,7 +235,7 @@ def test_unexpected_size() -> None: def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: - outfile = str(tmp_path / "temp_saved_hopper_draw.ico") + outfile = tmp_path / "temp_saved_hopper_draw.ico" draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, "#f00") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index d29998801..235914a2b 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -23,7 +23,7 @@ def test_sanity() -> None: def test_name_limit(tmp_path: Path) -> None: - out = str(tmp_path / ("name_limit_test" * 7 + ".im")) + out = tmp_path / ("name_limit_test" * 7 + ".im") with Image.open(TEST_IM) as im: im.save(out) assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") @@ -87,7 +87,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) def test_roundtrip(mode: str, tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) @@ -98,7 +98,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 1, 2] im.putpalette(colors) - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im.save(out) with Image.open(out) as reloaded: @@ -106,7 +106,7 @@ def test_small_palette(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: - out = str(tmp_path / "temp.im") + out = tmp_path / "temp.im" im = hopper("HSV") with pytest.raises(ValueError): im.save(out) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index a2481c336..8ab853b85 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -83,7 +83,7 @@ class TestFileJpeg: @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) @@ -194,7 +194,7 @@ class TestFileJpeg: icc_profile = im1.info["icc_profile"] assert len(icc_profile) == 3144 # Roundtrip via physical file. - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im1.save(f, icc_profile=icc_profile) with Image.open(f) as im2: assert im2.info.get("icc_profile") == icc_profile @@ -238,7 +238,7 @@ class TestFileJpeg: # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. with Image.open("Tests/images/icc_profile_big.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" icc_profile = im.info["icc_profile"] # Should not raise OSError for image with icc larger than image size. im.save( @@ -250,11 +250,11 @@ class TestFileJpeg: ) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp2.jpg") + f = tmp_path / "temp2.jpg" im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) with Image.open("Tests/images/flower2.jpg") as im: - f = str(tmp_path / "temp3.jpg") + f = tmp_path / "temp3.jpg" im.save(f, progressive=True, quality=94, exif=b" " * 43668) def test_optimize(self) -> None: @@ -268,7 +268,7 @@ class TestFileJpeg: def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) @@ -288,13 +288,13 @@ class TestFileJpeg: assert im1_bytes >= im3_bytes def test_progressive_large_buffer(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -307,7 +307,7 @@ class TestFileJpeg: def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = hopper() im.save(f, "JPEG", quality=90, exif=b"1" * 65533) @@ -335,7 +335,7 @@ class TestFileJpeg: assert exif[gps_index] == expected_exif_gps # Writing - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" exif = Image.Exif() exif[gps_index] = expected_exif_gps hopper().save(f, exif=exif) @@ -505,15 +505,15 @@ class TestFileJpeg: def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # Grayscale with Image.open("Tests/images/hopper_gray.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") # CMYK with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im.save(f, quality="keep") def test_junk_jpeg_header(self) -> None: @@ -726,7 +726,7 @@ class TestFileJpeg: def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) - f = str(tmp_path / "temp.jpeg") + f = tmp_path / "temp.jpeg" im.save(f, quality=100, optimize=True) with Image.open(f) as reloaded: @@ -762,7 +762,7 @@ class TestFileJpeg: def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -773,7 +773,7 @@ class TestFileJpeg: assert im.info["dpi"] == reloaded.info["dpi"] def test_save_dpi_rounding(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jpg") + outfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -859,7 +859,7 @@ class TestFileJpeg: exif = im.getexif() assert exif[282] == 180 - out = str(tmp_path / "out.jpg") + out = tmp_path / "out.jpg" with warnings.catch_warnings(): warnings.simplefilter("error") @@ -1005,7 +1005,7 @@ class TestFileJpeg: assert im.getxmp() == {"xmpmeta": None} def test_save_xmp(self, tmp_path: Path) -> None: - f = str(tmp_path / "temp.jpg") + f = tmp_path / "temp.jpg" im = hopper() im.save(f, xmp=b"XMP test") with Image.open(f) as reloaded: @@ -1094,7 +1094,7 @@ class TestFileJpeg: @skip_unless_feature("jpg") class TestFileCloseW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.jpg") + tmpfile = tmp_path / "temp.jpg" with Image.open("Tests/images/hopper.jpg") as im: im.save(tmpfile) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index e429610ad..916df2586 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -99,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None: def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() - outfile = str(tmp_path / "temp_test-card.png") + outfile = tmp_path / "temp_test-card.png" im.save(outfile) assert_image_similar(im, card, 1.0e-3) @@ -213,7 +213,7 @@ def test_header_errors() -> None: def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp_layers.jp2") + outfile = tmp_path / "temp_layers.jp2" for quality_layers in [[100, 50, 10], (100, 50, 10), None]: card.save(outfile, quality_layers=quality_layers) @@ -289,7 +289,7 @@ def test_mct(card: ImageFile.ImageFile) -> None: def test_sgnd(tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.jp2") + outfile = tmp_path / "temp.jp2" im = Image.new("L", (1, 1)) im.save(outfile) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f284c3f2f..25d1f5712 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -39,7 +39,7 @@ class LibTiffTestCase: assert im._compression == "group4" # can we write it back out, in a different form. - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) out_bytes = io.BytesIO() @@ -123,7 +123,7 @@ class TestFileLibTiff(LibTiffTestCase): """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" rot = orig.transpose(Image.Transpose.ROTATE_90) assert rot.size == (500, 500) rot.save(out) @@ -151,7 +151,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("legacy_api", (False, True)) def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" - f = str(tmp_path / "temp.tiff") + f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: img.save(f, tiffinfo=img.tag) @@ -247,7 +247,7 @@ class TestFileLibTiff(LibTiffTestCase): # Extra samples really doesn't make sense in this application. del new_ifd[338] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, tiffinfo=new_ifd) @@ -313,7 +313,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: @@ -347,13 +347,13 @@ class TestFileLibTiff(LibTiffTestCase): ) def test_osubfiletype(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -365,7 +365,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: @@ -375,7 +375,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out, dpi=(72, 72)) with Image.open(out) as reloaded: @@ -383,7 +383,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" i.save(out, compression="group3") with Image.open(out) as reread: @@ -400,7 +400,7 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" # out = "temp.le.tif" im.save(out) with Image.open(out) as reread: @@ -420,7 +420,7 @@ class TestFileLibTiff(LibTiffTestCase): assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) with Image.open(out) as reread: assert reread.info["compression"] == im.info["compression"] @@ -430,7 +430,7 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) @@ -457,7 +457,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with Image.open("Tests/images/pport_g4.tif") as im: im = im.convert("L") @@ -470,7 +470,7 @@ class TestFileLibTiff(LibTiffTestCase): # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out) size_raw = os.path.getsize(out) @@ -494,7 +494,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_jpeg") with Image.open(out) as reloaded: @@ -502,7 +502,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_deflate") with Image.open(out) as reloaded: @@ -510,7 +510,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(ValueError): im.save(out, compression="tiff_lzw", quality=50) @@ -525,7 +525,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression="tiff_adobe_deflate") assert_image_equal_tofile(im, out) @@ -534,7 +534,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_palette_save( self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) @@ -546,7 +546,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(out, compression=compression) @@ -686,7 +686,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: @@ -713,7 +713,7 @@ class TestFileLibTiff(LibTiffTestCase): ) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) # this shouldn't crash @@ -724,7 +724,7 @@ class TestFileLibTiff(LibTiffTestCase): # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit @@ -736,7 +736,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with open(tmpfile, "wb") as f: with open("Tests/images/g4-multi.tiff", "rb") as src: f.write(src.read()) @@ -779,7 +779,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc_profile = img.info["icc_profile"] - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" img.save(out, icc_profile=icc_profile) with Image.open(out) as reloaded: assert icc_profile == reloaded.info["icc_profile"] @@ -802,7 +802,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Contains JPEGTables (347) tag @@ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase): self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: im = Image.new("F", (1, 1)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) im.save(out) @@ -1008,7 +1008,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", (None, "jpeg")) def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" tags = { TiffImagePlugin.TILEWIDTH: 256, @@ -1147,7 +1147,7 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" im.save(out, compression=compression) with Image.open(out) as im: @@ -1160,7 +1160,7 @@ class TestFileLibTiff(LibTiffTestCase): self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: im = hopper("RGB").resize((256, 256)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" if not argument: monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) @@ -1176,13 +1176,13 @@ class TestFileLibTiff(LibTiffTestCase): @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" with pytest.raises(SystemError): im.save(out, compression=compression) def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.tif") + out = tmp_path / "temp.tif" for _ in range(10000): im.save(out, compression="jpeg") diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index b0964aabe..8c91922bd 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp" def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.msp") + test_file = tmp_path / "temp.msp" hopper("1").save(test_file) @@ -84,7 +84,7 @@ def test_msp_v2() -> None: def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() - filename = str(tmp_path / "temp.msp") + filename = tmp_path / "temp.msp" # Act/Assert with pytest.raises(OSError): diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 194f39b30..a1859bc33 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) - outfile = str(tmp_path / ("temp_" + mode + ".palm")) + outfile = tmp_path / ("temp_" + mode + ".palm") # Act im.save(outfile) @@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None: def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 21c32268c..5d7fd1c1b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper def _roundtrip(tmp_path: Path, im: Image.Image) -> None: - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im.save(f) with Image.open(f) as im2: assert im2.mode == im.mode @@ -31,7 +31,7 @@ def test_sanity(tmp_path: Path) -> None: _roundtrip(tmp_path, im) # Test an unsupported mode - f = str(tmp_path / "temp.pcx") + f = tmp_path / "temp.pcx" im = hopper("RGBA") with pytest.raises(ValueError): im.save(f) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 815686a52..bde1e3ab8 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None: def test_p_alpha(tmp_path: Path) -> None: # Arrange - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" with Image.open("Tests/images/pil123p.png") as im: assert im.mode == "P" assert isinstance(im.info["transparency"], bytes) @@ -80,7 +80,7 @@ def test_monochrome(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") - outfile = str(tmp_path / "temp_PA.pdf") + outfile = tmp_path / "temp_PA.pdf" with pytest.raises(ValueError): im.save(outfile) @@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None: def test_resolution(tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, resolution=150) with open(outfile, "rb") as fp: @@ -117,7 +117,7 @@ def test_resolution(tmp_path: Path) -> None: def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, "PDF", **params) with open(outfile, "rb") as fp: @@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None: # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile, save_all=True) assert os.path.isfile(outfile) @@ -177,7 +177,7 @@ def test_save_all(tmp_path: Path) -> None: def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") + outfile = tmp_path / "temp.pdf" im.save(outfile) assert os.path.isfile(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index efd2e5cd9..f99ca91a3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename: str) -> list[bytes]: + def get_chunks(self, filename: Path) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -89,7 +89,7 @@ class TestFilePng: assert version is not None assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" hopper("RGB").save(test_file) @@ -250,7 +250,7 @@ class TestFilePng: # each palette entry assert len(im.info["transparency"]) == 256 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -271,7 +271,7 @@ class TestFilePng: assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -294,7 +294,7 @@ class TestFilePng: assert im.getcolors() == [(100, (0, 0, 0, 0))] im = im.convert("P") - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) # check if saved image contains same transparency @@ -315,7 +315,7 @@ class TestFilePng: im_rgba = im.convert("RGBA") assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) with Image.open(test_file) as test_im: @@ -329,7 +329,7 @@ class TestFilePng: def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file) def test_load_verify(self) -> None: @@ -488,7 +488,7 @@ class TestFilePng: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im.save(f) with Image.open(f) as im2: @@ -549,7 +549,7 @@ class TestFilePng: def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.convert("P").save(test_file, dpi=(100, 100)) chunks = self.get_chunks(test_file) @@ -661,7 +661,7 @@ class TestFilePng: def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: im = hopper("P") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out, bits=4, save_all=save_all) with Image.open(out) as reloaded: @@ -671,8 +671,8 @@ class TestFilePng: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) - out = str(tmp_path / "temp.png") - im.save(str(tmp_path / "temp.png")) + out = tmp_path / "temp.png" + im.save(tmp_path / "temp.png") with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 @@ -721,7 +721,7 @@ class TestFilePng: def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" with Image.open("Tests/images/exif.png") as im: im.save(test_file) @@ -741,7 +741,7 @@ class TestFilePng: ) def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: @@ -750,7 +750,7 @@ class TestFilePng: def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" im.save(test_file, exif=b"exifstring") with Image.open(test_file) as reloaded: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index c93a8c73a..41e2b5416 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -94,7 +94,7 @@ def test_16bit_pgm() -> None: def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: - filename = str(tmp_path / "temp.pgm") + filename = tmp_path / "temp.pgm" im.save(filename, "PPM") assert_image_equal_tofile(im, filename) @@ -106,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - filename = str(tmp_path / "temp.pnm") + filename = tmp_path / "temp.pnm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -117,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -128,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - filename = str(tmp_path / "tmp.pfm") + filename = tmp_path / "tmp.pfm" im.save(filename) assert_image_equal_tofile(im, filename) @@ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None: def test_plain_data_with_comment( tmp_path: Path, header: bytes, data: bytes, comment_count: int ) -> None: - path1 = str(tmp_path / "temp1.ppm") - path2 = str(tmp_path / "temp2.ppm") + path1 = tmp_path / "temp1.ppm" + path2 = tmp_path / "temp2.ppm" comment = b"# comment" * comment_count with open(path1, "wb") as f1, open(path2, "wb") as f2: f1.write(header + b"\n\n" + data) @@ -207,7 +207,7 @@ def test_plain_data_with_comment( @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -218,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -235,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: ), ) def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(data) @@ -245,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: def test_plain_ppm_value_negative(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n-1") @@ -255,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None: def test_plain_ppm_value_too_large(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -270,7 +270,7 @@ def test_magic() -> None: def test_header_with_comments(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -279,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None: def test_non_integer_token(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -289,7 +289,7 @@ def test_non_integer_token(tmp_path: Path) -> None: def test_header_token_too_long(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -300,7 +300,7 @@ def test_header_token_too_long(tmp_path: Path) -> None: def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P6") @@ -316,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None: def test_not_enough_image_data(tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -327,7 +327,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: - path = str(tmp_path / "temp.ppm") + path = tmp_path / "temp.ppm" with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -350,7 +350,7 @@ def test_neg_ppm() -> None: def test_mimetypes(tmp_path: Path) -> None: - path = str(tmp_path / "temp.pgm") + path = tmp_path / "temp.pgm" with open(path, "wb") as f: f.write(b"P4\n128 128\n255") diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index e13a8019e..7d34fa4b5 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -73,11 +73,11 @@ def test_invalid_file() -> None: def test_write(tmp_path: Path) -> None: def roundtrip(img: Image.Image) -> None: - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" img.save(out, format="sgi") assert_image_equal_tofile(img, out) - out = str(tmp_path / "fp.sgi") + out = tmp_path / "fp.sgi" with open(out, "wb") as fp: img.save(fp) assert_image_equal_tofile(img, out) @@ -95,7 +95,7 @@ def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" im.save(out, format="sgi", bpc=2) assert_image_equal_tofile(im, out) @@ -103,7 +103,7 @@ def test_write16(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") - out = str(tmp_path / "temp.sgi") + out = tmp_path / "temp.sgi" with pytest.raises(ValueError): im.save(out, format="sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index cdb7b3e0b..b64a629f5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -51,7 +51,7 @@ def test_context_manager() -> None: def test_save(tmp_path: Path) -> None: # Arrange - temp = str(tmp_path / "temp.spider") + temp = tmp_path / "temp.spider" im = hopper() # Act diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index b6396bd64..f7b14beab 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -24,7 +24,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) def test_sanity(mode: str, tmp_path: Path) -> None: def roundtrip(original_im: Image.Image) -> None: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" original_im.save(out, rle=rle) with Image.open(out) as saved_im: @@ -76,7 +76,7 @@ def test_palette_depth_16(tmp_path: Path) -> None: assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" im.save(out) with Image.open(out) as reloaded: assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png") @@ -122,7 +122,7 @@ def test_cross_scan_line() -> None: def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -141,7 +141,7 @@ def test_small_palette(tmp_path: Path) -> None: colors = [0, 0, 0] im.putpalette(colors) - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as reloaded: @@ -155,7 +155,7 @@ def test_missing_palette() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with pytest.raises(OSError): im.save(out) @@ -172,7 +172,7 @@ def test_save_mapdepth() -> None: def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Check there is no id section im.save(out) @@ -202,7 +202,7 @@ def test_save_id_section(tmp_path: Path) -> None: def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" with Image.open(test_file) as im: assert im.info["orientation"] == -1 @@ -229,7 +229,7 @@ def test_save_rle(tmp_path: Path) -> None: with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" # Save im.save(out) @@ -266,7 +266,7 @@ def test_save_l_transparency(tmp_path: Path) -> None: assert im.mode == "LA" assert im.getchannel("A").getcolors()[0][0] == num_transparent - out = str(tmp_path / "temp.tga") + out = tmp_path / "temp.tga" im.save(out) with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e98e55aca..6962a5c98 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -31,7 +31,7 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" hopper("RGB").save(filename) @@ -112,11 +112,11 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) def test_bigtiff_save(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper() im.save(outfile, big_tiff=True) @@ -185,7 +185,7 @@ class TestFileTiff: assert im.info["dpi"] == (dpi, dpi) def test_save_float_dpi(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) im.save(outfile, dpi=dpi) @@ -220,12 +220,12 @@ class TestFileTiff: def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with pytest.raises(OSError): im.save(outfile) @@ -485,14 +485,14 @@ class TestFileTiff: assert gps[0] == b"\x03\x02\x00\x00" assert gps[18] == "WGS-84" - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() check_exif(exif) im.save(outfile, exif=exif) - outfile2 = str(tmp_path / "temp2.tif") + outfile2 = tmp_path / "temp2.tif" with Image.open(outfile) as im: exif = im.getexif() check_exif(exif) @@ -504,7 +504,7 @@ class TestFileTiff: check_exif(exif) def test_modify_exif(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() exif[264] = 100 @@ -533,7 +533,7 @@ class TestFileTiff: @pytest.mark.parametrize("mode", ("1", "L")) def test_photometric(self, mode: str, tmp_path: Path) -> None: - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: @@ -612,7 +612,7 @@ class TestFileTiff: def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} - filename = str(tmp_path / "temp.tif") + filename = tmp_path / "temp.tif" hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: # legacy interface @@ -630,14 +630,14 @@ class TestFileTiff: with Image.open(infile) as im: assert im.getpixel((0, 0)) == pixel_value - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile) assert_image_equal_tofile(im, tmpfile) def test_iptc(self, tmp_path: Path) -> None: # Do not preserve IPTC_NAA_CHUNK by default if type is LONG - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/hopper.tif") as im: im.load() assert isinstance(im, TiffImagePlugin.TiffImageFile) @@ -652,7 +652,7 @@ class TestFileTiff: assert 33723 not in im.tag_v2 def test_rowsperstrip(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper() im.save(outfile, tiffinfo={278: 256}) @@ -703,7 +703,7 @@ class TestFileTiff: with Image.open(infile) as im: assert im._planar_configuration == 2 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) with Image.open(outfile) as reloaded: @@ -718,7 +718,7 @@ class TestFileTiff: @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im = hopper(mode) im.save(outfile) @@ -812,7 +812,7 @@ class TestFileTiff: im.info["icc_profile"] = "Dummy value" # Try save-load round trip to make sure both handle icc_profile. - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im.save(tmpfile, "TIFF", compression="raw") with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] @@ -821,7 +821,7 @@ class TestFileTiff: im = hopper() assert "icc_profile" not in im.info - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" icc_profile = b"Dummy value" im.save(outfile, icc_profile=icc_profile) @@ -832,11 +832,11 @@ class TestFileTiff: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" im.save(outfile) def test_discard_icc_profile(self, tmp_path: Path) -> None: - outfile = str(tmp_path / "temp.tif") + outfile = tmp_path / "temp.tif" with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info @@ -889,7 +889,7 @@ class TestFileTiff: ] def test_tiff_chunks(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" im = hopper() with open(tmpfile, "wb") as fp: @@ -911,7 +911,7 @@ class TestFileTiff: def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -923,7 +923,7 @@ class TestFileTiff: assert fp.closed def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) @@ -974,7 +974,7 @@ class TestFileTiff: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: def test_fd_leak(self, tmp_path: Path) -> None: - tmpfile = str(tmp_path / "temp.tif") + tmpfile = tmp_path / "temp.tif" # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 36aabf4f8..0734d1db1 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -56,7 +56,7 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageDescription] = text_data - f = str(tmp_path / "temp.tif") + f = tmp_path / "temp.tif" img.save(f, tiffinfo=info) @@ -128,7 +128,7 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: - f = str(tmp_path / "temp.tiff") + f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -163,7 +163,7 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 del info[278] @@ -210,7 +210,7 @@ def test_no_duplicate_50741_tag() -> None: def test_iptc(tmp_path: Path) -> None: - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @@ -227,7 +227,7 @@ def test_writing_other_types_to_ascii( info[271] = value im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -244,7 +244,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) info[700] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -263,7 +263,7 @@ def test_writing_other_types_to_undefined( info[33723] = value - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info) with Image.open(out) as reloaded: @@ -296,7 +296,7 @@ def test_empty_metadata() -> None: def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.iccprofile.tif") as im: im.save(out) @@ -317,13 +317,13 @@ def test_iccprofile_binary() -> None: def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: - outfile = str(tmp_path / "temp.png") + outfile = tmp_path / "temp.png" im.save(outfile) @@ -332,7 +332,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -351,7 +351,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -363,7 +363,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: info[41493] = TiffImagePlugin.IFDRational(numerator, 1) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -381,7 +381,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -393,7 +393,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -406,7 +406,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -420,7 +420,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: info[37000] = -60000 - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: @@ -446,7 +446,7 @@ def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: assert len(reloaded.tag_v2[34377]) == 70 @@ -480,7 +480,7 @@ def test_tag_group_data() -> None: def test_empty_subifd(tmp_path: Path) -> None: - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" im = hopper() exif = im.getexif() diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c88fe3589..c573390c4 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" # temp_file = "temp.webp" pil_image = hopper("RGBA") @@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None: Does it have the bits we expect? """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) @@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: half_transparent_image.putalpha(new_alpha) # save with transparent area preserved - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" half_transparent_image.save(temp_file, exact=True, lossless=True) with Image.open(temp_file) as reloaded: @@ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: should work, and be similar to the original file. """ - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" file_path = "Tests/images/transparent.gif" with Image.open(file_path) as im: im.save(temp_file) @@ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" im.save(out) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" im.save(out_quality, alpha_quality=50) with Image.open(out) as reloaded: with Image.open(out_quality) as reloaded_quality: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 967a0aae8..d4b1fda97 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -39,7 +39,7 @@ def test_write_animation_L(tmp_path: Path) -> None: with Image.open("Tests/images/iss634.gif") as orig: assert orig.n_frames > 1 - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: assert im.n_frames == orig.n_frames @@ -67,7 +67,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: are visually similar to the originals. """ - def check(temp_file: str) -> None: + def check(temp_file: Path) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -87,7 +87,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: - temp_file1 = str(tmp_path / "temp.webp") + temp_file1 = tmp_path / "temp.webp" frame1.copy().save( temp_file1, save_all=True, append_images=[frame2], lossless=True ) @@ -99,7 +99,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: ) -> Generator[Image.Image, None, None]: yield from ims - temp_file2 = str(tmp_path / "temp_generator.webp") + temp_file2 = tmp_path / "temp_generator.webp" frame1.copy().save( temp_file2, save_all=True, @@ -116,7 +116,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: """ durations = [0, 10, 20, 30, 40] - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( @@ -141,7 +141,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: def test_float_duration(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -159,7 +159,7 @@ def test_seeking(tmp_path: Path) -> None: """ dur = 33 - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( @@ -196,10 +196,10 @@ def test_alpha_quality(tmp_path: Path) -> None: with Image.open("Tests/images/transparent.png") as im: first_frame = Image.new("L", im.size) - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" first_frame.save(out, save_all=True, append_images=[im]) - out_quality = str(tmp_path / "quality.webp") + out_quality = tmp_path / "quality.webp" first_frame.save( out_quality, save_all=True, append_images=[im], alpha_quality=50 ) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 80429715e..5eaa4f599 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,7 +13,7 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c68a20d7a..d1d3421ec 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -146,7 +146,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None: exif_data = b"" xmp_data = b"" - temp_file = str(tmp_path / "temp.webp") + temp_file = tmp_path / "temp.webp" with Image.open("Tests/images/anim_frame1.webp") as frame1: with Image.open("Tests/images/anim_frame2.webp") as frame2: frame1.save( diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 97469b77e..07c945848 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -59,7 +59,7 @@ def test_register_handler(tmp_path: Path) -> None: WmfImagePlugin.register_handler(handler) im = hopper() - tmpfile = str(tmp_path / "temp.wmf") + tmpfile = tmp_path / "temp.wmf" im.save(tmpfile) assert handler.methodCalled @@ -93,6 +93,6 @@ def test_load_set_dpi() -> None: def test_save(ext: str, tmp_path: Path) -> None: im = hopper() - tmpfile = str(tmp_path / ("temp" + ext)) + tmpfile = tmp_path / ("temp" + ext) with pytest.raises(OSError): im.save(tmpfile) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 44dd2541f..154f3dcc0 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -73,7 +73,7 @@ def test_invalid_file() -> None: def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" with pytest.raises(OSError): im.save(out) @@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None: def test_hotspot(tmp_path: Path) -> None: im = hopper("1") - out = str(tmp_path / "temp.xbm") + out = tmp_path / "temp.xbm" hotspot = (0, 7) im.save(out, hotspot=hotspot) diff --git a/Tests/test_image.py b/Tests/test_image.py index d64816b1e..7f46cb7b0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -187,14 +187,14 @@ class TestImage: for ext in (".jpg", ".jp2"): if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") - temp_file = str(tmp_path / ("temp." + ext)) + temp_file = tmp_path / ("temp." + ext) im.save(Path(temp_file)) def test_fp_name(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" class FP(io.BytesIO): - name: str + name: Path if sys.version_info >= (3, 12): from collections.abc import Buffer @@ -225,7 +225,7 @@ class TestImage: def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() - temp_file = str(tmp_path / "temp.unknown") + temp_file = tmp_path / "temp.unknown" with pytest.raises(ValueError): im.save(temp_file) @@ -245,7 +245,7 @@ class TestImage: reason="Test requires opening an mmaped file for writing", ) def test_readonly_save(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.bmp") + temp_file = tmp_path / "temp.bmp" shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) with Image.open(temp_file) as im: @@ -728,7 +728,7 @@ class TestImage: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" # Act/Assert with Image.open(test_file) as im: @@ -738,7 +738,7 @@ class TestImage: im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: - temp_file = str(tmp_path / "temp.jpg") + temp_file = tmp_path / "temp.jpg" im = Image.new("RGB", (0, 0)) with pytest.raises(ValueError): @@ -805,7 +805,7 @@ class TestImage: assert exif[296] == 2 assert exif[11] == "gThumb 3.0.1" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[274] del exif[282] @@ -827,7 +827,7 @@ class TestImage: assert exif[274] == 1 assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" - out = str(tmp_path / "temp.jpg") + out = tmp_path / "temp.jpg" exif[258] = 8 del exif[306] exif[274] = 455 @@ -846,7 +846,7 @@ class TestImage: exif = im.getexif() assert exif == {} - out = str(tmp_path / "temp.webp") + out = tmp_path / "temp.webp" exif[258] = 8 exif[40963] = 455 exif[305] = "Pillow test" @@ -868,7 +868,7 @@ class TestImage: exif = im.getexif() assert exif == {274: 1} - out = str(tmp_path / "temp.png") + out = tmp_path / "temp.png" exif[258] = 8 del exif[274] exif[40963] = 455 diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 5f8b35c79..7d4f78c23 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == 0 @@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_la = im.convert("LA") assert "transparency" not in im_la.info @@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) - f = str(tmp_path / "temp.png") + f = tmp_path / "temp.png" im_l = im.convert("L") assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 1166371b8..f700d20c0 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -171,7 +171,7 @@ class TestImagingCoreResize: # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. im = hopper().resize((64, 64)) - temp_file = str(tmp_path / "temp.gif") + temp_file = tmp_path / "temp.gif" im.save(temp_file) with Image.open(temp_file) as reloaded: diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 3385f81f5..43068535e 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None: def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): - test_file = str(tmp_path / "temp.png") + test_file = tmp_path / "temp.png" else: - test_file = str(tmp_path / "temp.pcx") + test_file = tmp_path / "temp.pcx" def split_open(mode: str) -> int: hopper(mode).save(test_file) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4cce8f180..69533c2f8 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None: def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + tempfile = tmp_path / ("temp_" + chr(128) + ".ttf") try: shutil.copy(FONT_PATH, tempfile) except UnicodeEncodeError: diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 26b287bb4..da9e71692 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -10,7 +10,7 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path: Path) -> None: - test_file = str(tmp_path / "temp.im") + test_file = tmp_path / "temp.im" im = hopper("RGB") im.save(test_file) diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c23a5c690..e8468e59f 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -88,7 +88,7 @@ if is_win32(): def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size - opath = str(tmp_path / "temp.png") + opath = tmp_path / "temp.png" imdib = ImageWin.Dib(im) hdr = BITMAPINFOHEADER() diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index e26f5d283..b78b7984f 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) verify(im_out) # transform - filename = str(tmp_path / "temp.im") + filename = tmp_path / "temp.im" im_in.save(filename) with Image.open(filename) as im_out: diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 05c41a802..70661ecc1 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -18,7 +18,7 @@ def helper_pickle_file( ) -> None: # Arrange with Image.open(test_file) as im: - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" if mode: im = im.convert(mode) @@ -87,7 +87,7 @@ def test_pickle_jpeg() -> None: def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" with Image.open("Tests/images/hopper.jpg") as im: im = im.convert("PA") @@ -151,7 +151,7 @@ def test_pickle_font_string(protocol: int) -> None: def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - filename = str(tmp_path / "temp.pkl") + filename = tmp_path / "temp.pkl" # Act: roundtrip with open(filename, "wb") as f: diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index a743d831f..78f5632c5 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -35,7 +35,7 @@ def test_draw_postscript(tmp_path: Path) -> None: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript # Arrange - tempfile = str(tmp_path / "temp.ps") + tempfile = tmp_path / "temp.ps" with open(tempfile, "wb") as fp: # Act ps = PSDraw.PSDraw(fp) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index dd4fc46c3..4fd3aab5d 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -35,7 +35,7 @@ class TestShellInjection: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: - src_file = str(tmp_path / filename) + src_file = tmp_path / filename shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 13f1f9c80..30dc73654 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -65,7 +65,7 @@ def test_ifd_rational_save( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: im = hopper() - out = str(tmp_path / "temp.tiff") + out = tmp_path / "temp.tiff" res = IFDRational(301, 1) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff) From acd8548f6e38b48c2af02d5081584472535afd52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 22:36:59 +1100 Subject: [PATCH 06/35] Removed FIXME --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 884659882..e2a76dfe1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -548,7 +548,6 @@ class Image: def __init__(self) -> None: # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? self._im: core.ImagingCore | DeferredError | None = None self._mode = "" self._size = (0, 0) From 0888dc02ac9700acdaf5d07691203fb7e57a538a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 23:10:09 +1100 Subject: [PATCH 07/35] Allow for two header fields and a comment --- Tests/images/full_gimp_palette.gpl | 260 +++++++++++++++++++++++++++++ Tests/test_file_gimppalette.py | 22 ++- src/PIL/GimpPaletteFile.py | 4 +- 3 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 Tests/images/full_gimp_palette.gpl diff --git a/Tests/images/full_gimp_palette.gpl b/Tests/images/full_gimp_palette.gpl new file mode 100644 index 000000000..004217210 --- /dev/null +++ b/Tests/images/full_gimp_palette.gpl @@ -0,0 +1,260 @@ +GIMP Palette +Name: fullpalette +Columns: 4 +# + 0 0 0 Index 0 + 1 1 1 Index 1 + 2 2 2 Index 2 + 3 3 3 Index 3 + 4 4 4 Index 4 + 5 5 5 Index 5 + 6 6 6 Index 6 + 7 7 7 Index 7 + 8 8 8 Index 8 + 9 9 9 Index 9 + 10 10 10 Index 10 + 11 11 11 Index 11 + 12 12 12 Index 12 + 13 13 13 Index 13 + 14 14 14 Index 14 + 15 15 15 Index 15 + 16 16 16 Index 16 + 17 17 17 Index 17 + 18 18 18 Index 18 + 19 19 19 Index 19 + 20 20 20 Index 20 + 21 21 21 Index 21 + 22 22 22 Index 22 + 23 23 23 Index 23 + 24 24 24 Index 24 + 25 25 25 Index 25 + 26 26 26 Index 26 + 27 27 27 Index 27 + 28 28 28 Index 28 + 29 29 29 Index 29 + 30 30 30 Index 30 + 31 31 31 Index 31 + 32 32 32 Index 32 + 33 33 33 Index 33 + 34 34 34 Index 34 + 35 35 35 Index 35 + 36 36 36 Index 36 + 37 37 37 Index 37 + 38 38 38 Index 38 + 39 39 39 Index 39 + 40 40 40 Index 40 + 41 41 41 Index 41 + 42 42 42 Index 42 + 43 43 43 Index 43 + 44 44 44 Index 44 + 45 45 45 Index 45 + 46 46 46 Index 46 + 47 47 47 Index 47 + 48 48 48 Index 48 + 49 49 49 Index 49 + 50 50 50 Index 50 + 51 51 51 Index 51 + 52 52 52 Index 52 + 53 53 53 Index 53 + 54 54 54 Index 54 + 55 55 55 Index 55 + 56 56 56 Index 56 + 57 57 57 Index 57 + 58 58 58 Index 58 + 59 59 59 Index 59 + 60 60 60 Index 60 + 61 61 61 Index 61 + 62 62 62 Index 62 + 63 63 63 Index 63 + 64 64 64 Index 64 + 65 65 65 Index 65 + 66 66 66 Index 66 + 67 67 67 Index 67 + 68 68 68 Index 68 + 69 69 69 Index 69 + 70 70 70 Index 70 + 71 71 71 Index 71 + 72 72 72 Index 72 + 73 73 73 Index 73 + 74 74 74 Index 74 + 75 75 75 Index 75 + 76 76 76 Index 76 + 77 77 77 Index 77 + 78 78 78 Index 78 + 79 79 79 Index 79 + 80 80 80 Index 80 + 81 81 81 Index 81 + 82 82 82 Index 82 + 83 83 83 Index 83 + 84 84 84 Index 84 + 85 85 85 Index 85 + 86 86 86 Index 86 + 87 87 87 Index 87 + 88 88 88 Index 88 + 89 89 89 Index 89 + 90 90 90 Index 90 + 91 91 91 Index 91 + 92 92 92 Index 92 + 93 93 93 Index 93 + 94 94 94 Index 94 + 95 95 95 Index 95 + 96 96 96 Index 96 + 97 97 97 Index 97 + 98 98 98 Index 98 + 99 99 99 Index 99 +100 100 100 Index 100 +101 101 101 Index 101 +102 102 102 Index 102 +103 103 103 Index 103 +104 104 104 Index 104 +105 105 105 Index 105 +106 106 106 Index 106 +107 107 107 Index 107 +108 108 108 Index 108 +109 109 109 Index 109 +110 110 110 Index 110 +111 111 111 Index 111 +112 112 112 Index 112 +113 113 113 Index 113 +114 114 114 Index 114 +115 115 115 Index 115 +116 116 116 Index 116 +117 117 117 Index 117 +118 118 118 Index 118 +119 119 119 Index 119 +120 120 120 Index 120 +121 121 121 Index 121 +122 122 122 Index 122 +123 123 123 Index 123 +124 124 124 Index 124 +125 125 125 Index 125 +126 126 126 Index 126 +127 127 127 Index 127 +128 128 128 Index 128 +129 129 129 Index 129 +130 130 130 Index 130 +131 131 131 Index 131 +132 132 132 Index 132 +133 133 133 Index 133 +134 134 134 Index 134 +135 135 135 Index 135 +136 136 136 Index 136 +137 137 137 Index 137 +138 138 138 Index 138 +139 139 139 Index 139 +140 140 140 Index 140 +141 141 141 Index 141 +142 142 142 Index 142 +143 143 143 Index 143 +144 144 144 Index 144 +145 145 145 Index 145 +146 146 146 Index 146 +147 147 147 Index 147 +148 148 148 Index 148 +149 149 149 Index 149 +150 150 150 Index 150 +151 151 151 Index 151 +152 152 152 Index 152 +153 153 153 Index 153 +154 154 154 Index 154 +155 155 155 Index 155 +156 156 156 Index 156 +157 157 157 Index 157 +158 158 158 Index 158 +159 159 159 Index 159 +160 160 160 Index 160 +161 161 161 Index 161 +162 162 162 Index 162 +163 163 163 Index 163 +164 164 164 Index 164 +165 165 165 Index 165 +166 166 166 Index 166 +167 167 167 Index 167 +168 168 168 Index 168 +169 169 169 Index 169 +170 170 170 Index 170 +171 171 171 Index 171 +172 172 172 Index 172 +173 173 173 Index 173 +174 174 174 Index 174 +175 175 175 Index 175 +176 176 176 Index 176 +177 177 177 Index 177 +178 178 178 Index 178 +179 179 179 Index 179 +180 180 180 Index 180 +181 181 181 Index 181 +182 182 182 Index 182 +183 183 183 Index 183 +184 184 184 Index 184 +185 185 185 Index 185 +186 186 186 Index 186 +187 187 187 Index 187 +188 188 188 Index 188 +189 189 189 Index 189 +190 190 190 Index 190 +191 191 191 Index 191 +192 192 192 Index 192 +193 193 193 Index 193 +194 194 194 Index 194 +195 195 195 Index 195 +196 196 196 Index 196 +197 197 197 Index 197 +198 198 198 Index 198 +199 199 199 Index 199 +200 200 200 Index 200 +201 201 201 Index 201 +202 202 202 Index 202 +203 203 203 Index 203 +204 204 204 Index 204 +205 205 205 Index 205 +206 206 206 Index 206 +207 207 207 Index 207 +208 208 208 Index 208 +209 209 209 Index 209 +210 210 210 Index 210 +211 211 211 Index 211 +212 212 212 Index 212 +213 213 213 Index 213 +214 214 214 Index 214 +215 215 215 Index 215 +216 216 216 Index 216 +217 217 217 Index 217 +218 218 218 Index 218 +219 219 219 Index 219 +220 220 220 Index 220 +221 221 221 Index 221 +222 222 222 Index 222 +223 223 223 Index 223 +224 224 224 Index 224 +225 225 225 Index 225 +226 226 226 Index 226 +227 227 227 Index 227 +228 228 228 Index 228 +229 229 229 Index 229 +230 230 230 Index 230 +231 231 231 Index 231 +232 232 232 Index 232 +233 233 233 Index 233 +234 234 234 Index 234 +235 235 235 Index 235 +236 236 236 Index 236 +237 237 237 Index 237 +238 238 238 Index 238 +239 239 239 Index 239 +240 240 240 Index 240 +241 241 241 Index 241 +242 242 242 Index 242 +243 243 243 Index 243 +244 244 244 Index 244 +245 245 245 Index 245 +246 246 246 Index 246 +247 247 247 Index 247 +248 248 248 Index 248 +249 249 249 Index 249 +250 250 250 Index 250 +251 251 251 Index 251 +252 252 252 Index 252 +253 253 253 Index 253 +254 254 254 Index 254 +255 255 255 Index 255 diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index ff9cc91c5..c122b37b3 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + import pytest from PIL.GimpPaletteFile import GimpPaletteFile @@ -22,9 +24,12 @@ def test_sanity() -> None: GimpPaletteFile(fp) -def test_get_palette() -> None: +@pytest.mark.parametrize( + "filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256)) +) +def test_get_palette(filename: str, size: int) -> None: # Arrange - with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + with open("Tests/images/" + filename, "rb") as fp: palette_file = GimpPaletteFile(fp) # Act @@ -32,4 +37,15 @@ def test_get_palette() -> None: # Assert assert mode == "RGB" - assert len(palette) / 3 == 8 + assert len(palette) / 3 == size + + +def test_palette_limit() -> None: + with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: + data = fp.read() + + # Test that __init__ only reads 256 entries + data = data.replace(b"#\n", b"") + b" 0 0 0 Index 256" + b = BytesIO(data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 256 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 0f079f457..a87487209 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -30,7 +30,7 @@ class GimpPaletteFile: raise SyntaxError(msg) palette: list[int] = [] - for _ in range(256): + for _ in range(256 + 3): s = fp.readline() if not s: break @@ -48,6 +48,8 @@ class GimpPaletteFile: raise ValueError(msg) palette += (int(v[i]) for i in range(3)) + if len(palette) == 768: + break self.palette = bytes(palette) From 510bc055774291d2380ea5d8e25faa84c7dfc637 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Mar 2025 23:12:35 +1100 Subject: [PATCH 08/35] Added frombytes() to allow for unlimited parsing --- Tests/test_file_gimppalette.py | 24 +++++++++++++++++++----- src/PIL/GimpPaletteFile.py | 23 +++++++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index c122b37b3..c3d2bfeb7 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -16,11 +16,11 @@ def test_sanity() -> None: GimpPaletteFile(fp) with open("Tests/images/bad_palette_file.gpl", "rb") as fp: - with pytest.raises(SyntaxError): + with pytest.raises(SyntaxError, match="bad palette file"): GimpPaletteFile(fp) with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bad palette entry"): GimpPaletteFile(fp) @@ -40,12 +40,26 @@ def test_get_palette(filename: str, size: int) -> None: assert len(palette) / 3 == size -def test_palette_limit() -> None: +def test_frombytes() -> None: with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: - data = fp.read() + full_data = fp.read() # Test that __init__ only reads 256 entries - data = data.replace(b"#\n", b"") + b" 0 0 0 Index 256" + data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256" b = BytesIO(data) palette = GimpPaletteFile(b) assert len(palette.palette) / 3 == 256 + + # Test that frombytes() can read beyond that + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 257 + + # Test that __init__ raises an error if a comment is too long + data = full_data[:-1] + b"a" * 100 + b = BytesIO(data) + with pytest.raises(SyntaxError, match="bad palette file"): + palette = GimpPaletteFile(b) + + # Test that frombytes() can read the data regardless + palette = GimpPaletteFile.frombytes(data) + assert len(palette.palette) / 3 == 256 diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index a87487209..379ffd739 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -16,6 +16,7 @@ from __future__ import annotations import re +from io import BytesIO from typing import IO @@ -24,13 +25,18 @@ class GimpPaletteFile: rawmode = "RGB" - def __init__(self, fp: IO[bytes]) -> None: + def _read(self, fp: IO[bytes], limit: bool = True) -> None: if not fp.readline().startswith(b"GIMP Palette"): msg = "not a GIMP palette file" raise SyntaxError(msg) palette: list[int] = [] - for _ in range(256 + 3): + i = 0 + while True: + if limit and i == 256 + 3: + break + + i += 1 s = fp.readline() if not s: break @@ -38,7 +44,7 @@ class GimpPaletteFile: # skip fields and comment lines if re.match(rb"\w+:|#", s): continue - if len(s) > 100: + if limit and len(s) > 100: msg = "bad palette file" raise SyntaxError(msg) @@ -48,10 +54,19 @@ class GimpPaletteFile: raise ValueError(msg) palette += (int(v[i]) for i in range(3)) - if len(palette) == 768: + if limit and len(palette) == 768: break self.palette = bytes(palette) + def __init__(self, fp: IO[bytes]) -> None: + self._read(fp) + + @classmethod + def frombytes(cls, data: bytes) -> GimpPaletteFile: + self = cls.__new__(cls) + self._read(BytesIO(data), False) + return self + def getpalette(self) -> tuple[bytes, str]: return self.palette, self.rawmode From 21ff960c9c796892e5e13fa8ca2d382565141539 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 08:51:41 +1100 Subject: [PATCH 09/35] Test that an unlimited number of lines is not read by __init__ --- Tests/test_file_gimppalette.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index c3d2bfeb7..08862113b 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -41,10 +41,17 @@ def test_get_palette(filename: str, size: int) -> None: def test_frombytes() -> None: - with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: - full_data = fp.read() + # Test that __init__ stops reading after 260 lines + with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: + custom_data = fp.read() + custom_data += b"#\n" * 300 + b" 0 0 0 Index 12" + b = BytesIO(custom_data) + palette = GimpPaletteFile(b) + assert len(palette.palette) / 3 == 8 # Test that __init__ only reads 256 entries + with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: + full_data = fp.read() data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256" b = BytesIO(data) palette = GimpPaletteFile(b) From 8d440f734bbf1caac75cd3f94dcd737e401a21b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:39:36 +1100 Subject: [PATCH 10/35] Removed unused argument --- Tests/test_file_gif.py | 2 +- Tests/test_file_tga.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index fb1a636ed..ba0963e8c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1242,7 +1242,7 @@ def test_rgba_transparency(tmp_path: Path) -> None: assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path: Path) -> None: +def test_background_outside_palettte() -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index f7b14beab..a10d4d7ab 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -65,7 +65,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: roundtrip(original_im) -def test_palette_depth_8(tmp_path: Path) -> None: +def test_palette_depth_8() -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/p_8.tga") From 8d5505487766b3e1c31e90c41599fb84de694174 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:41:15 +1100 Subject: [PATCH 11/35] Reuse temp path --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index f99ca91a3..c969bd502 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -672,7 +672,7 @@ class TestFilePng: im.putpalette((1, 1, 1)) out = tmp_path / "temp.png" - im.save(tmp_path / "temp.png") + im.save(out) with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 From 9334bf040ef35f31c6b388e95d91f8fa8dcfc220 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:41:52 +1100 Subject: [PATCH 12/35] Do not cast unnecessarily --- Tests/test_image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7f46cb7b0..d13e47602 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -187,8 +187,7 @@ class TestImage: for ext in (".jpg", ".jp2"): if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") - temp_file = tmp_path / ("temp." + ext) - im.save(Path(temp_file)) + im.save(tmp_path / ("temp." + ext)) def test_fp_name(self, tmp_path: Path) -> None: temp_file = tmp_path / "temp.jpg" From c7e3158d515fe339f67e1b0313105ae9b88efa42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Mar 2025 20:47:38 +1100 Subject: [PATCH 13/35] Added explicit test for opening and saving image with string --- Tests/test_image.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index d13e47602..f18d8489c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -175,6 +175,13 @@ class TestImage: with Image.open(io.StringIO()): # type: ignore[arg-type] pass + def test_string(self, tmp_path: Path) -> None: + out = str(tmp_path / "temp.png") + im = hopper() + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) + def test_pathlib(self, tmp_path: Path) -> None: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" From bca693bd82ce1dab40a375d101c5292e3a275143 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:33:45 +1100 Subject: [PATCH 14/35] Updated harfbuzz to 11.0.0 (#8830) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index e9c54536e..51ba0cf28 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=10.4.0 +HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ea3d99394..2e9e18719 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.4.0", + "HARFBUZZ": "11.0.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.17", "LIBIMAGEQUANT": "4.3.4", From 053b5790e13030b9ac13a70489f48453dab7cb8f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:22:21 +1100 Subject: [PATCH 15/35] Added media_white_point (#8829) Co-authored-by: Andrew Murray --- docs/reference/ImageCms.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 96bd14dd3..4a1f5a3ee 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -286,6 +286,14 @@ can be easily displayed in a chromaticity diagram, for example). The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point + :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None + + This tag specifies the media white point and is used for + generating absolute colorimetry. + + The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. + .. py:attribute:: media_white_point_temperature :type: float | None From 14d495a519a4c444c3f3ef369c21ad45551f689b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:41:03 +0000 Subject: [PATCH 16/35] Update dependency cibuildwheel to v2.23.2 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index f2109ed61..db5f89c9a 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.23.1 +cibuildwheel==2.23.2 From b8abded99b4f77db591e270836156349a074c3bc Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:31:49 +1100 Subject: [PATCH 17/35] Change back to actions/setup-python (#8833) Co-authored-by: Andrew Murray --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4ad88be9..006d574f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: Quansight-Labs/setup-python@v5 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true From 295a5e9bd7fe021df6ea3aa41d54738dcd3f8250 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 19:23:04 +1100 Subject: [PATCH 18/35] Do not convert BC1 LUT to UINT32 --- src/libImaging/BcnDecode.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 9a41febc7..7b3d8f908 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -25,7 +25,6 @@ typedef struct { typedef struct { UINT16 c0, c1; - UINT32 lut; } bc1_color; typedef struct { @@ -40,13 +39,10 @@ typedef struct { #define LOAD16(p) (p)[0] | ((p)[1] << 8) -#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) - static void bc1_color_load(bc1_color *dst, const UINT8 *src) { dst->c0 = LOAD16(src); dst->c1 = LOAD16(src + 2); - dst->lut = LOAD32(src + 4); } static rgba @@ -70,7 +66,7 @@ static void decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; - int n, cw; + int n, o, cw; UINT16 r0, g0, b0, r1, g1, b1; bc1_color_load(&col, src); @@ -103,9 +99,11 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { p[3].b = 0; p[3].a = 0; } - for (n = 0; n < 16; n++) { - cw = 3 & (col.lut >> (2 * n)); - dst[n] = p[cw]; + for (n = 0; n < 4; n++) { + for (o = 0; o < 4; o++) { + cw = 3 & ((src + 4)[n] >> (2 * o)); + dst[n * 4 + o] = p[cw]; + } } } From e1f0def839776c131a8f297c7b05712d1cfbee75 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:43:07 +1100 Subject: [PATCH 19/35] Updated xz to 5.8.0, except on manylinux2014 (#8836) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 51ba0cf28..461729a74 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,11 @@ HARFBUZZ_VERSION=11.0.0 LIBPNG_VERSION=1.6.47 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 -XZ_VERSION=5.6.4 +if [[ $MB_ML_VER == 2014 ]]; then + XZ_VERSION=5.6.4 +else + XZ_VERSION=5.8.0 +fi TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_NG_VERSION=2.2.4 From 3c185d1f696dfbe20a5401a2f36edd392b2094d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:44:27 +1100 Subject: [PATCH 20/35] Do not load image during save if file extension is unknown (#8835) Co-authored-by: Andrew Murray --- Tests/test_file_hdf5stub.py | 2 +- src/PIL/Image.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 50864009f..e4f09a09c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -43,7 +43,7 @@ def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = BytesIO() - dummy_filename = "dummy.filename" + dummy_filename = "dummy.h5" # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e2a76dfe1..d338e4dfd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2510,13 +2510,6 @@ class Image: # only set the name for metadata purposes filename = os.fspath(fp.name) - # may mutate self! - self._ensure_mutable() - - save_all = params.pop("save_all", False) - self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} - self.encoderconfig: tuple[Any, ...] = () - preinit() filename_ext = os.path.splitext(filename)[1].lower() @@ -2531,6 +2524,13 @@ class Image: msg = f"unknown file extension: {ext}" raise ValueError(msg) from e + # may mutate self! + self._ensure_mutable() + + save_all = params.pop("save_all", False) + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} + self.encoderconfig: tuple[Any, ...] = () + if format.upper() not in SAVE: init() if save_all: From 10ccbd7788532f72939d38c41f3f7303ff07b4da Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 28 Mar 2025 03:01:09 +1100 Subject: [PATCH 21/35] If append_images is populated, default save_all to True (#8781) Co-authored-by: Andrew Murray --- Tests/test_file_gif.py | 6 ++++++ docs/handbook/image-file-formats.rst | 26 ++++++++++++++------------ docs/handbook/tutorial.rst | 1 - src/PIL/Image.py | 8 ++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index ba0963e8c..376eab0c6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1138,6 +1138,12 @@ def test_append_images(tmp_path: Path) -> None: ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(out, save_all=True, append_images=ims) + with Image.open(out) as reread: + assert reread.n_frames == 3 + + # Test append_images without save_all + im.copy().save(out, append_images=ims) + with Image.open(out) as reread: assert reread.n_frames == 3 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 97599ace5..c0b1a9d4e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -235,8 +235,9 @@ following options are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) **save_all** - If present and true, all frames of the image will be saved. If - not, then only the first frame of a multiframe image will be saved. + If present and true, or if ``append_images`` is not empty, all frames of + the image will be saved. Otherwise, only the first frame of a multiframe + image will be saved. **append_images** A list of images to append as additional frames. Each of the @@ -723,8 +724,8 @@ Saving When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -option will also be available. +argument is present and true, or if ``append_images`` is not empty, all frames +will be saved. **append_images** A list of images to append as additional pictures. Each of the @@ -934,7 +935,8 @@ Saving When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` -parameter must be set to ``True``. The following parameters can also be set: +parameter should be set to ``True`` or ``append_images`` should not be empty. The +following parameters can also be set: **default_image** Boolean value, specifying whether or not the base image is a default image. @@ -1163,7 +1165,8 @@ Saving The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **save_all** - If true, Pillow will save all frames of the image to a multiframe tiff document. + If true, or if ``append_images`` is not empty, Pillow will save all frames of the + image to a multiframe tiff document. .. versionadded:: 3.4.0 @@ -1313,8 +1316,8 @@ Saving sequences When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` -argument is present and true, then all frames will be saved, and the following -options will also be available. +argument is present and true, or if ``append_images`` is not empty, all frames +will be saved, and the following options will also be available. **append_images** A list of images to append as additional frames. Each of the @@ -1616,15 +1619,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **save_all** If a multiframe image is used, by default, only the first image will be saved. To save all frames, each frame to a separate page of the PDF, the ``save_all`` - parameter must be present and set to ``True``. + parameter should be present and set to ``True`` or ``append_images`` should not be + empty. .. versionadded:: 3.0.0 **append_images** A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each - of the images in the list can be single or multiframe images. The ``save_all`` - parameter must be present and set to ``True`` in conjunction with - ``append_images``. + of the images in the list can be single or multiframe images. .. versionadded:: 4.2.0 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index f771ae7ae..f1a2849b8 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g. # Save the images as an animated GIF images[0].save( "animated_hopper.gif", - save_all=True, append_images=images[1:], duration=500, # duration of each frame in milliseconds loop=0, # loop forever diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d338e4dfd..67e068b06 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2527,13 +2527,17 @@ class Image: # may mutate self! self._ensure_mutable() - save_all = params.pop("save_all", False) + save_all = params.pop("save_all", None) self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: init() - if save_all: + if save_all or ( + save_all is None + and params.get("append_images") + and format.upper() in SAVE_ALL + ): save_handler = SAVE_ALL[format.upper()] else: save_handler = SAVE[format.upper()] From 1cb6c7c347a7509e0303c32cfe8f5d1e063be1b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 28 Mar 2025 23:27:39 +1100 Subject: [PATCH 22/35] Parametrize tests (#8838) Co-authored-by: Andrew Murray --- Tests/test_file_bmp.py | 28 ++++++++------------ Tests/test_file_sgi.py | 28 +++++++++++--------- Tests/test_file_tga.py | 60 ++++++++++++++++++++++-------------------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 64d2acaf5..8a94011e8 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -15,25 +15,19 @@ from .helper import ( ) -def test_sanity(tmp_path: Path) -> None: - def roundtrip(im: Image.Image) -> None: - outfile = tmp_path / "temp.bmp" +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_sanity(mode: str, tmp_path: Path) -> None: + outfile = tmp_path / "temp.bmp" - im.save(outfile, "BMP") + im = hopper(mode) + im.save(outfile, "BMP") - with Image.open(outfile) as reloaded: - reloaded.load() - assert im.mode == reloaded.mode - assert im.size == reloaded.size - assert reloaded.format == "BMP" - assert reloaded.get_format_mimetype() == "image/bmp" - - roundtrip(hopper()) - - roundtrip(hopper("1")) - roundtrip(hopper("L")) - roundtrip(hopper("P")) - roundtrip(hopper("RGB")) + with Image.open(outfile) as reloaded: + reloaded.load() + assert im.mode == reloaded.mode + assert im.size == reloaded.size + assert reloaded.format == "BMP" + assert reloaded.get_format_mimetype() == "image/bmp" def test_invalid_file() -> None: diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 7d34fa4b5..da0965fa1 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -71,24 +71,26 @@ def test_invalid_file() -> None: SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path: Path) -> None: - def roundtrip(img: Image.Image) -> None: - out = tmp_path / "temp.sgi" - img.save(out, format="sgi") +def roundtrip(img: Image.Image, tmp_path: Path) -> None: + out = tmp_path / "temp.sgi" + img.save(out, format="sgi") + assert_image_equal_tofile(img, out) + + out = tmp_path / "fp.sgi" + with open(out, "wb") as fp: + img.save(fp) assert_image_equal_tofile(img, out) - out = tmp_path / "fp.sgi" - with open(out, "wb") as fp: - img.save(fp) - assert_image_equal_tofile(img, out) + assert not fp.closed - assert not fp.closed - for mode in ("L", "RGB", "RGBA"): - roundtrip(hopper(mode)) +@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) +def test_write(mode: str, tmp_path: Path) -> None: + roundtrip(hopper(mode), tmp_path) - # Test 1 dimension for an L mode image - roundtrip(Image.new("L", (10, 1))) + +def test_write_L_mode_1_dimension(tmp_path: Path) -> None: + roundtrip(Image.new("L", (10, 1)), tmp_path) def test_write16(tmp_path: Path) -> None: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a10d4d7ab..8b6ed3ed2 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,8 +1,6 @@ from __future__ import annotations import os -from glob import glob -from itertools import product from pathlib import Path import pytest @@ -15,14 +13,27 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -_MODES = ("L", "LA", "P", "RGB", "RGBA") _ORIGINS = ("tl", "bl") _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -@pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode: str, tmp_path: Path) -> None: +@pytest.mark.parametrize( + "size_mode", + ( + ("1x1", "L"), + ("200x32", "L"), + ("200x32", "LA"), + ("200x32", "P"), + ("200x32", "RGB"), + ("200x32", "RGBA"), + ), +) +@pytest.mark.parametrize("origin", _ORIGINS) +@pytest.mark.parametrize("rle", (True, False)) +def test_sanity( + size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path +) -> None: def roundtrip(original_im: Image.Image) -> None: out = tmp_path / "temp.tga" @@ -36,33 +47,26 @@ def test_sanity(mode: str, tmp_path: Path) -> None: assert_image_equal(saved_im, original_im) - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + size, mode = size_mode + png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png") + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode + path_no_ext = os.path.splitext(png_path)[0] + tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == _ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert original_im.getpalette() == reference_im.getpalette() + assert_image_equal(original_im, reference_im) - assert_image_equal(original_im, reference_im) - - roundtrip(original_im) + roundtrip(original_im) def test_palette_depth_8() -> None: From ae52f9f37d2bee5ccd2ce10fb9c3d6318e675b89 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:21:51 +1100 Subject: [PATCH 23/35] Added release notes for #8781 and #8837 (#8843) Co-authored-by: Andrew Murray --- docs/releasenotes/11.2.0.rst | 37 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst index 3e977221e..d40d86f21 100644 --- a/docs/releasenotes/11.2.0.rst +++ b/docs/releasenotes/11.2.0.rst @@ -4,21 +4,12 @@ Security ======== -TODO -^^^^ +Undefined shift when loading compressed DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ +When loading some compressed DDS formats, an integer was bitshifted by 24 places to +generate the 32 bits of the lookup table. This was undefined behaviour, and has been +present since Pillow 3.4.0. Deprecations ============ @@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance. API Changes =========== -TODO -^^^^ +``append_images`` no longer requires ``save_all`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Previously, ``save_all`` was required to in order to use ``append_images``. Now, +``save_all`` will default to ``True`` if ``append_images`` is not empty and the format +supports saving multiple frames:: + + im.save("out.gif", append_images=ims) API Additions ============= @@ -73,11 +68,3 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT5, BC2, BC3 and BC5 are supported:: im.save("out.dds", pixel_format="DXT1") - -Other Changes -============= - -TODO -^^^^ - -TODO From 6d42449788e4c05a76cc7c9c81a7c8b2a40d099e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:25:13 +1100 Subject: [PATCH 24/35] Allow loading of EMF images at a given DPI (#8536) Co-authored-by: Andrew Murray --- Tests/images/drawing_emf_ref_72_144.png | Bin 0 -> 984 bytes Tests/test_file_wmf.py | 14 ++++++++++++++ src/PIL/WmfImagePlugin.py | 24 ++++++++++++++---------- 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 Tests/images/drawing_emf_ref_72_144.png diff --git a/Tests/images/drawing_emf_ref_72_144.png b/Tests/images/drawing_emf_ref_72_144.png new file mode 100644 index 0000000000000000000000000000000000000000..000377b6c7b1e05688fc17d5da09d163a18c5b4b GIT binary patch literal 984 zcmV;}11J26P) z+m?eM3`K+g|If}tT8An^fFxX!{W??4_QVUcOTu}cFoF=ms9ghq-o8T!`G3$n3L4r; z;S(Tv7 z*g4bg5BZ5u>}=ZTECjnbkG7~Y!fVc;t>BC>n)hm}IU`)=UE0dd2#a~U_7G>J-@H+K zpfl2G-l#p+8R@B^MO*Hfv6kjaTC`_~8fk9zYVCQVM%pr{(;j{$OVW@;o%V#z&{S20 z_H6APQ(GHVd(QU0sJ*sPwP$ulswyOD&)nWI^g2n}^GA))>lB$n90)P+vi2$+jt~Pc zYp>GbRTQ+>iW;HRT+m)IYD&#H?G>X&ik0WISBx4dR=(Q}jL56@x*d+>>wnc=x5JTq z{odpet9ST^cZ;<4>K$IhoBYcr9ge)XB(%5haPTI##a(-=6B|hx-L);8*x*fWiy!R` zPi*Aj^`mX%#D-XV+o%EHVdv+zC0yGQuDvz4pF4cCC;yEGJ66)Z;o6pP?cIql<_Flj zjDxAPV_e%3u5Ag|wuEb2!nG~o+Lo}TeT_YBX8{;W*E8=kK*&u$uPh z0#pg#mvPzBzHn_zxV9x++Y+vA3D>rSYg^&~0E0trLup^5PB5h%oqCm9%f69=AWL)}qDpk;FvW&-2%W_7m4ewmZF(V~zdOPTrXJ z*G{sz_SAfY3qS45;(n1@>+kbrKnL=A$hI8>3{A})shuAu$f!EHj>I!TPwG& zL#U6Wa@E!;18=CRere*`4+zs%Pqp@J*S59>Y+8SNnpSTPpm8WNL*NZpvWrIT;jP}| z3_SzSf##jg&^g{7V&3lz{nHG}<}A*@GP|N?&gBeTlS(?~j5kiUxinGpzU3_C`9IR4e2CH??qha)B=@Nu*N0000 None: assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) + with Image.open("Tests/images/drawing.emf") as im: + assert im.size == (1625, 1625) + + if not hasattr(Image.core, "drawwmf"): + return + im.load(im.info["dpi"]) + assert im.size == (1625, 1625) + + with Image.open("Tests/images/drawing.emf") as im: + im.load((72, 144)) + assert im.size == (82, 164) + + assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png") + @pytest.mark.parametrize("ext", (".wmf", ".emf")) def test_save(ext: str, tmp_path: Path) -> None: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 04abd52f0..f709d026b 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -80,8 +80,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - self._inch = None - # check placable header s = self.fp.read(80) @@ -89,10 +87,11 @@ class WmfStubImageFile(ImageFile.StubImageFile): # placeable windows metafile # get units per inch - self._inch = word(s, 14) - if self._inch == 0: + inch = word(s, 14) + if inch == 0: msg = "Invalid inch" raise ValueError(msg) + self._inch: tuple[float, float] = inch, inch # get bounding box x0 = short(s, 6) @@ -103,8 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # normalize size to 72 dots per inch self.info["dpi"] = 72 size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + (x1 - x0) * self.info["dpi"] // inch, + (y1 - y0) * self.info["dpi"] // inch, ) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -138,6 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi else: self.info["dpi"] = xdpi, ydpi + self._inch = xdpi, ydpi else: msg = "Unsupported file format" @@ -153,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _load(self) -> ImageFile.StubHandler | None: return _handler - def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None: - if dpi is not None and self._inch is not None: + def load( + self, dpi: float | tuple[float, float] | None = None + ) -> Image.core.PixelAccess | None: + if dpi is not None: self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] + if not isinstance(dpi, tuple): + dpi = dpi, dpi self._size = ( - (x1 - x0) * self.info["dpi"] // self._inch, - (y1 - y0) * self.info["dpi"] // self._inch, + int((x1 - x0) * dpi[0] / self._inch[0]), + int((y1 - y0) * dpi[1] / self._inch[1]), ) return super().load() From 93cdfeb48879064314af70faf98726e09fb06ab0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:25:57 +1100 Subject: [PATCH 25/35] Prevent TIFFRGBAImageBegin from applying image orientation (#8556) Co-authored-by: Andrew Murray --- Tests/test_file_libtiff.py | 11 +++++++++++ src/libImaging/TiffDecode.c | 1 + 2 files changed, 12 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 25d1f5712..9e63e9c10 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1026,6 +1026,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + def test_old_style_jpeg_orientation(self) -> None: + with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: + data = fp.read() + + # Set EXIF Orientation to 2 + data = data[:102] + b"\x02" + data[103:] + + with Image.open(io.BytesIO(data)) as im: + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e4da9162d..9a2db95b4 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { return -1; } + img.orientation = ORIENTATION_TOPLEFT; img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; From 140e426082bfe1ec84c5d6eb34a3c47eb1d89185 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:27:00 +1100 Subject: [PATCH 26/35] Added USE_RAW_ALPHA (#8602) Co-authored-by: Andrew Murray --- Tests/test_file_bmp.py | 10 ++++++++++ src/PIL/BmpImagePlugin.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 8a94011e8..757650711 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -224,3 +224,13 @@ def test_offset() -> None: # to exclude the palette size from the pixel data offset with Image.open("Tests/images/pal8_offset.bmp") as im: assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") + + +def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"] + assert im.mode == "RGB" + + monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True) + with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: + assert im.mode == "RGBA" diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index d60ea591a..43131cfe2 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -48,6 +48,8 @@ BIT2MODE = { 32: ("RGB", "BGRX"), } +USE_RAW_ALPHA = False + def _accept(prefix: bytes) -> bool: return prefix.startswith(b"BM") @@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile): msg = "Unsupported BMP bitfields layout" raise OSError(msg) elif file_info["compression"] == self.COMPRESSIONS["RAW"]: - if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset + if file_info["bits"] == 32 and ( + header == 22 or USE_RAW_ALPHA # 32-bit .cur offset + ): raw_mode, self._mode = "BGRA", "RGBA" elif file_info["compression"] in ( self.COMPRESSIONS["RLE8"], From 6bffa3a9d4fe3d5a50c505ec0637d70cc1e8d59b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:29:02 +1100 Subject: [PATCH 27/35] Only read until the offset of the next tile (#8609) Co-authored-by: Andrew Murray --- Tests/test_imagefile.py | 20 ++++++++++++++++++++ src/PIL/ImageFile.py | 9 +++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index c60a475a3..7622eea99 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -131,6 +131,26 @@ class TestImageFile: assert_image_equal(im1, im2) + def test_tile_size(self) -> None: + with open("Tests/images/hopper.tif", "rb") as im_fp: + data = im_fp.read() + + reads = [] + + class FP(BytesIO): + def read(self, size: int | None = None) -> bytes: + reads.append(size) + return super().read(size) + + fp = FP(data) + with Image.open(fp) as im: + assert len(im.tile) == 7 + + im.load() + + # Despite multiple tiles, assert only one tile caused a read of maxblock size + assert reads.count(im.decodermaxblock) == 1 + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 1bf8a7e5f..9470a8dd7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -345,7 +345,7 @@ class ImageFile(Image.Image): self.tile, lambda tile: (tile[0], tile[1], tile[3]) ) ] - for decoder_name, extents, offset, args in self.tile: + for i, (decoder_name, extents, offset, args) in enumerate(self.tile): seek(offset) decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig @@ -358,8 +358,13 @@ class ImageFile(Image.Image): else: b = prefix while True: + read_bytes = self.decodermaxblock + if i + 1 < len(self.tile): + next_offset = self.tile[i + 1].offset + if next_offset > offset: + read_bytes = next_offset - offset try: - s = read(self.decodermaxblock) + s = read(read_bytes) except (IndexError, struct.error) as e: # truncated png/gif if LOAD_TRUNCATED_IMAGES: From 03dc994baaa98385ef313669dbfd6e5e80f86380 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:30:30 +1100 Subject: [PATCH 28/35] Check that _fp type is not DeferredError before use (#8640) --- src/PIL/DcxImagePlugin.py | 3 +++ src/PIL/FliImagePlugin.py | 3 +++ src/PIL/GifImagePlugin.py | 3 +++ src/PIL/ImImagePlugin.py | 3 +++ src/PIL/ImageFile.py | 2 +- src/PIL/MpoImagePlugin.py | 5 +++++ src/PIL/PngImagePlugin.py | 3 +++ src/PIL/PsdImagePlugin.py | 5 +++++ src/PIL/SpiderImagePlugin.py | 3 +++ src/PIL/TiffImagePlugin.py | 4 +++- 10 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73..aea661b9c 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -24,6 +24,7 @@ from __future__ import annotations from . import Image from ._binary import i32le as i32 +from ._util import DeferredError from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? @@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame self.fp = self._fp self.fp.seek(self._offset[frame]) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index b534b30ab..7c5bfeefa 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 from ._binary import i32le as i32 from ._binary import o8 +from ._util import DeferredError # # decoder @@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile): self._seek(f) def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 259e93f09..045ab1027 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -45,6 +45,7 @@ from . import ( from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -167,6 +168,8 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: # rewind self.__offset = 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 9f20b30f8..71b999678 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ import re from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._util import DeferredError # -------------------------------------------------------------------- # Standard tags @@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9470a8dd7..f5b72ee0d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -167,7 +167,7 @@ class ImageFile(Image.Image): pass def _close_fp(self): - if getattr(self, "_fp", False): + if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: self._fp.close() self._fp = DeferredError(ValueError("Operation on closed image")) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index e08f80b6b..f7393eac0 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -32,6 +32,7 @@ from . import ( TiffImagePlugin, ) from ._binary import o32le +from ._util import DeferredError def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.readonly = 1 def load_seek(self, pos: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(pos) def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.offset = self.__mpoffsets[frame] diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4fc6217e1..3e3cf6526 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -869,6 +870,8 @@ class PngImageFile(ImageFile.ImageFile): def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.dispose: _imaging.ImagingCore | None dispose_extent = None diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 0aada8a06..f49aaeeb1 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -27,6 +27,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 from ._binary import si32be as si32 +from ._util import DeferredError MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile): ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: layers = [] if self._layers_position is not None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(self._layers_position) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) layers = _layerinfo(_layer_data, self._layers_size) @@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile): def seek(self, layer: int) -> None: if not self._seek_check(layer): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex # seek to given layer (1..max) _, mode, _, tile = self.layers[layer - 1] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index b26e1a996..62fa7be03 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -40,6 +40,7 @@ import sys from typing import IO, TYPE_CHECKING, Any, cast from . import Image, ImageFile +from ._util import DeferredError def isInt(f: Any) -> int: @@ -178,6 +179,8 @@ class SpiderImageFile(ImageFile.ImageFile): raise EOFError(msg) if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.fp = self._fp self.fp.seek(self.stkoffset) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 39783f1f8..ebe599cca 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -58,7 +58,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate from ._typing import StrOrBytesPath -from ._util import is_path +from ._util import DeferredError, is_path from .TiffTags import TYPES if TYPE_CHECKING: @@ -1222,6 +1222,8 @@ class TiffImageFile(ImageFile.ImageFile): self._im = None def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp while len(self._frame_pos) <= frame: From e8a9b566036a041f7643d32bf9050ee31de8163c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:33:51 +1100 Subject: [PATCH 29/35] Improved connecting discontiguous corners (#8659) --- .../discontiguous_corners_polygon.png | Bin 533 -> 533 bytes Tests/test_imagedraw.py | 2 +- src/libImaging/Draw.c | 58 ++++++++---------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 1b58889c8f3ae45243a7509c907f1928534bcbde..8992a165758676b9ba7777347fb09a3b8c453e3d 100644 GIT binary patch delta 498 zcmV!!QU#x&NVZwfxr}B*1oAc++Wu^hDyMmFv3h`#y7( z^bX*wh={0kNqVm~ZJk<@{7}`frq38OUF0%$Ri9_ST)mw}n@*lB8`kG(v?=7-vMC9z zPn)FNOFlq0L$jBBfNY*-7kRd9wq_%Fw(Ja(TpUFHEepqypH5182bmeUaqnc-s_XgQ{+#-eC+wU`t#9JT*W=9jlj!u-K3$uKcEi$mwNKY+qig+H7V^#a7ga8(`}`_7Z^}zP zQ9q<-sx8egsg|Yw#I!ktma!{-pYw9{cA9Otc{FxdpQqV|m`7tnCY0Yc zh}>&F0UN>WHJ^ZuV|JNGW22dk=F!+W%xd#!?5C`Y&7-j;iGLTEM`LSqPco0j_UtvF zc|TaBnTP~M<};~0<^#KGm-vZS{wjg@Oab*XJUP5=3P_!s&8re=9eQD8B{9Jc+$-AK5ydi~mnpf?M*P557 z@N)B#eer(t%M?yAKiL-tnSV;*IP>a^$~!1re}Az#u=?k@{eksw9&mD^TA$$8<{7%T zU(d|ucb|eKly1+L^El`HZd84>FK6@E9$Xr)_T`*5+iTxtF+aS1Q}Pz|J-^GG59Kvq z$RE;k|Gt3?V-1fyE-1*Ls!QC1{o@oTsl+9J2Pa? o!@AA+ej3>3+2wVTOis1_06el#wiqRwUjP6A07*qoM6N<$f>{3oE&u=k diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 2767418ea..ffe9c0979 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None: BLACK, ) expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") - assert_image_similar_tofile(img, expected, 1) + assert_image_equal_tofile(img, expected) def test_polygon2() -> None: diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index ea6f8805e..d5aff8709 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,55 +501,49 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && j % 2 == 1 && - roundf(xx[j - 1]) == xx[j - 1]) { + } else if ((ymin == current->ymin || ymin == current->ymax) && + current->dx != 0) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; - if ((current->dx > 0 && other_edge->dx <= 0) || - (current->dx < 0 && other_edge->dx >= 0)) { + if ((ymin != other_edge->ymin && ymin != other_edge->ymax) || + other_edge->dx == 0) { continue; } // Check if the two edges join to make a corner - if (xx[j - 1] == - (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { + if (roundf(xx[j - 1]) == + roundf( + (ymin - other_edge->y0) * other_edge->dx + + other_edge->x0 + )) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row - int offset = ymin == ymax ? -1 : 1; + int offset = ymin == current->ymax ? -1 : 1; adjacent_line_x = (ymin + offset - current->y0) * current->dx + current->x0; - adjacent_line_x_other_edge = - (ymin + offset - other_edge->y0) * other_edge->dx + - other_edge->x0; - if (ymin == current->ymax) { - if (current->dx > 0) { - xx[k] = - fmax( + if (ymin + offset >= other_edge->ymin && + ymin + offset <= other_edge->ymax) { + adjacent_line_x_other_edge = + (ymin + offset - other_edge->y0) * other_edge->dx + + other_edge->x0; + if (xx[j - 1] > adjacent_line_x + 1 && + xx[j - 1] > adjacent_line_x_other_edge + 1) { + xx[j - 1] = + roundf(fmax( adjacent_line_x, adjacent_line_x_other_edge - ) + + )) + 1; - } else { - xx[k] = - fmin( + } else if (xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1) { + xx[j - 1] = + roundf(fmin( adjacent_line_x, adjacent_line_x_other_edge - ) - - 1; - } - } else { - if (current->dx > 0) { - xx[k] = fmin( - adjacent_line_x, adjacent_line_x_other_edge - ); - } else { - xx[k] = - fmax( - adjacent_line_x, adjacent_line_x_other_edge - ) + + )) - 1; } + break; } - break; } } } From 25653d2f87f0f0be370442836b472a43c1898b71 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:34:42 +1100 Subject: [PATCH 30/35] Corrected P mode save (#8685) --- Tests/test_file_palm.py | 6 +++++- src/PIL/PalmImagePlugin.py | 33 +++++++++------------------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index a1859bc33..58208ba99 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None: im.save(outfile) converted = open_with_magick(magick, tmp_path, outfile) + if mode == "P": + assert converted.mode == "P" + + im = im.convert("RGB") + converted = converted.convert("RGB") assert_image_equal(converted, im) @@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None: roundtrip(tmp_path, mode) -@pytest.mark.xfail(reason="Palm P image is wrong") def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index b33245376..15f712908 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, - # unless the "info" dict has a "custom-colormap" field - rawmode = "P" bpp = 8 version = 1 @@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: compression_type = _COMPRESSION_TYPES["none"] flags = 0 - if im.mode == "P" and "custom-colormap" in im.info: - assert im.palette is not None - flags = flags & _FLAGS["custom-colormap"] - colormapsize = 4 * 256 + 2 - colormapmode = im.palette.mode - colormap = im.getdata().getpalette() + if im.mode == "P": + flags |= _FLAGS["custom-colormap"] + colormap = im.im.getpalette() + colors = len(colormap) // 3 + colormapsize = 4 * colors + 2 else: colormapsize = 0 @@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # now write colormap if necessary - if colormapsize > 0: - fp.write(o16b(256)) - for i in range(256): + if colormapsize: + fp.write(o16b(colors)) + for i in range(colors): fp.write(o8(i)) - if colormapmode == "RGB": - fp.write( - o8(colormap[3 * i]) - + o8(colormap[3 * i + 1]) - + o8(colormap[3 * i + 2]) - ) - elif colormapmode == "RGBA": - fp.write( - o8(colormap[4 * i]) - + o8(colormap[4 * i + 1]) - + o8(colormap[4 * i + 2]) - ) + fp.write(colormap[3 * i : 3 * i + 3]) # now convert data to raw form ImageFile._save( From bce83ac800dd70c8b49fff9662a2352b2a388a0b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:36:36 +1100 Subject: [PATCH 31/35] Enable mmap on PyPy (#8840) --- src/PIL/Image.py | 2 ++ src/PIL/ImageFile.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 67e068b06..662afadf4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -621,6 +621,8 @@ class Image: more information. """ if getattr(self, "map", None): + if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): + self.map.close() self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f5b72ee0d..a7848c369 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,7 +34,6 @@ import itertools import logging import os import struct -import sys from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast from . import ExifTags, Image @@ -278,8 +277,6 @@ class ImageFile(Image.Image): self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 - # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") readonly = 0 From 14fb62e36c5e933faf9f88e9c0418f71be883a9c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 30 Mar 2025 18:42:46 +1100 Subject: [PATCH 32/35] Assert image type (#8619) --- Tests/test_file_apng.py | 44 ++++++++++++++++++++++++++++++++ Tests/test_file_dcx.py | 2 ++ Tests/test_file_eps.py | 6 +++++ Tests/test_file_fli.py | 8 ++++++ Tests/test_file_fpx.py | 3 ++- Tests/test_file_gif.py | 25 ++++++++++++++++++ Tests/test_file_icns.py | 4 +++ Tests/test_file_ico.py | 5 ++++ Tests/test_file_im.py | 2 ++ Tests/test_file_jpeg.py | 10 ++++++++ Tests/test_file_jpeg2k.py | 2 ++ Tests/test_file_libtiff.py | 21 +++++++++++++++ Tests/test_file_mic.py | 5 +++- Tests/test_file_mpo.py | 7 ++++- Tests/test_file_png.py | 7 +++++ Tests/test_file_psd.py | 7 +++++ Tests/test_file_spider.py | 1 + Tests/test_file_tiff.py | 44 +++++++++++++++++++++++++++++--- Tests/test_file_tiff_metadata.py | 20 +++++++++++++++ Tests/test_file_webp_animated.py | 9 ++++++- Tests/test_file_webp_metadata.py | 3 ++- Tests/test_file_wmf.py | 3 +++ Tests/test_file_xpm.py | 1 + Tests/test_imagesequence.py | 4 ++- Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + 26 files changed, 236 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index abd7d510b..a5734c202 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # (referenced from https://wiki.mozilla.org/APNG_Specification) def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated assert im.n_frames == 1 assert im.get_format_mimetype() == "image/apng" @@ -20,6 +21,7 @@ def test_apng_basic() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.is_animated assert im.n_frames == 2 assert im.get_format_mimetype() == "image/apng" @@ -52,6 +54,7 @@ def test_apng_basic() -> None: ) def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -59,31 +62,37 @@ def test_apng_fdat(filename: str) -> None: def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) @@ -91,21 +100,25 @@ def test_apng_dispose() -> None: def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -132,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None: # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (255, 0, 0, 255) @@ -145,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None: def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((64, 32)) == (0, 255, 0, 2) with Image.open("Tests/images/apng/blend_op_over.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -178,6 +197,7 @@ def test_apng_blend_transparency() -> None: def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -233,24 +253,28 @@ def test_apng_num_plays() -> None: def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "RGBA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191) with Image.open("Tests/images/apng/mode_grayscale.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "L" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == 128 assert im.getpixel((64, 32)) == 255 with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "LA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191) with Image.open("Tests/images/apng/mode_palette.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGB") @@ -258,6 +282,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -265,6 +290,7 @@ def test_apng_mode() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") @@ -274,25 +300,31 @@ def test_apng_mode() -> None: def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) @@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() # we can handle this case gracefully with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() @@ -339,6 +376,7 @@ def test_apng_syntax_errors() -> None: def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -349,6 +387,7 @@ def test_apng_save(tmp_path: Path) -> None: im.save(test_file, save_all=True) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert not im.is_animated assert im.n_frames == 1 @@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None: ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.load() assert im.is_animated assert im.n_frames == 2 @@ -403,6 +443,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -445,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 1 assert "duration" not in im.info @@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: duration=[500, 100, 150], ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 @@ -466,6 +509,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: frame.info["duration"] = 300 frame.save(test_file, save_all=True, append_images=[frame, different_frame]) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ab6b9f983..e9d88dd39 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -69,12 +69,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index f5acc532c..b484a8cfa 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = ( def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: + assert isinstance(image, EpsImagePlugin.EpsImageFile) + image.load(scale=scale) assert image.mode == "RGB" assert image.size == expected_size @@ -227,6 +229,8 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: + assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) + plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -308,6 +312,7 @@ def test_render_scale2() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale2: + assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: image1_scale2_compare = image1_scale2_compare.convert("RGB") @@ -316,6 +321,7 @@ def test_render_scale2() -> None: # Non-zero bounding box with Image.open(FILE2) as image2_scale2: + assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: image2_scale2_compare = image2_scale2_compare.convert("RGB") diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 2f39adc69..81df1ab0b 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" def test_sanity() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + im.load() assert im.mode == "P" assert im.size == (128, 128) @@ -29,6 +31,8 @@ def test_sanity() -> None: assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None: def test_n_frames() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 384 assert im.is_animated def test_eoferror() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -166,6 +173,7 @@ def test_seek_tell() -> None: def test_seek() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a01..8d8064692 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -22,10 +22,11 @@ def test_sanity() -> None: def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass + assert isinstance(im, FpxImagePlugin.FpxImageFile) assert im.ole.fp.closed im = Image.open("Tests/images/input_bw_one_band.fpx") + assert isinstance(im, FpxImagePlugin.FpxImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 376eab0c6..20d58a9dd 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) frame_count = 0 try: while True: @@ -446,10 +447,12 @@ def test_seek_rewind() -> None: def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated == (n_frames != 1) # Test is_animated after n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -459,6 +462,7 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 5 assert_image_equal(im, expected) @@ -466,17 +470,20 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(3) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated assert_image_equal(im, expected) with Image.open("Tests/images/comment_after_only_frame.gif") as im: expected = Image.new("P", (1, 1)) + assert isinstance(im, GifImagePlugin.GifImageFile) assert not im.is_animated assert_image_equal(im, expected) def test_eoferror() -> None: with Image.open(TEST_GIF) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None: def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None: def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -571,6 +580,7 @@ def test_transparent_dispose( def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None: for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for _ in range(2): img.seek(img.tell() + 1) assert img.disposal_method == method @@ -621,6 +632,7 @@ def test_save_dispose(tmp_path: Path) -> None: ) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -743,6 +755,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) with Image.open(out) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 3 @@ -924,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None: out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -953,6 +968,8 @@ def test_identical_frames_to_single_frame( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that all frames were combined assert reread.n_frames == 1 @@ -1139,12 +1156,14 @@ def test_append_images(tmp_path: Path) -> None: im.copy().save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Test append_images without save_all im.copy().save(out, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending using a generator @@ -1154,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending single and multiple frame images @@ -1162,6 +1182,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 10 @@ -1262,6 +1283,7 @@ def test_bbox(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1274,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1425,6 +1448,7 @@ def test_extents( ) -> None: monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) with Image.open("Tests/images/" + test_file) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.size == (100, 100) # Check that n_frames does not change the size @@ -1472,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: + assert isinstance(reloaded, GifImagePlugin.GifImageFile) assert reloaded.n_frames == 2 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index b6dc4bc19..2dabfd2f3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) reread.size = (16, 16) reread.load(2) assert_image_equal(reread, provided_im) @@ -90,6 +91,7 @@ def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: + assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: wr = w * r hr = h * r @@ -118,6 +120,7 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" @@ -135,6 +138,7 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h) im2.load(r) assert im2.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37bfd3f1f..5d2ace35e 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -77,6 +77,7 @@ def test_save_to_bytes() -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert im.mode == reloaded.mode @@ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None: im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) with Image.open(temp_file) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.load() reloaded.size = (32, 32) @@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert "RGBA" == reloaded.mode @@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: + assert isinstance(im, IcoImagePlugin.IcoImageFile) with pytest.raises(ValueError): im.size = (1, 1) @@ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None: im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: + assert isinstance(reread, IcoImagePlugin.IcoImageFile) assert_image_equal(reread, hopper("RGBA")) reread.size = (32, 32) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 235914a2b..55c6b7305 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -68,12 +68,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 8ab853b85..79f0ec1a8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -91,6 +91,7 @@ class TestFileJpeg: def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[1] == ( "COM", @@ -316,6 +317,8 @@ class TestFileJpeg: def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -500,6 +503,7 @@ class TestFileJpeg: def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im._getmp() is None def test_quality_keep(self, tmp_path: Path) -> None: @@ -558,12 +562,14 @@ class TestFileJpeg: with Image.open(test_file) as im: im.save(b, "JPEG", qtables=[[n] * 64] * n) with Image.open(b) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) assert im.quantization == reloaded.quantization @@ -663,6 +669,7 @@ class TestFileJpeg: def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 @@ -705,6 +712,7 @@ class TestFileJpeg: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: + assert isinstance(img, JpegImagePlugin.JpegImageFile) img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @@ -909,6 +917,7 @@ class TestFileJpeg: def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] @@ -1084,6 +1093,7 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) with pytest.warns(DeprecationWarning): assert im.huffman_ac == {} with pytest.warns(DeprecationWarning): diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 916df2586..4095bfaf2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None: out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 1 im.load() assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 3 im.load() assert_image_similar(im, card, 0.4) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9e63e9c10..9916215fb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,6 +36,7 @@ class LibTiffTestCase: im.load() im.getdata() + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._compression == "group4" # can we write it back out, in a different form. @@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase): """Test metadata writing through libtiff""" f = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) img.save(f, tiffinfo=img.tag) if legacy_api: @@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them with Image.open("Tests/images/hopper_g4.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: try: del core_items[tag] @@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) for tag, value in tiffinfo.items(): reloaded_value = reloaded.tag_v2[tag] if ( @@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase): def test_osubfiletype(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: outfile = tmp_path / "temp.tif" with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[SUBIFD] = 10000 # Should not segfault @@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase): hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" @@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: + assert isinstance(orig, TiffImagePlugin.TiffImageFile) + out = tmp_path / "temp.tif" orig.tag[269] = "temp.tif" orig.save(out) with Image.open(out) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] @@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: # colormap/palette tag + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im.size == (10, 10) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) @@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): # issue #862 monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) frames = im.n_frames assert frames == 3 for _ in range(frames): @@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert not im.tag.next im.load() assert not im.tag.next @@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase): im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) def test_exif_ifd(self) -> None: out = io.BytesIO() with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[34665] == 125456 im.save(out, "TIFF") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 34665 not in reloaded.tag_v2 im.save(out, "TIFF", tiffinfo={34665: 125456}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 @@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -1090,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 274 in im.tag_v2 im.load() diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..9aeb306e4 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -30,11 +30,13 @@ def test_sanity() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.n_frames == 1 def test_is_animated() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert not im.is_animated @@ -55,10 +57,11 @@ def test_seek() -> None: def test_close() -> None: with Image.open(TEST_FILE) as im: - pass + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.ole.fp.closed im = Image.open(TEST_FILE) + assert isinstance(im, MicImagePlugin.MicImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 6b4f6423b..73838ef44 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from PIL import Image, ImageFile, MpoImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin from .helper import ( assert_image_equal, @@ -80,6 +80,7 @@ def test_context_manager() -> None: def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" assert im.applist[1][1].startswith( @@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None: def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -239,6 +242,8 @@ def test_eoferror() -> None: def test_adopt_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + with pytest.raises(ValueError): MpoImagePlugin.MpoImageFile.adopt(im) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c969bd502..0f0886ab8 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -576,6 +576,7 @@ class TestFilePng: def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.private_chunks == [(b"orNT", b"\x01")] def test_roundtrip_private_chunk(self) -> None: @@ -598,6 +599,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/hopper.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", @@ -607,15 +609,19 @@ class TestFilePng: # Raises a SyntaxError in load_end with Image.open("Tests/images/broken_data_stream.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(OSError): assert isinstance(im.text, dict) # Raises an EOFError in load_end with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + # The file is truncated with pytest.raises(OSError): im.text @@ -726,6 +732,7 @@ class TestFilePng: im.save(test_file) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) assert reloaded._getexif() is None # Test passing in exif diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 1793c269d..38a88cd17 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -59,17 +59,21 @@ def test_invalid_file() -> None: def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 assert not im.is_animated for path in [test_file, "Tests/images/negative_layer_count.psd"]: with Image.open(path) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -119,11 +123,13 @@ def test_rgba() -> None: def test_negative_top_left_layer() -> None: with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.layers[0][2] == (-50, -50, 50, 50) def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 @@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: def test_layer_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index b64a629f5..3b3c3b4a5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -96,6 +96,7 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, SpiderImagePlugin.SpiderImageFile) assert im.n_frames == 1 assert not im.is_animated diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6962a5c98..502d9df9a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -9,7 +9,13 @@ from types import ModuleType import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + UnidentifiedImageError, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -113,6 +119,7 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: outfile = tmp_path / "temp.tif" + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) def test_bigtiff_save(self, tmp_path: Path) -> None: @@ -121,11 +128,13 @@ class TestFileTiff: im.save(outfile, big_tiff=True) with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2._bigtiff is True im.save(outfile, save_all=True, append_images=[im], big_tiff=True) with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2._bigtiff is True def test_seek_too_large(self) -> None: @@ -140,6 +149,8 @@ class TestFileTiff: def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -153,6 +164,8 @@ class TestFileTiff: def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -167,6 +180,8 @@ class TestFileTiff: def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -181,6 +196,7 @@ class TestFileTiff: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) @@ -198,6 +214,7 @@ class TestFileTiff: with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 @@ -213,10 +230,12 @@ class TestFileTiff: TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + with Image.open("Tests/images/hopper_bad_exif.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise struct.error. with pytest.warns(UserWarning): - i._getexif() + im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") @@ -307,11 +326,13 @@ class TestFileTiff: ) def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -355,19 +376,24 @@ class TestFileTiff: def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 1 # A frame can't progress to a frame that has already been read with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 2 # Frames don't have to be in sequence with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Act ret = str(im.ifd) @@ -378,6 +404,8 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -417,6 +445,7 @@ class TestFileTiff: def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) @@ -449,6 +478,7 @@ class TestFileTiff: def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: @@ -537,6 +567,7 @@ class TestFileTiff: im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) @@ -615,6 +646,8 @@ class TestFileTiff: filename = tmp_path / "temp.tif" hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 @@ -701,6 +734,7 @@ class TestFileTiff: def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._planar_configuration == 2 outfile = tmp_path / "temp.tif" @@ -733,6 +767,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 # Test appending images @@ -743,6 +778,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 # Test appending using a generator @@ -754,6 +790,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 def test_fixoffsets(self) -> None: @@ -864,6 +901,7 @@ class TestFileTiff: def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert list(im.get_photoshop_blocks().keys()) == [ 1061, 1002, diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0734d1db1..884868345 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -128,6 +131,7 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) f = tmp_path / "temp.tiff" del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None: original = img.tag_v2.named() with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = tmp_path / "temp.tiff" with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) info = im.tag_v2 del info[278] @@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG @@ -231,6 +238,7 @@ def test_writing_other_types_to_ascii( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[271] == expected @@ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[700] == b"\x01" @@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[33723] == b"1" @@ -311,6 +321,7 @@ def test_iccprofile_binary() -> None: # but probably won't be able to save it. with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.tagtype[34675] == 1 assert im.info["icc_profile"] @@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].denominator @@ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == -60000 @@ -444,11 +462,13 @@ def test_empty_values() -> None: def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) out = tmp_path / "temp.tiff" im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[34377]) == 70 assert isinstance(reloaded.tag_v2[34377], bytes) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index d4b1fda97..503761374 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import GifImagePlugin, Image, WebPImagePlugin, features from .helper import ( assert_image_equal, @@ -22,10 +22,12 @@ def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/iss634.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 42 assert im.is_animated @@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: """ with Image.open("Tests/images/iss634.gif") as orig: + assert isinstance(orig, GifImagePlugin.GifImageFile) assert orig.n_frames > 1 temp_file = tmp_path / "temp.webp" orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF @@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: def check(temp_file: Path) -> None: with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 2 # Compare first frame to original @@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d1d3421ec..7543d22da 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,7 +6,7 @@ from types import ModuleType import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature @@ -110,6 +110,7 @@ def test_read_no_exif() -> None: test_buffer.seek(0) with Image.open(test_buffer) as webp_image: + assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) assert not webp_image._getexif() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index a752f8013..dcf5f000f 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -89,6 +89,7 @@ def test_load_float_dpi() -> None: def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) assert im.size == (82, 82) if hasattr(Image.core, "drawwmf"): @@ -102,10 +103,12 @@ def test_load_set_dpi() -> None: if not hasattr(Image.core, "drawwmf"): return + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load(im.info["dpi"]) assert im.size == (1625, 1625) with Image.open("Tests/images/drawing.emf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) im.load((72, 144)) assert im.size == (82, 164) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f4..73c62a44d 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -30,6 +30,7 @@ def test_invalid_file() -> None: def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: + assert isinstance(im, XpmImagePlugin.XpmImageFile) dummy_bytes = 1 # Act diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index da9e71692..7b9ac80bc 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import Image, ImageSequence, TiffImagePlugin +from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature @@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) i = ImageSequence.Iterator(im) for index in range(im.n_frames): assert i[index] == next(i) @@ -42,6 +43,7 @@ def test_iterator() -> None: def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 4fd3aab5d..03e92b5b9 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -39,6 +39,7 @@ class TestShellInjection: shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 30dc73654..42d06b896 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -72,4 +72,5 @@ def test_ifd_rational_save( im.save(out, dpi=(res, res), compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) From 4236b583a1578c089cd06538a9ded20bcba1a863 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Mar 2025 22:16:16 +1100 Subject: [PATCH 33/35] Do not import TYPE_CHECKING --- src/PIL/GifImagePlugin.py | 3 ++- src/PIL/Image.py | 10 ++-------- src/PIL/ImageDraw.py | 3 ++- src/PIL/ImageFile.py | 3 ++- src/PIL/ImageFilter.py | 3 ++- src/PIL/ImageFont.py | 3 ++- src/PIL/ImagePalette.py | 3 ++- src/PIL/ImageQt.py | 3 ++- src/PIL/ImageTk.py | 3 ++- src/PIL/JpegImagePlugin.py | 3 ++- src/PIL/PSDraw.py | 5 ++++- src/PIL/PdfParser.py | 3 ++- src/PIL/PngImagePlugin.py | 3 ++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TiffImagePlugin.py | 3 ++- src/PIL/_typing.py | 3 ++- 16 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 045ab1027..4392c4cb9 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union from . import ( Image, @@ -47,6 +47,7 @@ from ._binary import o8 from ._binary import o16le as o16 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import Buffer diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 662afadf4..19b22342a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -41,14 +41,7 @@ import warnings from collections.abc import Callable, Iterator, MutableMapping, Sequence from enum import IntEnum from types import ModuleType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Literal, - Protocol, - cast, -) +from typing import IO, Any, Literal, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries +TYPE_CHECKING = False if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c2ed9034d..e6c7b0298 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -35,7 +35,7 @@ import math import struct from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast +from typing import Any, AnyStr, Callable, Union, cast from . import Image, ImageColor from ._deprecate import deprecate @@ -44,6 +44,7 @@ from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageDraw2, ImageFont diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a7848c369..c5d6383a5 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -34,12 +34,13 @@ import itertools import logging import os import struct -from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast +from typing import IO, Any, NamedTuple, cast from . import ExifTags, Image from ._deprecate import deprecate from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import StrOrBytesPath diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 05829d0c6..b9ed54ab2 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -20,8 +20,9 @@ import abc import functools from collections.abc import Sequence from types import ModuleType -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import Any, Callable, cast +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8f05fbb7..ebe510ba9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,12 +34,13 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast +from typing import IO, Any, BinaryIO, TypedDict, cast from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path +TYPE_CHECKING = False if TYPE_CHECKING: from . import ImageFile from ._imaging import ImagingFont diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 183f85526..103697117 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,10 +19,11 @@ from __future__ import annotations import array from collections.abc import Sequence -from typing import IO, TYPE_CHECKING +from typing import IO from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile +TYPE_CHECKING = False if TYPE_CHECKING: from . import Image diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2cc40f855..df7a57b65 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,11 +19,12 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import Any, Callable, Union from . import Image from ._util import is_path +TYPE_CHECKING = False if TYPE_CHECKING: import PyQt6 import PySide6 diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index e6a9d8eea..3a4cb81e9 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -28,10 +28,11 @@ from __future__ import annotations import tkinter from io import BytesIO -from typing import TYPE_CHECKING, Any +from typing import Any from . import Image, ImageFile +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import CapsuleType diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9465d8e2d..cc1d54b93 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, TYPE_CHECKING, Any +from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -52,6 +52,7 @@ from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets +TYPE_CHECKING = False if TYPE_CHECKING: from .MpoImagePlugin import MpoImageFile diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 02939d26b..7fd4c5c94 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,10 +17,13 @@ from __future__ import annotations import sys -from typing import IO, TYPE_CHECKING +from typing import IO from . import EpsImagePlugin +TYPE_CHECKING = False + + ## # Simple PostScript graphics interface. diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 41b38ebbf..73d8c21c0 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,7 @@ import os import re import time import zlib -from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union +from typing import IO, Any, NamedTuple, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,6 +251,7 @@ class PdfArray(list[Any]): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" +TYPE_CHECKING = False if TYPE_CHECKING: _DictBase = collections.UserDict[Union[str, bytes], Any] else: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 3e3cf6526..f3815a122 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast +from typing import IO, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -50,6 +50,7 @@ from ._binary import o16be as o16 from ._binary import o32be as o32 from ._util import DeferredError +TYPE_CHECKING = False if TYPE_CHECKING: from . import _imaging diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 62fa7be03..868019e80 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,11 +37,13 @@ from __future__ import annotations import os import struct import sys -from typing import IO, TYPE_CHECKING, Any, cast +from typing import IO, Any, cast from . import Image, ImageFile from ._util import DeferredError +TYPE_CHECKING = False + def isInt(f: Any) -> int: try: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ebe599cca..88af9162e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,7 +50,7 @@ import warnings from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast +from typing import IO, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -61,6 +61,7 @@ from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES +TYPE_CHECKING = False if TYPE_CHECKING: from ._typing import Buffer, IntegralLike diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 34a9a81e1..373938e71 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,8 +3,9 @@ from __future__ import annotations import os import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar, Union +TYPE_CHECKING = False if TYPE_CHECKING: from numbers import _IntegralLike as IntegralLike From 81be8d54103d008dcfb7d75edc5ca43d0f78afd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:25 +1100 Subject: [PATCH 34/35] Fixed unclosed file warning (#8847) --- Tests/test_image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f18d8489c..c2e850c36 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -230,10 +230,10 @@ class TestImage: assert_image_similar(im, reloaded, 20) def test_unknown_extension(self, tmp_path: Path) -> None: - im = hopper() temp_file = tmp_path / "temp.unknown" - with pytest.raises(ValueError): - im.save(temp_file) + with hopper() as im: + with pytest.raises(ValueError): + im.save(temp_file) def test_internals(self) -> None: im = Image.new("L", (100, 100)) From f673f3e543881ae283b25ef7db06fa828a4e353a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:16:50 +1100 Subject: [PATCH 35/35] Close file handle on error (#8846) --- src/PIL/TarIO.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 779288b1c..86490a496 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]): while True: s = self.fh.read(512) if len(s) != 512: + self.fh.close() + msg = "unexpected end of tar file" raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: + self.fh.close() + msg = "cannot find subfile" raise OSError(msg) if i > 0: