From 5b065970753d464c0d283f9e0c2f4a0971800ed1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:29:22 +1100 Subject: [PATCH 01/10] Use fixture to re-open image for each test --- Tests/test_file_jpeg2k.py | 57 +++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 26b085601b6..79f53e21195 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,6 +2,7 @@ import os import re +from collections.abc import Generator from io import BytesIO from pathlib import Path from typing import Any @@ -29,8 +30,16 @@ pytestmark = skip_unless_feature("jpg_2000") -test_card = Image.open("Tests/images/test-card.png") -test_card.load() + +@pytest.fixture +def test_card() -> Generator[ImageFile.ImageFile, None, None]: + with Image.open("Tests/images/test-card.png") as im: + im.load() + try: + yield im + finally: + im.close() + # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should # ignore it---it doesn't represent a test failure. @@ -74,7 +83,7 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio() -> None: +def test_bytesio(test_card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: @@ -86,7 +95,7 @@ def test_bytesio() -> None: # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path: Path) -> None: +def test_lossless(test_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") @@ -94,54 +103,56 @@ def test_lossless(tmp_path: Path) -> None: assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled() -> None: +def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt() -> None: +def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt() -> None: +def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt() -> None: +def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt() -> None: +def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small() -> None: +def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt() -> None: +def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt() -> None: +def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt() -> None: +def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions: int) -> None: +def test_default_num_resolutions( + test_card: ImageFile.ImageFile, num_resolutions: int +) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -205,7 +216,7 @@ def test_header_errors() -> None: pass -def test_layers_type(tmp_path: Path) -> None: +def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) @@ -215,7 +226,7 @@ def test_layers_type(tmp_path: Path) -> None: test_card.save(outfile, quality_layers=quality_layers_str) -def test_layers() -> None: +def test_layers(test_card: ImageFile.ImageFile) -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -245,7 +256,13 @@ def test_layers() -> None: (None, {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: +def test_no_jp2( + test_card: ImageFile.ImageFile, + name: str, + args: dict[str, bool], + offset: int, + data: bytes, +) -> None: out = BytesIO() if name: out.name = name @@ -254,7 +271,7 @@ def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> N assert out.read(2) == data -def test_mct() -> None: +def test_mct(test_card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() @@ -419,7 +436,7 @@ def test_comment() -> None: pass -def test_save_comment() -> None: +def test_save_comment(test_card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -457,7 +474,7 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker() -> None: +def test_plt_marker(test_card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) From 55579084cd57461517dfe77d7804dfa24219a9f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Oct 2024 20:40:13 +1100 Subject: [PATCH 02/10] Corrected EMF DPI --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263d6..424640d7b18 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 @@ -61,6 +62,12 @@ def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 + with open("Tests/images/drawing.emf", "rb") as fp: + data = fp.read() + b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) + with Image.open(b) as im: + assert im.info["dpi"][0] == 2540 + def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 68f8a74f599..cad6c98d53f 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -128,7 +128,7 @@ def _open(self) -> None: size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0]) ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 From c8e301c47456114f245261fccd316b4fd8964f3e Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 24 Oct 2024 15:52:59 +0200 Subject: [PATCH 03/10] Fix SEGFAULT from calling FT_New_Face/FT_Done_Face in multiple threads --- src/_imagingft.c | 12 +- src/thirdparty/pythoncapi_compat.h | 341 ++++++++++++++++++++++++++++- 2 files changed, 351 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7f246412455..6fed821bc0c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,10 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ - static FT_Library library; +static FT_Library library; +#ifdef Py_GIL_DISABLED +PyMutex ft_library_mutex; +#endif typedef struct { PyObject_HEAD FT_Face face; @@ -187,7 +190,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (filename && font_bytes_size <= 0) { self->font_bytes = NULL; + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Face(library, filename, index, &self->face); + MUTEX_UNLOCK(&ft_library_mutex); } else { /* need to have allocated storage for font_bytes for the life of the object.*/ /* Don't free this before FT_Done_Face */ @@ -197,6 +202,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); + MUTEX_LOCK(&ft_library_mutex); error = FT_New_Memory_Face( library, (FT_Byte *)self->font_bytes, @@ -204,6 +210,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { index, &self->face ); + MUTEX_UNLOCK(&ft_library_mutex); } } @@ -1433,7 +1440,9 @@ font_setvaraxes(FontObject *self, PyObject *args) { static void font_dealloc(FontObject *self) { if (self->face) { + MUTEX_LOCK(&ft_library_mutex); FT_Done_Face(self->face); + MUTEX_UNLOCK(&ft_library_mutex); } if (self->font_bytes) { PyMem_Free(self->font_bytes); @@ -1639,6 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED + ft_library_mutex = (PyMutex) {0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h index 51e8c0de752..ca23d5ffa9a 100644 --- a/src/thirdparty/pythoncapi_compat.h +++ b/src/thirdparty/pythoncapi_compat.h @@ -7,7 +7,10 @@ // https://github.com/python/pythoncapi_compat // // Latest version: -// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h +// +// This file was vendored from the following commit: +// https://github.com/python/pythoncapi-compat/commit/0041177c4f348c8952b4c8980b2c90856e61c7c7 // // SPDX-License-Identifier: 0BSD @@ -45,6 +48,13 @@ extern "C" { # define _PyObject_CAST(op) _Py_CAST(PyObject*, op) #endif +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + // bpo-42262 added Py_NewRef() to Python 3.10.0a3 #if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) @@ -1338,6 +1348,166 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #endif +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +static inline int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 // gh-116560 added PyLong_GetSign() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 @@ -1354,6 +1524,175 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) #endif +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + #ifdef __cplusplus } #endif From 7999da38a7ff5aa359aa6ae3a7f88b835c7f7f06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:07:43 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 6fed821bc0c..884b8f96c2e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,7 +81,7 @@ struct { /* -------------------------------------------------------------------- */ /* font objects */ -static FT_Library library; + static FT_Library library; #ifdef Py_GIL_DISABLED PyMutex ft_library_mutex; #endif @@ -1648,7 +1648,7 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex) {0}; + ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From bb3515d649cda179037161ee3bea264a4d289b32 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Fri, 25 Oct 2024 17:32:29 +0200 Subject: [PATCH 05/10] Make PyMutex static and get rid of initialization --- src/_imagingft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 884b8f96c2e..d38279f3e4b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -83,7 +83,7 @@ struct { static FT_Library library; #ifdef Py_GIL_DISABLED -PyMutex ft_library_mutex; +static PyMutex ft_library_mutex; #endif typedef struct { @@ -1648,7 +1648,6 @@ PyInit__imagingft(void) { } #ifdef Py_GIL_DISABLED - ft_library_mutex = (PyMutex){0}; PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif From a43e5bb7354006766a67a353aa21d9a0198cdcab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 14:26:47 +1100 Subject: [PATCH 06/10] brew remove libdeflate --- .github/workflows/wheels-dependencies.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf098a..7970d4d1525 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -121,6 +121,7 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then + # libdeflate may cause a minimum target error when repairing the wheel # libtiff and libxcb cause a conflict with building libtiff and libxcb # libxau and libxdmcp cause an issue on macOS < 11 # remove cairo to fix building harfbuzz on arm64 @@ -132,7 +133,7 @@ if [[ -n "$IS_MACOS" ]]; then if [[ "$CIBW_ARCHS" == "arm64" ]]; then brew remove --ignore-dependencies jpeg-turbo else - brew remove --ignore-dependencies webp + brew remove --ignore-dependencies libdeflate webp fi brew install pkg-config From e1f4b5a68fa783126f5a81ccba5fac4526f6ab00 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 15:10:41 +1100 Subject: [PATCH 07/10] Move MPO into "Fully supported formats" --- docs/handbook/image-file-formats.rst | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8183473e4f5..bf3087f6f68 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -692,6 +692,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: you fail to do this, you will get errors about not being able to load the ``_imaging`` DLL). +MPO +^^^ + +Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads +the primary image. The :py:meth:`~PIL.Image.Image.seek` and +:py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the +file. The pictures are zero-indexed and random access is supported. + +.. _mpo-saving: + +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. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + MSP ^^^ @@ -1435,30 +1459,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files. To enable MIC support, you must install :pypi:`olefile`. -MPO -^^^ - -Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary -image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` -methods may be used to read other pictures from the file. The pictures are -zero-indexed and random access is supported. - -.. _mpo-saving: - -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. - -**append_images** - A list of images to append as additional pictures. Each of the - images in the list can be single or multiframe images. - - .. versionadded:: 9.3.0 - PCD ^^^ From 413bbb31c959a3e2a6929d4a12861a79d82f8e80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 16:15:46 +1100 Subject: [PATCH 08/10] Fixed catching warnings --- Tests/test_bmp_reference.py | 2 ++ Tests/test_file_dcx.py | 4 ++++ Tests/test_file_fli.py | 4 ++++ Tests/test_file_gif.py | 4 ++++ Tests/test_file_icns.py | 2 ++ Tests/test_file_im.py | 4 ++++ Tests/test_file_jpeg.py | 2 ++ Tests/test_file_mpo.py | 4 ++++ Tests/test_file_png.py | 2 ++ Tests/test_file_psd.py | 4 ++++ Tests/test_file_spider.py | 4 ++++ Tests/test_file_tar.py | 4 ++++ Tests/test_file_tiff.py | 4 ++++ Tests/test_file_webp.py | 2 ++ Tests/test_image.py | 2 ++ Tests/test_imageqt.py | 2 ++ Tests/test_numpy.py | 2 ++ 17 files changed, 52 insertions(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f848792131..82cab39c613 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -22,6 +22,8 @@ def test_bad() -> None: for f in get_files("b"): # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + try: with Image.open(f) as im: im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9b1..5deacd878ea 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -36,6 +36,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -43,6 +45,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d0925..0a7740cc87d 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -65,6 +65,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(static_test_file) im.load() im.close() @@ -81,6 +83,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(static_test_file) as im: im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f331..248347d5bb9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -46,6 +46,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_GIF) im.load() im.close() @@ -67,6 +69,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_GIF) as im: im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 16f6b36516d..141b88dfa01 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -21,6 +21,8 @@ def test_sanity() -> None: with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5d8..1d3fa485f3b 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -41,6 +41,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_IM) im.load() im.close() @@ -48,6 +50,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_IM) as im: im.load() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395a7..2c66652e5d2 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -850,6 +850,8 @@ def test_exif_x_resolution(self, tmp_path: Path) -> None: out = str(tmp_path / "out.jpg") with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(out, exif=exif) with Image.open(out) as reloaded: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a26649..94958318557 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -48,6 +48,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_files[0]) im.load() im.close() @@ -63,6 +65,8 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_files[0]) as im: im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0abf9866ff2..ffafc3c582a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -338,6 +338,8 @@ def test_load_verify(self) -> None: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning with warnings.catch_warnings(): + warnings.simplefilter("error") + im.verify() with Image.open(TEST_PNG_FILE) as im: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b6e..5f22001f324 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -35,6 +35,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(test_file) im.load() im.close() @@ -42,6 +44,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(test_file) as im: im.load() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 66c88e9d8eb..4cafda86536 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -34,6 +34,8 @@ def open() -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open(TEST_FILE) im.load() im.close() @@ -41,6 +43,8 @@ def test_closed_file() -> None: def test_context_manager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 6217ebedd8a..49220a8b690 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -37,11 +37,15 @@ def test_unclosed_file() -> None: def test_close() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() def test_contextmanager() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index af766022b5c..6f51d46513e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -72,6 +72,8 @@ def open() -> None: def test_closed_file(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + im = Image.open("Tests/images/multipage.tiff") im.load() im.close() @@ -88,6 +90,8 @@ def test_seek_after_close(self) -> None: def test_context_manager(self) -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + with Image.open("Tests/images/multipage.tiff") as im: im.load() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 79f6bb4e0e4..ad5aa9ed62c 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -191,6 +191,8 @@ def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: with warnings.catch_warnings(): + warnings.simplefilter("error") + image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 9b65041f4b4..c8df474f493 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -737,6 +737,8 @@ def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # Act/Assert with Image.open(test_file) as im: with warnings.catch_warnings(): + warnings.simplefilter("error") + im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 22cd674ce28..2d7ca0ae0fd 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -52,4 +52,6 @@ def test_image(mode: str) -> None: def test_closed_file() -> None: with warnings.catch_warnings(): + warnings.simplefilter("error") + ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 040472d69e3..79cd14b6690 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -264,4 +264,6 @@ def test_no_resource_warning_for_numpy_array() -> None: with Image.open(test_file) as im: # Act/Assert with warnings.catch_warnings(): + warnings.simplefilter("error") + array(im) From f92599aa9394d9b1f59a503a796bc0fb6ddb8cda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 19:05:16 +1100 Subject: [PATCH 09/10] Renamed fixture --- Tests/test_file_jpeg2k.py | 102 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 79f53e21195..fbf72ae0518 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -32,7 +32,7 @@ @pytest.fixture -def test_card() -> Generator[ImageFile.ImageFile, None, None]: +def card() -> Generator[ImageFile.ImageFile, None, None]: with Image.open("Tests/images/test-card.png") as im: im.load() try: @@ -83,78 +83,76 @@ def test_invalid_file() -> None: Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(test_card: ImageFile.ImageFile) -> None: +def test_bytesio(card: ImageFile.ImageFile) -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) with Image.open(data) as im: im.load() - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # These two test pre-written JPEG 2000 files that were not written with # PIL (they were made using Adobe Photoshop) -def test_lossless(test_card: ImageFile.ImageFile, tmp_path: Path) -> 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") im.save(outfile) - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) -def test_lossy_tiled(test_card: ImageFile.ImageFile) -> None: - assert_image_similar_tofile( - test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 - ) +def test_lossy_tiled(card: ImageFile.ImageFile) -> None: + assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0) -def test_lossless_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card) - assert_image_equal(im, test_card) +def test_lossless_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card) + assert_image_equal(im, card) -def test_lossy_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_lossy_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_tiled_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) +def test_tiled_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128)) + assert_image_equal(im, card) -def test_tiled_offset_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) - assert_image_equal(im, test_card) +def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) + assert_image_equal(im, card) -def test_tiled_offset_too_small(test_card: ImageFile.ImageFile) -> None: +def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None: with pytest.raises(ValueError): - roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) + roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) +def test_irreversible_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, irreversible=True, quality_layers=[20]) + assert_image_similar(im, card, 2.0) -def test_prog_qual_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") - assert_image_similar(im, test_card, 2.0) +def test_prog_qual_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP") + assert_image_similar(im, card, 2.0) -def test_prog_res_rt(test_card: ImageFile.ImageFile) -> None: - im = roundtrip(test_card, num_resolutions=8, progression="RLCP") - assert_image_equal(im, test_card) +def test_prog_res_rt(card: ImageFile.ImageFile) -> None: + im = roundtrip(card, num_resolutions=8, progression="RLCP") + assert_image_equal(im, card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( - test_card: ImageFile.ImageFile, num_resolutions: int + card: ImageFile.ImageFile, num_resolutions: int ) -> None: d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) + im = card.resize((d - 1, d - 1)) with pytest.raises(OSError): roundtrip(im, num_resolutions=num_resolutions) reloaded = roundtrip(im) @@ -216,31 +214,31 @@ def test_header_errors() -> None: pass -def test_layers_type(test_card: ImageFile.ImageFile, tmp_path: Path) -> None: +def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: - test_card.save(outfile, quality_layers=quality_layers) + card.save(outfile, quality_layers=quality_layers) for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers_str) + card.save(outfile, quality_layers=quality_layers_str) -def test_layers(test_card: ImageFile.ImageFile) -> None: +def test_layers(card: ImageFile.ImageFile) -> None: out = BytesIO() - test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") + card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) with Image.open(out) as im: im.layers = 1 im.load() - assert_image_similar(im, test_card, 13) + assert_image_similar(im, card, 13) out.seek(0) with Image.open(out) as im: im.layers = 3 im.load() - assert_image_similar(im, test_card, 0.4) + assert_image_similar(im, card, 0.4) @pytest.mark.parametrize( @@ -257,7 +255,7 @@ def test_layers(test_card: ImageFile.ImageFile) -> None: ), ) def test_no_jp2( - test_card: ImageFile.ImageFile, + card: ImageFile.ImageFile, name: str, args: dict[str, bool], offset: int, @@ -266,20 +264,20 @@ def test_no_jp2( out = BytesIO() if name: out.name = name - test_card.save(out, "JPEG2000", **args) + card.save(out, "JPEG2000", **args) out.seek(offset) assert out.read(2) == data -def test_mct(test_card: ImageFile.ImageFile) -> None: +def test_mct(card: ImageFile.ImageFile) -> None: # Three component for val in (0, 1): out = BytesIO() - test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + card.save(out, "JPEG2000", mct=val, no_jp2=True) assert out.getvalue()[59] == val with Image.open(out) as im: - assert_image_similar(im, test_card, 1.0e-3) + assert_image_similar(im, card, 1.0e-3) # Single component should have MCT disabled for val in (0, 1): @@ -436,22 +434,22 @@ def test_comment() -> None: pass -def test_save_comment(test_card: ImageFile.ImageFile) -> None: +def test_save_comment(card: ImageFile.ImageFile) -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) + card.save(out, "JPEG2000", comment=comment) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" out = BytesIO() long_comment = b" " * 65531 - test_card.save(out, "JPEG2000", comment=long_comment) + card.save(out, "JPEG2000", comment=long_comment) with Image.open(out) as im: assert im.info["comment"] == long_comment with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=long_comment + b" ") + card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( @@ -474,10 +472,10 @@ def test_crashes(test_file: str) -> None: @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(test_card: ImageFile.ImageFile) -> None: +def test_plt_marker(card: ImageFile.ImageFile) -> None: # Search the start of the codesteam for PLT out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, plt=True) + card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: marker = out.read(2) From 29cdbce39e1d3860486319b46ea829e0c3566279 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 21:13:01 +1100 Subject: [PATCH 10/10] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae384156963..87945bc8456 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Corrected EMF DPI #8485 + [radarhere] + - Fix IFDRational with a zero denominator #8474 [radarhere]