Repository: python-pillow/Pillow

Issue:
GIF encoder: black pixels incorrectly cropped when `disposal=2`
### What did you do?

I created a GIF with these two identically sized (120x120) frames:

```py
first = Image.open("first.png")
second = Image.open("second.png")
first.save("out.gif", save_all=True, append_images=[second], disposal=2, loop=0, duration=500)
```

![Image](https://github.com/user-attachments/assets/85b1a354-4b9d-4d89-8415-84e051287d0d) ![Image](https://github.com/user-attachments/assets/0edd09bb-a968-4516-b9a0-01197ba6950f)

### What did you expect to happen?

Pillow creates a GIF with two frames, each 120x120.

### What actually happened?

Pillow creates a GIF with a cropped second frame:

```sh
> identify out.gif
out.gif[0] GIF 120x120 120x120+0+0 8-bit sRGB 8c 0.000u 0:00.000
out.gif[1] GIF 90x90 120x120+15+15 8-bit sRGB 8c 1147B 0.000u 0:00.000
```

On most viewers (Safari being an exception, as far as I can tell), the GIF is displayed incorrectly:

![Image](https://github.com/user-attachments/assets/3855c811-8393-40b8-ba0e-0b2317b248ac)

This seems related to the already-fixed https://github.com/python-pillow/Pillow/issues/8222, but disabling the optimizer does not solve the problem in this case.
On the other hand, changing the disposal method, or having a non-black color like RGB(1,1,1), is enough to avoid this bug. 

### What are your OS, Python and Pillow versions?

```sh
--------------------------------------------------------------------
Pillow 11.1.0
Python 3.13.1 (main, Dec  3 2024, 17:59:52) [Clang 16.0.0 (clang-1600.0.26.4)]
--------------------------------------------------------------------
Python executable is /Users/lucach/tmp/bin/python3
Environment Python files loaded from /Users/lucach/tmp
System Python files loaded from /opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13
--------------------------------------------------------------------
Python Pillow modules loaded from /Users/lucach/tmp/lib/python3.13/site-packages/PIL
Binary Pillow modules loaded from /Users/lucach/tmp/lib/python3.13/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.1.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.5.0
--- JPEG support ok, compiled for libjpeg-turbo 3.1.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.3
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1.zlib-ng, compiled for zlib-ng 2.2.2
--- LIBTIFF support ok, loaded 4.6.0
--- RAQM (Bidirectional Text) support ok, loaded 0.10.1, fribidi 1.0.16, harfbuzz 10.1.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
```

Hints from issue comments:
@radarhere: I've created #8748 to resolve this.

With it, your code creates

![Image](https://github.com/user-attachments/assets/6de22633-a53b-4e17-820f-b8aa4d41799d)@lucach: Looks great, thanks for the quick turnaround and the fix!

Solution patch:
diff --git a/.ci/install.sh b/.ci/install.sh
index 5c20e7f37..e61752750 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -2,12 +2,12 @@
 
 aptget_update()
 {
-    if [ ! -z $1 ]; then
+    if [ -n "$1" ]; then
         echo ""
         echo "Retrying apt-get update..."
         echo ""
     fi
-    output=`sudo apt-get update 2>&1`
+    output=$(sudo apt-get update 2>&1)
     echo "$output"
     if [[ $output == *[WE]:\ * ]]; then
         return 1
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 8faab2ef4..ef49ff332 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,7 +35,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
+        python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
         architecture: ["x64"]
         os: ["windows-latest"]
         include:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e3efe0b59..c4ad88be9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -41,6 +41,7 @@ jobs:
           "ubuntu-latest",
         ]
         python-version: [
+          "pypy3.11",
           "pypy3.10",
           "3.14",
           "3.13t",
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index dffb36085..30bebe684 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -45,7 +45,7 @@ OPENJPEG_VERSION=2.5.3
 XZ_VERSION=5.6.4
 TIFF_VERSION=4.6.0
 LCMS2_VERSION=2.16
-ZLIB_NG_VERSION=2.2.3
+ZLIB_NG_VERSION=2.2.4
 LIBWEBP_VERSION=1.5.0
 BZIP2_VERSION=1.0.8
 LIBXCB_VERSION=1.17.0
@@ -72,6 +72,14 @@ function build_zlib_ng {
         && ./configure --prefix=$BUILD_PREFIX --zlib-compat \
         && make -j4 \
         && make install)
+
+    if [ -n "$IS_MACOS" ]; then
+        # Ensure that on macOS, the library name is an absolute path, not an
+        # @rpath, so that delocate picks up the right library (and doesn't need
+        # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
+        # option to control the install_name.
+        install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
+    fi
     touch zlib-stamp
 }
 
diff --git a/Tests/helper.py b/Tests/helper.py
index e7b0db1d6..764935f87 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -9,7 +9,6 @@ import os
 import shutil
 import subprocess
 import sys
-import sysconfig
 import tempfile
 from collections.abc import Sequence
 from functools import lru_cache
@@ -342,10 +341,6 @@ def is_pypy() -> bool:
     return hasattr(sys, "pypy_translation_info")
 
 
-def is_mingw() -> bool:
-    return sysconfig.get_platform() == "mingw"
-
-
 class CachedProperty:
     def __init__(self, func: Callable[[Any], Any]) -> None:
         self.func = func
diff --git a/pyproject.toml b/pyproject.toml
index 2c6c7bcd0..aaaba0032 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -104,7 +104,6 @@ test-extras = "tests"
 
 [tool.cibuildwheel.macos.environment]
 PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
-DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib"
 
 [tool.black]
 exclude = "wheels/multibuild"
diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py
index bc1416c74..bfd66aa6a 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -26,17 +26,6 @@ from typing import BinaryIO
 
 from . import FontFile, Image
 
-bdf_slant = {
-    "R": "Roman",
-    "I": "Italic",
-    "O": "Oblique",
-    "RI": "Reverse Italic",
-    "RO": "Reverse Oblique",
-    "OT": "Other",
-}
-
-bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
-
 
 def bdf_char(
     f: BinaryIO,
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 47022d584..ff7262efc 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -689,16 +689,21 @@ def _write_multiple_frames(
                         im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
                     continue
                 if im_frames[-1].encoderinfo.get("disposal") == 2:
-                    if background_im is None:
-                        color = im.encoderinfo.get(
-                            "transparency", im.info.get("transparency", (0, 0, 0))
-                        )
-                        background = _get_background(im_frame, color)
-                        background_im = Image.new("P", im_frame.size, background)
-                        first_palette = im_frames[0].im.palette
-                        assert first_palette is not None
-                        background_im.putpalette(first_palette, first_palette.mode)
-                    bbox = _getbbox(background_im, im_frame)[1]
+                    # To appear correctly in viewers using a convention,
+                    # only consider transparency, and not background color
+                    color = im.encoderinfo.get(
+                        "transparency", im.info.get("transparency")
+                    )
+                    if color is not None:
+                        if background_im is None:
+                            background = _get_background(im_frame, color)
+                            background_im = Image.new("P", im_frame.size, background)
+                            first_palette = im_frames[0].im.palette
+                            assert first_palette is not None
+                            background_im.putpalette(first_palette, first_palette.mode)
+                        bbox = _getbbox(background_im, im_frame)[1]
+                    else:
+                        bbox = (0, 0) + im_frame.size
                 elif encoderinfo.get("optimize") and im_frame.mode != "1":
                     if "transparency" not in encoderinfo:
                         assert im_frame.palette is not None
@@ -764,7 +769,8 @@ def _write_multiple_frames(
             if not palette:
                 frame_data.encoderinfo["include_color_table"] = True
 
-            im_frame = im_frame.crop(frame_data.bbox)
+            if frame_data.bbox != (0, 0) + im_frame.size:
+                im_frame = im_frame.crop(frame_data.bbox)
             offset = frame_data.bbox[:2]
         _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
     return True
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index e723b6a2e..a5243549f 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2996,15 +2996,6 @@ class ImageTransformHandler:
 # --------------------------------------------------------------------
 # Factories
 
-#
-# Debugging
-
-
-def _wedge() -> Image:
-    """Create grayscale wedge (for debugging only)"""
-
-    return Image()._new(core.wedge("L"))
-
 
 def _check_size(size: Any) -> None:
     """
diff --git a/src/_imaging.c b/src/_imaging.c
index ee373e964..daaa56c75 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -4256,7 +4256,6 @@ static PyMethodDef functions[] = {
     {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS},
     {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS},
     {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS},
-    {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */
 
     /* Drawing support stuff */
     {"font", (PyCFunction)_font_new, METH_VARARGS},
diff --git a/src/thirdparty/pythoncapi_compat.h b/src/thirdparty/pythoncapi_compat.h
index ca23d5ffa..04fcf61e0 100644
--- a/src/thirdparty/pythoncapi_compat.h
+++ b/src/thirdparty/pythoncapi_compat.h
@@ -10,7 +10,7 @@
 // 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
+// https://github.com/python/pythoncapi-compat/commit/c84545f0e1e21757d4901f75c47333d25a3fcff0
 //
 // SPDX-License-Identifier: 0BSD
 
@@ -22,11 +22,15 @@ extern "C" {
 #endif
 
 #include <Python.h>
+#include <stddef.h>               // offsetof()
 
 // Python 3.11.0b4 added PyFrame_Back() to Python.h
 #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION)
 #  include "frameobject.h"        // PyFrameObject, PyFrame_GetBack()
 #endif
+#if PY_VERSION_HEX < 0x030C00A3
+#  include <structmember.h>       // T_SHORT, READONLY
+#endif
 
 
 #ifndef _Py_CAST
@@ -290,7 +294,7 @@ PyFrame_GetVarString(PyFrameObject *frame, const char *name)
 
 
 // bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5
-#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION)
+#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000)
 static inline PyInterpreterState *
 PyThreadState_GetInterpreter(PyThreadState *tstate)
 {
@@ -583,7 +587,7 @@ static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj)
         return 0;
     }
     *pobj = Py_NewRef(obj);
-    return (*pobj != NULL);
+    return 1;
 }
 #endif
 
@@ -921,7 +925,7 @@ static inline int
 PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg)
 {
     PyObject **dict = _PyObject_GetDictPtr(obj);
-    if (*dict == NULL) {
+    if (dict == NULL || *dict == NULL) {
         return -1;
     }
     Py_VISIT(*dict);
@@ -932,7 +936,7 @@ static inline void
 PyObject_ClearManagedDict(PyObject *obj)
 {
     PyObject **dict = _PyObject_GetDictPtr(obj);
-    if (*dict == NULL) {
+    if (dict == NULL || *dict == NULL) {
         return;
     }
     Py_CLEAR(*dict);
@@ -1207,11 +1211,11 @@ static inline int PyTime_PerfCounter(PyTime_t *result)
 #endif
 
 // gh-111389 added hash constants to Python 3.13.0a5. These constants were
-// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9.
+// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8.
 #if (!defined(PyHASH_BITS) \
      && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \
          || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \
-             && PYPY_VERSION_NUM >= 0x07090000)))
+             && PYPY_VERSION_NUM >= 0x07030800)))
 #  define PyHASH_BITS _PyHASH_BITS
 #  define PyHASH_MODULUS _PyHASH_MODULUS
 #  define PyHASH_INF _PyHASH_INF
@@ -1523,6 +1527,36 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign)
 }
 #endif
 
+// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2
+#if PY_VERSION_HEX < 0x030E00A2
+static inline int PyLong_IsPositive(PyObject *obj)
+{
+    if (!PyLong_Check(obj)) {
+        PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name);
+        return -1;
+    }
+    return _PyLong_Sign(obj) == 1;
+}
+
+static inline int PyLong_IsNegative(PyObject *obj)
+{
+    if (!PyLong_Check(obj)) {
+        PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name);
+        return -1;
+    }
+    return _PyLong_Sign(obj) == -1;
+}
+
+static inline int PyLong_IsZero(PyObject *obj)
+{
+    if (!PyLong_Check(obj)) {
+        PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name);
+        return -1;
+    }
+    return _PyLong_Sign(obj) == 0;
+}
+#endif
+
 
 // gh-124502 added PyUnicode_Equal() to Python 3.14.0a0
 #if PY_VERSION_HEX < 0x030E00A0
@@ -1693,6 +1727,479 @@ static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue)
 #endif
 
 
+// gh-102471 added import and export API for integers to 3.14.0a2.
+#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION)
+// Helpers to access PyLongObject internals.
+static inline void
+_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size)
+{
+#if PY_VERSION_HEX >= 0x030C0000
+    op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3);
+#elif PY_VERSION_HEX >= 0x030900A4
+    Py_SET_SIZE(op, sign * size);
+#else
+    Py_SIZE(op) = sign * size;
+#endif
+}
+
+static inline Py_ssize_t
+_PyLong_DigitCount(const PyLongObject *op)
+{
+#if PY_VERSION_HEX >= 0x030C0000
+    return (Py_ssize_t)(op->long_value.lv_tag >> 3);
+#else
+    return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op);
+#endif
+}
+
+static inline digit*
+_PyLong_GetDigits(const PyLongObject *op)
+{
+#if PY_VERSION_HEX >= 0x030C0000
+    return (digit*)(op->long_value.ob_digit);
+#else
+    return (digit*)(op->ob_digit);
+#endif
+}
+
+typedef struct PyLongLayout {
+    uint8_t bits_per_digit;
+    uint8_t digit_size;
+    int8_t digits_order;
+    int8_t digit_endianness;
+} PyLongLayout;
+
+typedef struct PyLongExport {
+    int64_t value;
+    uint8_t negative;
+    Py_ssize_t ndigits;
+    const void *digits;
+    Py_uintptr_t _reserved;
+} PyLongExport;
+
+typedef struct PyLongWriter PyLongWriter;
+
+static inline const PyLongLayout*
+PyLong_GetNativeLayout(void)
+{
+    static const PyLongLayout PyLong_LAYOUT = {
+        PyLong_SHIFT,
+        sizeof(digit),
+        -1,  // least significant first
+        PY_LITTLE_ENDIAN ? -1 : 1,
+    };
+
+    return &PyLong_LAYOUT;
+}
+
+static inline int
+PyLong_Export(PyObject *obj, PyLongExport *export_long)
+{
+    if (!PyLong_Check(obj)) {
+        memset(export_long, 0, sizeof(*export_long));
+        PyErr_Format(PyExc_TypeError, "expected int, got %s",
+                     Py_TYPE(obj)->tp_name);
+        return -1;
+    }
+
+    // Fast-path: try to convert to a int64_t
+    PyLongObject *self = (PyLongObject*)obj;
+    int overflow;
+#if SIZEOF_LONG == 8
+    long value = PyLong_AsLongAndOverflow(obj, &overflow);
+#else
+    // Windows has 32-bit long, so use 64-bit long long instead
+    long long value = PyLong_AsLongLongAndOverflow(obj, &overflow);
+#endif
+    Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t));
+    // the function cannot fail since obj is a PyLongObject
+    assert(!(value == -1 && PyErr_Occurred()));
+
+    if (!overflow) {
+        export_long->value = value;
+        export_long->negative = 0;
+        export_long->ndigits = 0;
+        export_long->digits = 0;
+        export_long->_reserved = 0;
+    }
+    else {
+        export_long->value = 0;
+        export_long->negative = _PyLong_Sign(obj) < 0;
+        export_long->ndigits = _PyLong_DigitCount(self);
+        if (export_long->ndigits == 0) {
+            export_long->ndigits = 1;
+        }
+        export_long->digits = _PyLong_GetDigits(self);
+        export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj);
+    }
+    return 0;
+}
+
+static inline void
+PyLong_FreeExport(PyLongExport *export_long)
+{
+    PyObject *obj = (PyObject*)export_long->_reserved;
+
+    if (obj) {
+        export_long->_reserved = 0;
+        Py_DECREF(obj);
+    }
+}
+
+static inline PyLongWriter*
+PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits)
+{
+    if (ndigits <= 0) {
+        PyErr_SetString(PyExc_ValueError, "ndigits must be positive");
+        return NULL;
+    }
+    assert(digits != NULL);
+
+    PyLongObject *obj = _PyLong_New(ndigits);
+    if (obj == NULL) {
+        return NULL;
+    }
+    _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits);
+
+    *digits = _PyLong_GetDigits(obj);
+    return (PyLongWriter*)obj;
+}
+
+static inline void
+PyLongWriter_Discard(PyLongWriter *writer)
+{
+    PyLongObject *obj = (PyLongObject *)writer;
+
+    assert(Py_REFCNT(obj) == 1);
+    Py_DECREF(obj);
+}
+
+static inline PyObject*
+PyLongWriter_Finish(PyLongWriter *writer)
+{
+    PyObject *obj = (PyObject *)writer;
+    PyLongObject *self = (PyLongObject*)obj;
+    Py_ssize_t j = _PyLong_DigitCount(self);
+    Py_ssize_t i = j;
+    int sign = _PyLong_Sign(obj);
+
+    assert(Py_REFCNT(obj) == 1);
+
+    // Normalize and get singleton if possible
+    while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) {
+        --i;
+    }
+    if (i != j) {
+        if (i == 0) {
+            sign = 0;
+        }
+        _PyLong_SetSignAndDigitCount(self, sign, i);
+    }
+    if (i <= 1) {
+        long val = sign * (long)(_PyLong_GetDigits(self)[0]);
+        Py_DECREF(obj);
+        return PyLong_FromLong(val);
+    }
+
+    return obj;
+}
+#endif
+
+
+#if PY_VERSION_HEX < 0x030C00A3
+#  define Py_T_SHORT      T_SHORT
+#  define Py_T_INT        T_INT
+#  define Py_T_LONG       T_LONG
+#  define Py_T_FLOAT      T_FLOAT
+#  define Py_T_DOUBLE     T_DOUBLE
+#  define Py_T_STRING     T_STRING
+#  define _Py_T_OBJECT    T_OBJECT
+#  define Py_T_CHAR       T_CHAR
+#  define Py_T_BYTE       T_BYTE
+#  define Py_T_UBYTE      T_UBYTE
+#  define Py_T_USHORT     T_USHORT
+#  define Py_T_UINT       T_UINT
+#  define Py_T_ULONG      T_ULONG
+#  define Py_T_STRING_INPLACE  T_STRING_INPLACE
+#  define Py_T_BOOL       T_BOOL
+#  define Py_T_OBJECT_EX  T_OBJECT_EX
+#  define Py_T_LONGLONG   T_LONGLONG
+#  define Py_T_ULONGLONG  T_ULONGLONG
+#  define Py_T_PYSSIZET   T_PYSSIZET
+
+#  if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION)
+#    define _Py_T_NONE      T_NONE
+#  endif
+
+#  define Py_READONLY            READONLY
+#  define Py_AUDIT_READ          READ_RESTRICTED
+#  define _Py_WRITE_RESTRICTED   PY_WRITE_RESTRICTED
+#endif
+
+
+// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4
+#if PY_VERSION_HEX < 0x030E00A4
+static inline FILE* Py_fopen(PyObject *path, const char *mode)
+{
+#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION)
+    PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode);
+
+    return _Py_fopen_obj(path, mode);
+#else
+    FILE *f;
+    PyObject *bytes;
+#if PY_VERSION_HEX >= 0x03000000
+    if (!PyUnicode_FSConverter(path, &bytes)) {
+        return NULL;
+    }
+#else
+    if (!PyString_Check(path)) {
+        PyErr_SetString(PyExc_TypeError, "except str");
+        return NULL;
+    }
+    bytes = Py_NewRef(path);
+#endif
+    const char *path_bytes = PyBytes_AS_STRING(bytes);
+
+    f = fopen(path_bytes, mode);
+    Py_DECREF(bytes);
+
+    if (f == NULL) {
+        PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path);
+        return NULL;
+    }
+    return f;
+#endif
+}
+
+static inline int Py_fclose(FILE *file)
+{
+    return fclose(file);
+}
+#endif
+
+
+#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION)
+static inline PyObject*
+PyConfig_Get(const char *name)
+{
+    typedef enum {
+        _PyConfig_MEMBER_INT,
+        _PyConfig_MEMBER_UINT,
+        _PyConfig_MEMBER_ULONG,
+        _PyConfig_MEMBER_BOOL,
+        _PyConfig_MEMBER_WSTR,
+        _PyConfig_MEMBER_WSTR_OPT,
+        _PyConfig_MEMBER_WSTR_LIST,
+    } PyConfigMemberType;
+
+    typedef struct {
+        const char *name;
+        size_t offset;
+        PyConfigMemberType type;
+        const char *sys_attr;
+    } PyConfigSpec;
+
+#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \
+    {#MEMBER, offsetof(PyConfig, MEMBER), \
+     _PyConfig_MEMBER_##TYPE, sys_attr}
+
+    static const PyConfigSpec config_spec[] = {
+        PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"),
+        PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"),
+        PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"),
+        PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"),
+        PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"),
+        PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"),
+        PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL),
+#if 0x030C0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"),
+        PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"),
+        PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"),
+        PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"),
+        PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL),
+#if 0x030B0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"),
+        PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"),
+        PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL),
+#if 0x030B0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL),
+#if 0x030D0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL),
+#if 0x030B0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL),
+#endif
+#ifdef Py_GIL_DISABLED
+        PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL),
+#ifdef MS_WINDOWS
+        PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL),
+#if 0x030A0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL),
+#if 0x030C0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL),
+#if 0x030B0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL),
+#if 0x030B0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL),
+#endif
+        PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL),
+        PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL),
+#if 0x030A0000 <= PY_VERSION_HEX
+        PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL),
+#endif
+    };
+
+#undef PYTHONCAPI_COMPAT_SPEC
+
+    const PyConfigSpec *spec;
+    int found = 0;
+    for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) {
+        spec = &config_spec[i];
+        if (strcmp(spec->name, name) == 0) {
+            found = 1;
+            break;
+        }
+    }
+    if (found) {
+        if (spec->sys_attr != NULL) {
+            PyObject *value = PySys_GetObject(spec->sys_attr);
+            if (value == NULL) {
+                PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr);
+                return NULL;
+            }
+            return Py_NewRef(value);
+        }
+
+        PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void);
+
+        const PyConfig *config = _Py_GetConfig();
+        void *member = (char *)config + spec->offset;
+        switch (spec->type) {
+        case _PyConfig_MEMBER_INT:
+        case _PyConfig_MEMBER_UINT:
+        {
+            int value = *(int *)member;
+            return PyLong_FromLong(value);
+        }
+        case _PyConfig_MEMBER_BOOL:
+        {
+            int value = *(int *)member;
+            return PyBool_FromLong(value != 0);
+        }
+        case _PyConfig_MEMBER_ULONG:
+        {
+            unsigned long value = *(unsigned long *)member;
+            return PyLong_FromUnsignedLong(value);
+        }
+        case _PyConfig_MEMBER_WSTR:
+        case _PyConfig_MEMBER_WSTR_OPT:
+        {
+            wchar_t *wstr = *(wchar_t **)member;
+            if (wstr != NULL) {
+                return PyUnicode_FromWideChar(wstr, -1);
+            }
+            else {
+                return Py_NewRef(Py_None);
+            }
+        }
+        case _PyConfig_MEMBER_WSTR_LIST:
+        {
+            const PyWideStringList *list = (const PyWideStringList *)member;
+            PyObject *tuple = PyTuple_New(list->length);
+            if (tuple == NULL) {
+                return NULL;
+            }
+
+            for (Py_ssize_t i = 0; i < list->length; i++) {
+                PyObject *item = PyUnicode_FromWideChar(list->items[i], -1);
+                if (item == NULL) {
+                    Py_DECREF(tuple);
+                    return NULL;
+                }
+                PyTuple_SET_ITEM(tuple, i, item);
+            }
+            return tuple;
+        }
+        default:
+            Py_UNREACHABLE();
+        }
+    }
+
+    PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name);
+    return NULL;
+}
+
+static inline int
+PyConfig_GetInt(const char *name, int *value)
+{
+    PyObject *obj = PyConfig_Get(name);
+    if (obj == NULL) {
+        return -1;
+    }
+
+    if (!PyLong_Check(obj)) {
+        Py_DECREF(obj);
+        PyErr_Format(PyExc_TypeError, "config option %s is not an int", name);
+        return -1;
+    }
+
+    int as_int = PyLong_AsInt(obj);
+    Py_DECREF(obj);
+    if (as_int == -1 && PyErr_Occurred()) {
+        PyErr_Format(PyExc_OverflowError,
+                     "config option %s value does not fit into a C int", name);
+        return -1;
+    }
+
+    *value = as_int;
+    return 0;
+}
+#endif  // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION)
+
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/tox.ini b/tox.ini
index e79d88500..4065245ee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,12 +11,8 @@ deps =
 extras =
     tests
 commands =
-    make clean
-    {envpython} -m pip install .
     {envpython} selftest.py
     {envpython} -m pytest -W always {posargs}
-allowlist_externals =
-    make
 
 [testenv:lint]
 skip_install = true
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 54b5d983f..f942716cb 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -116,12 +116,13 @@ V = {
     "HARFBUZZ": "10.2.0",
     "JPEGTURBO": "3.1.0",
     "LCMS2": "2.16",
+    "LIBIMAGEQUANT": "4.3.4",
     "LIBPNG": "1.6.46",
     "LIBWEBP": "1.5.0",
     "OPENJPEG": "2.5.3",
     "TIFF": "4.6.0",
     "XZ": "5.6.4",
-    "ZLIBNG": "2.2.3",
+    "ZLIBNG": "2.2.4",
 }
 V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
 
@@ -335,24 +336,15 @@ DEPS: dict[str, dict[str, Any]] = {
         "libs": [r"bin\*.lib"],
     },
     "libimagequant": {
-        # commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
-        "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
-        "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
+        "url": "https://github.com/ImageOptim/libimagequant/archive/{V['LIBIMAGEQUANT']}.tar.gz",
+        "filename": f"libimagequant-{V['LIBIMAGEQUANT']}.tar.gz",
         "license": "COPYRIGHT",
-        "patch": {
-            "CMakeLists.txt": {
-                "if(OPENMP_FOUND)": "if(false)",
-                "install": "#install",
-                # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly
-                "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)",  # noqa: E501
-            }
-        },
         "build": [
-            *cmds_cmake("imagequant_a"),
-            cmd_copy("imagequant_a.lib", "imagequant.lib"),
+            cmd_cd("imagequant-sys"),
+            "cargo build --release",
         ],
-        "headers": [r"*.h"],
-        "libs": [r"imagequant.lib"],
+        "headers": ["libimagequant.h"],
+        "libs": [r"..\target\release\imagequant_sys.lib"],
     },
     "harfbuzz": {
         "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip",


Test patch:
diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py
index 5deacd878..ab6b9f983 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -26,12 +26,12 @@ def test_sanity() -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(TEST_FILE)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index 876561a88..8adbd30f5 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -52,12 +52,12 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(static_test_file)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 46215db1f..da3210c3a 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -22,9 +22,6 @@ from .helper import (
 # sample gif stream
 TEST_GIF = "Tests/images/hopper.gif"
 
-with open(TEST_GIF, "rb") as f:
-    data = f.read()
-
 
 def test_sanity() -> None:
     with Image.open(TEST_GIF) as im:
@@ -37,12 +34,12 @@ def test_sanity() -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(TEST_GIF)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
@@ -762,6 +759,21 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
         assert im.getpixel((0, 0)) == (0, 0, 0, 255)
 
 
+def test_dispose2_without_transparency(tmp_path: Path) -> None:
+    out = str(tmp_path / "temp.gif")
+
+    im = Image.new("P", (100, 100))
+
+    im2 = Image.new("P", (100, 100), (0, 0, 0))
+    im2.putpixel((50, 50), (255, 0, 0))
+
+    im.save(out, save_all=True, append_images=[im2], disposal=2)
+
+    with Image.open(out) as reloaded:
+        reloaded.seek(1)
+        assert reloaded.tile[0].extents == (0, 0, 100, 100)
+
+
 def test_transparency_in_second_frame(tmp_path: Path) -> None:
     out = str(tmp_path / "temp.gif")
     with Image.open("Tests/images/different_transparency.gif") as im:
@@ -1345,32 +1357,30 @@ def test_save_I(tmp_path: Path) -> None:
         assert_image_equal(reloaded.convert("L"), im.convert("L"))
 
 
-def test_getdata() -> None:
+def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
     # Test getheader/getdata against legacy values.
     # Create a 'P' image with holes in the palette.
-    im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
+    im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST)
     im.putpalette(ImagePalette.ImagePalette("RGB"))
     im.info = {"background": 0}
 
     passed_palette = bytes(255 - i // 3 for i in range(768))
 
-    GifImagePlugin._FORCE_OPTIMIZE = True
-    try:
-        h = GifImagePlugin.getheader(im, passed_palette)
-        d = GifImagePlugin.getdata(im)
+    monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True)
+
+    h = GifImagePlugin.getheader(im, passed_palette)
+    d = GifImagePlugin.getdata(im)
 
-        import pickle
+    import pickle
 
-        # Enable to get target values on pre-refactor version
-        # with open('Tests/images/gif_header_data.pkl', 'wb') as f:
-        #    pickle.dump((h, d), f, 1)
-        with open("Tests/images/gif_header_data.pkl", "rb") as f:
-            (h_target, d_target) = pickle.load(f)
+    # Enable to get target values on pre-refactor version
+    # with open('Tests/images/gif_header_data.pkl', 'wb') as f:
+    #    pickle.dump((h, d), f, 1)
+    with open("Tests/images/gif_header_data.pkl", "rb") as f:
+        (h_target, d_target) = pickle.load(f)
 
-        assert h == h_target
-        assert d == d_target
-    finally:
-        GifImagePlugin._FORCE_OPTIMIZE = False
+    assert h == h_target
+    assert d == d_target
 
 
 def test_lzw_bits() -> None:
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index 1d3fa485f..d29998801 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(TEST_IM)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index 66fa29177..311085cf7 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -38,12 +38,12 @@ def test_sanity(test_file: str) -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(test_files[0])
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 5f22001f3..1793c269d 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -25,12 +25,12 @@ def test_sanity() -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(test_file)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 713db848d..cdb7b3e0b 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -24,12 +24,12 @@ def test_sanity() -> None:
 
 @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
 def test_unclosed_file() -> None:
-    def open() -> None:
+    def open_test_image() -> None:
         im = Image.open(TEST_FILE)
         im.load()
 
     with pytest.warns(ResourceWarning):
-        open()
+        open_test_image()
 
 
 def test_closed_file() -> None:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index fe8f69848..a8a407963 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -63,12 +63,12 @@ class TestFileTiff:
 
     @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
     def test_unclosed_file(self) -> None:
-        def open() -> None:
+        def open_test_image() -> None:
             im = Image.open("Tests/images/multipage.tiff")
             im.load()
 
         with pytest.warns(ResourceWarning):
-            open()
+            open_test_image()
 
     def test_closed_file(self) -> None:
         with warnings.catch_warnings():
diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py
index c07024a2c..9cbf18566 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -22,28 +22,26 @@ def test_sanity() -> None:
     Image.new("HSV", (100, 100))
 
 
-def wedge() -> Image.Image:
-    w = Image._wedge()
-    w90 = w.rotate(90)
+def linear_gradient() -> Image.Image:
+    im = Image.linear_gradient(mode="L")
+    im90 = im.rotate(90)
 
-    (px, h) = w.size
+    (px, h) = im.size
 
     r = Image.new("L", (px * 3, h))
     g = r.copy()
     b = r.copy()
 
-    r.paste(w, (0, 0))
-    r.paste(w90, (px, 0))
+    r.paste(im, (0, 0))
+    r.paste(im90, (px, 0))
 
-    g.paste(w90, (0, 0))
-    g.paste(w, (2 * px, 0))
+    g.paste(im90, (0, 0))
+    g.paste(im, (2 * px, 0))
 
-    b.paste(w, (px, 0))
-    b.paste(w90, (2 * px, 0))
+    b.paste(im, (px, 0))
+    b.paste(im90, (2 * px, 0))
 
-    img = Image.merge("RGB", (r, g, b))
-
-    return img
+    return Image.merge("RGB", (r, g, b))
 
 
 def to_xxx_colorsys(
@@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image:
     return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
 
 
-def test_wedge() -> None:
-    src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
+def test_linear_gradient() -> None:
+    src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR)
     im = src.convert("HSV")
     comparable = to_hsv_colorsys(src)