Merge branch 'use-ptr' into use-ptr

This commit is contained in:
Alexander Karpinsky 2024-09-08 17:59:35 +04:00 committed by GitHub
commit 4318834775
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 229 additions and 155 deletions

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347 - Return early from BoxBlur if either width or height is zero #8347
[radarhere] [radarhere]

View File

@ -1011,7 +1011,9 @@ class TestFileJpeg:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder): class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0 return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder) Image.register_decoder("INFINITE", InfiniteMockPyDecoder)

View File

@ -775,6 +775,14 @@ class TestImage:
exif.load(b"Exif\x00\x00") exif.load(b"Exif\x00\x00")
assert not dict(exif) assert not dict(exif)
def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]
exif = im.getexif()
assert exif[274] == 1
def test_empty_get_ifd(self) -> None: def test_empty_get_ifd(self) -> None:
exif = Image.Exif() exif = Image.Exif()
ifd = exif.get_ifd(0x8769) ifd = exif.get_ifd(0x8769)

View File

@ -210,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0

View File

@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name # generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or # if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH'). # ('envvar', 'LD_LIBRARY_PATH').
# nitpick_ignore = [] nitpick_ignore = [("py:class", "_io.BytesIO")]
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------

View File

@ -152,6 +152,13 @@ Removed features
Deprecated features are only removed in major releases after an appropriate Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed. period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0
``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting.
PSFile PSFile
~~~~~~ ~~~~~~

View File

@ -257,7 +257,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DXT1Decoder(ImageFile.PyDecoder): class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
try: try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
@ -270,7 +270,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
try: try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))

View File

@ -33,6 +33,10 @@ Internal Modules
Provides a convenient way to import type hints that are not available Provides a convenient way to import type hints that are not available
on some Python versions. on some Python versions.
.. py:class:: Buffer
Typing alias.
.. py:class:: IntegralLike .. py:class:: IntegralLike
Typing alias. Typing alias.

View File

@ -40,6 +40,11 @@ removed. Pillow's C API will now be used on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was ``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
similarly removed. similarly removed.
TiffImagePlugin IFD_LEGACY_API
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed.
Deprecations Deprecations
============ ============

249
setup.py
View File

@ -15,18 +15,20 @@ import struct
import subprocess import subprocess
import sys import sys
import warnings import warnings
from collections.abc import Iterator
from typing import Any
from setuptools import Extension, setup from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
def get_version(): def get_version() -> str:
version_file = "src/PIL/_version.py" version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f: with open(version_file, encoding="utf-8") as f:
return f.read().split('"')[1] return f.read().split('"')[1]
configuration = {} configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version() PILLOW_VERSION = get_version()
@ -143,7 +145,7 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
def _dbg(s, tp=None): def _dbg(s: str, tp: Any = None) -> None:
if DEBUG: if DEBUG:
if tp: if tp:
print(s % tp) print(s % tp)
@ -151,10 +153,13 @@ def _dbg(s, tp=None):
print(s) print(s)
def _find_library_dirs_ldconfig(): def _find_library_dirs_ldconfig() -> list[str]:
# Based on ctypes.util from Python 2 # Based on ctypes.util from Python 2
ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
args: list[str]
env: dict[str, str]
expr: str
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4: if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32" machine = os.uname()[4] + "-32"
@ -184,13 +189,11 @@ def _find_library_dirs_ldconfig():
try: try:
p = subprocess.Popen( p = subprocess.Popen(
args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True
) )
except OSError: # E.g. command not found except OSError: # E.g. command not found
return [] return []
[data, _] = p.communicate() data = p.communicate()[0]
if isinstance(data, bytes):
data = data.decode("latin1")
dirs = [] dirs = []
for dll in re.findall(expr, data): for dll in re.findall(expr, data):
@ -200,7 +203,9 @@ def _find_library_dirs_ldconfig():
return dirs return dirs
def _add_directory(path, subdir, where=None): def _add_directory(
path: list[str], subdir: str | None, where: int | None = None
) -> None:
if subdir is None: if subdir is None:
return return
subdir = os.path.realpath(subdir) subdir = os.path.realpath(subdir)
@ -216,7 +221,7 @@ def _add_directory(path, subdir, where=None):
path.insert(where, subdir) path.insert(where, subdir)
def _find_include_file(self, include): def _find_include_file(self: pil_build_ext, include: str) -> int:
for directory in self.compiler.include_dirs: for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory)) _dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)): if os.path.isfile(os.path.join(directory, include)):
@ -225,7 +230,7 @@ def _find_include_file(self, include):
return 0 return 0
def _find_library_file(self, library): def _find_library_file(self: pil_build_ext, library: str) -> str | None:
ret = self.compiler.find_library_file(self.compiler.library_dirs, library) ret = self.compiler.find_library_file(self.compiler.library_dirs, library)
if ret: if ret:
_dbg("Found library %s at %s", (library, ret)) _dbg("Found library %s at %s", (library, ret))
@ -234,7 +239,7 @@ def _find_library_file(self, library):
return ret return ret
def _find_include_dir(self, dirname, include): def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str:
for directory in self.compiler.include_dirs: for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory)) _dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)): if os.path.isfile(os.path.join(directory, include)):
@ -245,6 +250,7 @@ def _find_include_dir(self, dirname, include):
if os.path.isfile(os.path.join(subdir, include)): if os.path.isfile(os.path.join(subdir, include)):
_dbg("Found %s in %s", (include, subdir)) _dbg("Found %s in %s", (include, subdir))
return subdir return subdir
return False
def _cmd_exists(cmd: str) -> bool: def _cmd_exists(cmd: str) -> bool:
@ -256,7 +262,7 @@ def _cmd_exists(cmd: str) -> bool:
) )
def _pkg_config(name): def _pkg_config(name: str) -> tuple[list[str], list[str]] | None:
command = os.environ.get("PKG_CONFIG", "pkg-config") command = os.environ.get("PKG_CONFIG", "pkg-config")
for keep_system in (True, False): for keep_system in (True, False):
try: try:
@ -283,10 +289,11 @@ def _pkg_config(name):
return libs, cflags return libs, cflags
except Exception: except Exception:
pass pass
return None
class pil_build_ext(build_ext): class pil_build_ext(build_ext):
class feature: class ext_feature:
features = [ features = [
"zlib", "zlib",
"jpeg", "jpeg",
@ -301,25 +308,32 @@ class pil_build_ext(build_ext):
] ]
required = {"jpeg", "zlib"} required = {"jpeg", "zlib"}
vendor = set() vendor: set[str] = set()
def __init__(self): def __init__(self) -> None:
self._settings: dict[str, str | bool | None] = {}
for f in self.features: for f in self.features:
setattr(self, f, None) self.set(f, None)
def require(self, feat): def require(self, feat: str) -> bool:
return feat in self.required return feat in self.required
def want(self, feat): def get(self, feat: str) -> str | bool | None:
return getattr(self, feat) is None return self._settings[feat]
def want_vendor(self, feat): def set(self, feat: str, value: str | bool | None) -> None:
self._settings[feat] = value
def want(self, feat: str) -> bool:
return self._settings[feat] is None
def want_vendor(self, feat: str) -> bool:
return feat in self.vendor return feat in self.vendor
def __iter__(self): def __iter__(self) -> Iterator[str]:
yield from self.features yield from self.features
feature = feature() feature = ext_feature()
user_options = ( user_options = (
build_ext.user_options build_ext.user_options
@ -337,10 +351,10 @@ class pil_build_ext(build_ext):
) )
@staticmethod @staticmethod
def check_configuration(option, value): def check_configuration(option: str, value: str) -> bool | None:
return True if value in configuration.get(option, []) else None return True if value in configuration.get(option, []) else None
def initialize_options(self): def initialize_options(self) -> None:
self.disable_platform_guessing = self.check_configuration( self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable" "platform-guessing", "disable"
) )
@ -355,7 +369,7 @@ class pil_build_ext(build_ext):
self.debug = True self.debug = True
self.parallel = configuration.get("parallel", [None])[-1] self.parallel = configuration.get("parallel", [None])[-1]
def finalize_options(self): def finalize_options(self) -> None:
build_ext.finalize_options(self) build_ext.finalize_options(self)
if self.debug: if self.debug:
global DEBUG global DEBUG
@ -363,12 +377,16 @@ class pil_build_ext(build_ext):
if not self.parallel: if not self.parallel:
# If --parallel (or -j) wasn't specified, we want to reproduce the same # If --parallel (or -j) wasn't specified, we want to reproduce the same
# behavior as before, that is, auto-detect the number of jobs. # behavior as before, that is, auto-detect the number of jobs.
self.parallel = None
cpu_count = os.cpu_count()
if cpu_count is not None:
try: try:
self.parallel = int( self.parallel = int(
os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count())) os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
) )
except TypeError: except TypeError:
self.parallel = None pass
for x in self.feature: for x in self.feature:
if getattr(self, f"disable_{x}"): if getattr(self, f"disable_{x}"):
setattr(self.feature, x, False) setattr(self.feature, x, False)
@ -402,7 +420,13 @@ class pil_build_ext(build_ext):
_dbg("Using vendored version of %s", x) _dbg("Using vendored version of %s", x)
self.feature.vendor.add(x) self.feature.vendor.add(x)
def _update_extension(self, name, libraries, define_macros=None, sources=None): def _update_extension(
self,
name: str,
libraries: list[str] | list[str | bool | None],
define_macros: list[tuple[str, str | None]] | None = None,
sources: list[str] | None = None,
) -> None:
for extension in self.extensions: for extension in self.extensions:
if extension.name == name: if extension.name == name:
extension.libraries += libraries extension.libraries += libraries
@ -415,13 +439,13 @@ class pil_build_ext(build_ext):
extension.extra_link_args = ["--stdlib=libc++"] extension.extra_link_args = ["--stdlib=libc++"]
break break
def _remove_extension(self, name): def _remove_extension(self, name: str) -> None:
for extension in self.extensions: for extension in self.extensions:
if extension.name == name: if extension.name == name:
self.extensions.remove(extension) self.extensions.remove(extension)
break break
def get_macos_sdk_path(self): def get_macos_sdk_path(self) -> str | None:
try: try:
sdk_path = ( sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"]) subprocess.check_output(["xcrun", "--show-sdk-path"])
@ -442,9 +466,9 @@ class pil_build_ext(build_ext):
sdk_path = commandlinetools_sdk_path sdk_path = commandlinetools_sdk_path
return sdk_path return sdk_path
def build_extensions(self): def build_extensions(self) -> None:
library_dirs = [] library_dirs: list[str] = []
include_dirs = [] include_dirs: list[str] = []
pkg_config = None pkg_config = None
if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")):
@ -468,19 +492,22 @@ class pil_build_ext(build_ext):
root = globals()[root_name] root = globals()[root_name]
if root is None and root_name in os.environ: if root is None and root_name in os.environ:
prefix = os.environ[root_name] root_prefix = os.environ[root_name]
root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include")) root = (
os.path.join(root_prefix, "lib"),
os.path.join(root_prefix, "include"),
)
if root is None and pkg_config: if root is None and pkg_config:
if isinstance(lib_name, tuple): if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
else:
for lib_name2 in lib_name: for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.") _dbg(f"Looking for `{lib_name2}` using pkg-config.")
root = pkg_config(lib_name2) root = pkg_config(lib_name2)
if root: if root:
break break
else:
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
if isinstance(root, tuple): if isinstance(root, tuple):
lib_root, include_root = root lib_root, include_root = root
@ -660,22 +687,22 @@ class pil_build_ext(build_ext):
_dbg("Looking for zlib") _dbg("Looking for zlib")
if _find_include_file(self, "zlib.h"): if _find_include_file(self, "zlib.h"):
if _find_library_file(self, "z"): if _find_library_file(self, "z"):
feature.zlib = "z" feature.set("zlib", "z")
elif sys.platform == "win32" and _find_library_file(self, "zlib"): elif sys.platform == "win32" and _find_library_file(self, "zlib"):
feature.zlib = "zlib" # alternative name feature.set("zlib", "zlib") # alternative name
if feature.want("jpeg"): if feature.want("jpeg"):
_dbg("Looking for jpeg") _dbg("Looking for jpeg")
if _find_include_file(self, "jpeglib.h"): if _find_include_file(self, "jpeglib.h"):
if _find_library_file(self, "jpeg"): if _find_library_file(self, "jpeg"):
feature.jpeg = "jpeg" feature.set("jpeg", "jpeg")
elif sys.platform == "win32" and _find_library_file(self, "libjpeg"): elif sys.platform == "win32" and _find_library_file(self, "libjpeg"):
feature.jpeg = "libjpeg" # alternative name feature.set("jpeg", "libjpeg") # alternative name
feature.openjpeg_version = None feature.set("openjpeg_version", None)
if feature.want("jpeg2000"): if feature.want("jpeg2000"):
_dbg("Looking for jpeg2000") _dbg("Looking for jpeg2000")
best_version = None best_version: tuple[int, ...] | None = None
best_path = None best_path = None
# Find the best version # Find the best version
@ -705,26 +732,26 @@ class pil_build_ext(build_ext):
# <openjpeg.h> rather than having to cope with the versioned # <openjpeg.h> rather than having to cope with the versioned
# include path # include path
_add_directory(self.compiler.include_dirs, best_path, 0) _add_directory(self.compiler.include_dirs, best_path, 0)
feature.jpeg2000 = "openjp2" feature.set("jpeg2000", "openjp2")
feature.openjpeg_version = ".".join(str(x) for x in best_version) feature.set("openjpeg_version", ".".join(str(x) for x in best_version))
if feature.want("imagequant"): if feature.want("imagequant"):
_dbg("Looking for imagequant") _dbg("Looking for imagequant")
if _find_include_file(self, "libimagequant.h"): if _find_include_file(self, "libimagequant.h"):
if _find_library_file(self, "imagequant"): if _find_library_file(self, "imagequant"):
feature.imagequant = "imagequant" feature.set("imagequant", "imagequant")
elif _find_library_file(self, "libimagequant"): elif _find_library_file(self, "libimagequant"):
feature.imagequant = "libimagequant" feature.set("imagequant", "libimagequant")
if feature.want("tiff"): if feature.want("tiff"):
_dbg("Looking for tiff") _dbg("Looking for tiff")
if _find_include_file(self, "tiff.h"): if _find_include_file(self, "tiff.h"):
if _find_library_file(self, "tiff"): if _find_library_file(self, "tiff"):
feature.tiff = "tiff" feature.set("tiff", "tiff")
if sys.platform in ["win32", "darwin"] and _find_library_file( if sys.platform in ["win32", "darwin"] and _find_library_file(
self, "libtiff" self, "libtiff"
): ):
feature.tiff = "libtiff" feature.set("tiff", "libtiff")
if feature.want("freetype"): if feature.want("freetype"):
_dbg("Looking for freetype") _dbg("Looking for freetype")
@ -745,31 +772,31 @@ class pil_build_ext(build_ext):
freetype_version = 21 freetype_version = 21
break break
if freetype_version: if freetype_version:
feature.freetype = "freetype" feature.set("freetype", "freetype")
if subdir: if subdir:
_add_directory(self.compiler.include_dirs, subdir, 0) _add_directory(self.compiler.include_dirs, subdir, 0)
if feature.freetype and feature.want("raqm"): if feature.get("freetype") and feature.want("raqm"):
if not feature.want_vendor("raqm"): # want system Raqm if not feature.want_vendor("raqm"): # want system Raqm
_dbg("Looking for Raqm") _dbg("Looking for Raqm")
if _find_include_file(self, "raqm.h"): if _find_include_file(self, "raqm.h"):
if _find_library_file(self, "raqm"): if _find_library_file(self, "raqm"):
feature.raqm = "raqm" feature.set("raqm", "raqm")
elif _find_library_file(self, "libraqm"): elif _find_library_file(self, "libraqm"):
feature.raqm = "libraqm" feature.set("raqm", "libraqm")
else: # want to build Raqm from src/thirdparty else: # want to build Raqm from src/thirdparty
_dbg("Looking for HarfBuzz") _dbg("Looking for HarfBuzz")
feature.harfbuzz = None feature.set("harfbuzz", None)
hb_dir = _find_include_dir(self, "harfbuzz", "hb.h") hb_dir = _find_include_dir(self, "harfbuzz", "hb.h")
if hb_dir: if hb_dir:
if isinstance(hb_dir, str): if isinstance(hb_dir, str):
_add_directory(self.compiler.include_dirs, hb_dir, 0) _add_directory(self.compiler.include_dirs, hb_dir, 0)
if _find_library_file(self, "harfbuzz"): if _find_library_file(self, "harfbuzz"):
feature.harfbuzz = "harfbuzz" feature.set("harfbuzz", "harfbuzz")
if feature.harfbuzz: if feature.get("harfbuzz"):
if not feature.want_vendor("fribidi"): # want system FriBiDi if not feature.want_vendor("fribidi"): # want system FriBiDi
_dbg("Looking for FriBiDi") _dbg("Looking for FriBiDi")
feature.fribidi = None feature.set("fribidi", None)
fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h") fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h")
if fribidi_dir: if fribidi_dir:
if isinstance(fribidi_dir, str): if isinstance(fribidi_dir, str):
@ -777,19 +804,19 @@ class pil_build_ext(build_ext):
self.compiler.include_dirs, fribidi_dir, 0 self.compiler.include_dirs, fribidi_dir, 0
) )
if _find_library_file(self, "fribidi"): if _find_library_file(self, "fribidi"):
feature.fribidi = "fribidi" feature.set("fribidi", "fribidi")
feature.raqm = True feature.set("raqm", True)
else: # want to build FriBiDi shim from src/thirdparty else: # want to build FriBiDi shim from src/thirdparty
feature.raqm = True feature.set("raqm", True)
if feature.want("lcms"): if feature.want("lcms"):
_dbg("Looking for lcms") _dbg("Looking for lcms")
if _find_include_file(self, "lcms2.h"): if _find_include_file(self, "lcms2.h"):
if _find_library_file(self, "lcms2"): if _find_library_file(self, "lcms2"):
feature.lcms = "lcms2" feature.set("lcms", "lcms2")
elif _find_library_file(self, "lcms2_static"): elif _find_library_file(self, "lcms2_static"):
# alternate Windows name. # alternate Windows name.
feature.lcms = "lcms2_static" feature.set("lcms", "lcms2_static")
if feature.want("webp"): if feature.want("webp"):
_dbg("Looking for webp") _dbg("Looking for webp")
@ -803,17 +830,17 @@ class pil_build_ext(build_ext):
_find_library_file(self, prefix + library) _find_library_file(self, prefix + library)
for library in ("webp", "webpmux", "webpdemux") for library in ("webp", "webpmux", "webpdemux")
): ):
feature.webp = prefix + "webp" feature.set("webp", prefix + "webp")
break break
if feature.want("xcb"): if feature.want("xcb"):
_dbg("Looking for xcb") _dbg("Looking for xcb")
if _find_include_file(self, "xcb/xcb.h"): if _find_include_file(self, "xcb/xcb.h"):
if _find_library_file(self, "xcb"): if _find_library_file(self, "xcb"):
feature.xcb = "xcb" feature.set("xcb", "xcb")
for f in feature: for f in feature:
if not getattr(feature, f) and feature.require(f): if not feature.get(f) and feature.require(f):
if f in ("jpeg", "zlib"): if f in ("jpeg", "zlib"):
raise RequiredDependencyException(f) raise RequiredDependencyException(f)
raise DependencyException(f) raise DependencyException(f)
@ -821,10 +848,11 @@ class pil_build_ext(build_ext):
# #
# core library # core library
libs = self.add_imaging_libs.split() libs: list[str | bool | None] = []
defs = [] libs.extend(self.add_imaging_libs.split())
if feature.tiff: defs: list[tuple[str, str | None]] = []
libs.append(feature.tiff) if feature.get("tiff"):
libs.append(feature.get("tiff"))
defs.append(("HAVE_LIBTIFF", None)) defs.append(("HAVE_LIBTIFF", None))
if sys.platform == "win32": if sys.platform == "win32":
# This define needs to be defined if-and-only-if it was defined # This define needs to be defined if-and-only-if it was defined
@ -832,22 +860,22 @@ class pil_build_ext(build_ext):
# so we have to guess; by default it is defined in all Windows builds. # so we have to guess; by default it is defined in all Windows builds.
# See #4237, #5243, #5359 for more information. # See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None)) defs.append(("USE_WIN32_FILEIO", None))
if feature.jpeg: if feature.get("jpeg"):
libs.append(feature.jpeg) libs.append(feature.get("jpeg"))
defs.append(("HAVE_LIBJPEG", None)) defs.append(("HAVE_LIBJPEG", None))
if feature.jpeg2000: if feature.get("jpeg2000"):
libs.append(feature.jpeg2000) libs.append(feature.get("jpeg2000"))
defs.append(("HAVE_OPENJPEG", None)) defs.append(("HAVE_OPENJPEG", None))
if sys.platform == "win32" and not PLATFORM_MINGW: if sys.platform == "win32" and not PLATFORM_MINGW:
defs.append(("OPJ_STATIC", None)) defs.append(("OPJ_STATIC", None))
if feature.zlib: if feature.get("zlib"):
libs.append(feature.zlib) libs.append(feature.get("zlib"))
defs.append(("HAVE_LIBZ", None)) defs.append(("HAVE_LIBZ", None))
if feature.imagequant: if feature.get("imagequant"):
libs.append(feature.imagequant) libs.append(feature.get("imagequant"))
defs.append(("HAVE_LIBIMAGEQUANT", None)) defs.append(("HAVE_LIBIMAGEQUANT", None))
if feature.xcb: if feature.get("xcb"):
libs.append(feature.xcb) libs.append(feature.get("xcb"))
defs.append(("HAVE_XCB", None)) defs.append(("HAVE_XCB", None))
if sys.platform == "win32": if sys.platform == "win32":
libs.extend(["kernel32", "user32", "gdi32"]) libs.extend(["kernel32", "user32", "gdi32"])
@ -861,22 +889,22 @@ class pil_build_ext(build_ext):
# #
# additional libraries # additional libraries
if feature.freetype: if feature.get("freetype"):
srcs = [] srcs = []
libs = ["freetype"] libs = ["freetype"]
defs = [] defs = []
if feature.raqm: if feature.get("raqm"):
if not feature.want_vendor("raqm"): # using system Raqm if not feature.want_vendor("raqm"): # using system Raqm
defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM", None))
defs.append(("HAVE_RAQM_SYSTEM", None)) defs.append(("HAVE_RAQM_SYSTEM", None))
libs.append(feature.raqm) libs.append(feature.get("raqm"))
else: # building Raqm from src/thirdparty else: # building Raqm from src/thirdparty
defs.append(("HAVE_RAQM", None)) defs.append(("HAVE_RAQM", None))
srcs.append("src/thirdparty/raqm/raqm.c") srcs.append("src/thirdparty/raqm/raqm.c")
libs.append(feature.harfbuzz) libs.append(feature.get("harfbuzz"))
if not feature.want_vendor("fribidi"): # using system FriBiDi if not feature.want_vendor("fribidi"): # using system FriBiDi
defs.append(("HAVE_FRIBIDI_SYSTEM", None)) defs.append(("HAVE_FRIBIDI_SYSTEM", None))
libs.append(feature.fribidi) libs.append(feature.get("fribidi"))
else: # building FriBiDi shim from src/thirdparty else: # building FriBiDi shim from src/thirdparty
srcs.append("src/thirdparty/fribidi-shim/fribidi.c") srcs.append("src/thirdparty/fribidi-shim/fribidi.c")
self._update_extension("PIL._imagingft", libs, defs, srcs) self._update_extension("PIL._imagingft", libs, defs, srcs)
@ -884,16 +912,17 @@ class pil_build_ext(build_ext):
else: else:
self._remove_extension("PIL._imagingft") self._remove_extension("PIL._imagingft")
if feature.lcms: if feature.get("lcms"):
extra = [] libs = [feature.get("lcms")]
if sys.platform == "win32": if sys.platform == "win32":
extra.extend(["user32", "gdi32"]) libs.extend(["user32", "gdi32"])
self._update_extension("PIL._imagingcms", [feature.lcms] + extra) self._update_extension("PIL._imagingcms", libs)
else: else:
self._remove_extension("PIL._imagingcms") self._remove_extension("PIL._imagingcms")
if feature.webp: webp = feature.get("webp")
libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] if isinstance(webp, str):
libs = [webp, webp + "mux", webp + "demux"]
self._update_extension("PIL._webp", libs) self._update_extension("PIL._webp", libs)
else: else:
self._remove_extension("PIL._webp") self._remove_extension("PIL._webp")
@ -908,14 +937,14 @@ class pil_build_ext(build_ext):
self.summary_report(feature) self.summary_report(feature)
def summary_report(self, feature): def summary_report(self, feature: ext_feature) -> None:
print("-" * 68) print("-" * 68)
print("PIL SETUP SUMMARY") print("PIL SETUP SUMMARY")
print("-" * 68) print("-" * 68)
print(f"version Pillow {PILLOW_VERSION}") print(f"version Pillow {PILLOW_VERSION}")
v = sys.version.split("[") version = sys.version.split("[")
print(f"platform {sys.platform} {v[0].strip()}") print(f"platform {sys.platform} {version[0].strip()}")
for v in v[1:]: for v in version[1:]:
print(f" [{v.strip()}") print(f" [{v.strip()}")
print("-" * 68) print("-" * 68)
@ -926,16 +955,20 @@ class pil_build_ext(build_ext):
raqm_extra_info += ", FriBiDi shim" raqm_extra_info += ", FriBiDi shim"
options = [ options = [
(feature.jpeg, "JPEG"), (feature.get("jpeg"), "JPEG"),
(feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), (
(feature.zlib, "ZLIB (PNG/ZIP)"), feature.get("jpeg2000"),
(feature.imagequant, "LIBIMAGEQUANT"), "OPENJPEG (JPEG2000)",
(feature.tiff, "LIBTIFF"), feature.get("openjpeg_version"),
(feature.freetype, "FREETYPE2"), ),
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.get("zlib"), "ZLIB (PNG/ZIP)"),
(feature.lcms, "LITTLECMS2"), (feature.get("imagequant"), "LIBIMAGEQUANT"),
(feature.webp, "WEBP"), (feature.get("tiff"), "LIBTIFF"),
(feature.xcb, "XCB (X protocol)"), (feature.get("freetype"), "FREETYPE2"),
(feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info),
(feature.get("lcms"), "LITTLECMS2"),
(feature.get("webp"), "WEBP"),
(feature.get("xcb"), "XCB (X protocol)"),
] ]
all = 1 all = 1
@ -964,7 +997,7 @@ class pil_build_ext(build_ext):
print("") print("")
def debug_build(): def debug_build() -> bool:
return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD

View File

@ -279,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_blp_header()
self._load() self._load()

View File

@ -321,7 +321,7 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder): class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
rle4 = self.args[1] rle4 = self.args[1]
data = bytearray() data = bytearray()

View File

@ -481,7 +481,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder): class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
bitcount, masks = self.args bitcount, masks = self.args

View File

@ -126,7 +126,7 @@ class FitsImageFile(ImageFile.ImageFile):
class FitsGzipDecoder(ImageFile.PyDecoder): class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
value = gzip.decompress(self.fd.read()) value = gzip.decompress(self.fd.read())

View File

@ -29,7 +29,6 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
@ -49,6 +48,7 @@ from ._binary import o16le as o16
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
from ._typing import Buffer
class LoadingStrategy(IntEnum): class LoadingStrategy(IntEnum):
@ -1157,19 +1157,10 @@ def getdata(
class Collector(BytesIO): class Collector(BytesIO):
data = [] data = []
if sys.version_info >= (3, 12):
from collections.abc import Buffer
def write(self, data: Buffer) -> int: def write(self, data: Buffer) -> int:
self.data.append(data) self.data.append(data)
return len(data) return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available im.load() # make sure raster data is available
fp = Collector() fp = Collector()

View File

@ -851,7 +851,10 @@ class Image:
) )
def frombytes( def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any self,
data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> None: ) -> None:
""" """
Loads this image with pixel data from a bytes object. Loads this image with pixel data from a bytes object.
@ -3140,7 +3143,7 @@ def new(
def frombytes( def frombytes(
mode: str, mode: str,
size: tuple[int, int], size: tuple[int, int],
data: bytes | bytearray, data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw", decoder_name: str = "raw",
*args: Any, *args: Any,
) -> Image: ) -> Image:
@ -3184,7 +3187,11 @@ def frombytes(
def frombuffer( def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any mode: str,
size: tuple[int, int],
data: bytes | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> Image: ) -> Image:
""" """
Creates an image memory referencing pixel data in a byte buffer. Creates an image memory referencing pixel data in a byte buffer.
@ -3963,7 +3970,7 @@ class Exif(_ExifBase):
self._data.clear() self._data.clear()
self._hidden_data.clear() self._hidden_data.clear()
self._ifds.clear() self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"): while data and data.startswith(b"Exif\x00\x00"):
data = data[6:] data = data[6:]
if not data: if not data:
self._info = None self._info = None

View File

@ -733,7 +733,7 @@ class PyDecoder(PyCodec):
def pulls_fd(self) -> bool: def pulls_fd(self) -> bool:
return self._pulls_fd return self._pulls_fd
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
""" """
Override to perform the decoding process. Override to perform the decoding process.

View File

@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
img = io.BytesIO() img = io.BytesIO()

View File

@ -284,7 +284,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break break
return data return data
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
self._comment_spans = False self._comment_spans = False
if self.mode == "1": if self.mode == "1":
data = self._decode_bitonal() data = self._decode_bitonal()
@ -300,7 +300,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
data = bytearray() data = bytearray()

View File

@ -47,7 +47,7 @@ class QoiDecoder(ImageFile.PyDecoder):
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value self._previously_seen_pixels[hash_value] = value
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}

View File

@ -214,7 +214,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
class SGI16Decoder(ImageFile.PyDecoder): class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
assert self.im is not None assert self.im is not None

View File

@ -62,14 +62,13 @@ from ._util import is_path
from .TiffTags import TYPES from .TiffTags import TYPES
if TYPE_CHECKING: if TYPE_CHECKING:
from ._typing import IntegralLike from ._typing import Buffer, IntegralLike
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Set these to true to force use of libtiff for reading or writing. # Set these to true to force use of libtiff for reading or writing.
READ_LIBTIFF = False READ_LIBTIFF = False
WRITE_LIBTIFF = False WRITE_LIBTIFF = False
IFD_LEGACY_API = True
STRIP_SIZE = 65536 STRIP_SIZE = 65536
II = b"II" # little-endian (Intel style) II = b"II" # little-endian (Intel style)
@ -1125,7 +1124,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
return val return val
# undone -- switch this pointer when IFD_LEGACY_API == False # undone -- switch this pointer
ImageFileDirectory = ImageFileDirectory_v1 ImageFileDirectory = ImageFileDirectory_v1
@ -1647,7 +1646,7 @@ SAVE_INFO = {
} }
def _save(im: Image.Image, fp, filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e: except KeyError as e:
@ -1958,7 +1957,7 @@ def _save(im: Image.Image, fp, filename: str | bytes) -> None:
setattr(im, "_debug_multipage", ifd) setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter: class AppendingTiffWriter(io.BytesIO):
fieldSizes = [ fieldSizes = [
0, # None 0, # None
1, # byte 1, # byte
@ -2068,6 +2067,12 @@ class AppendingTiffWriter:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
"""
:param offset: Distance to seek.
:param whence: Whether the distance is relative to the start,
end or current position.
:returns: The resulting position, relative to the start.
"""
if whence == os.SEEK_SET: if whence == os.SEEK_SET:
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
@ -2101,7 +2106,7 @@ class AppendingTiffWriter:
num_tags = self.readShort() num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data: bytes) -> int | None: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
def readShort(self) -> int: def readShort(self) -> int:
@ -2143,6 +2148,7 @@ class AppendingTiffWriter:
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
if self.close_fp:
self.f.close() self.f.close()
def fixIFD(self) -> None: def fixIFD(self) -> None:

View File

@ -20,6 +20,11 @@ if sys.version_info >= (3, 13):
else: else:
CapsuleType = object CapsuleType = object
if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
Buffer = Any
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from typing import TypeGuard from typing import TypeGuard
else: else:
@ -45,4 +50,4 @@ class SupportsRead(Protocol[_T_co]):
StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]

View File

@ -185,7 +185,7 @@ put_pixel_32(Imaging im, int x, int y, const void *color) {
} }
void void
ImagingAccessInit() { ImagingAccessInit(void) {
#define ADD(mode_, get_pixel_, put_pixel_) \ #define ADD(mode_, get_pixel_, put_pixel_) \
{ \ { \
ImagingAccess access = add_item(mode_); \ ImagingAccess access = add_item(mode_); \

View File

@ -48,7 +48,7 @@ char *libjpeg_turbo_version = NULL;
#endif #endif
int int
ImagingJpegUseJCSExtensions() { ImagingJpegUseJCSExtensions(void) {
int use_jcs_extensions = 0; int use_jcs_extensions = 0;
#ifdef JCS_EXTENSIONS #ifdef JCS_EXTENSIONS
#if defined(LIBJPEG_TURBO_VERSION_NUMBER) #if defined(LIBJPEG_TURBO_VERSION_NUMBER)

View File

@ -36,4 +36,4 @@ deps =
extras = extras =
typing typing
commands = commands =
mypy docs src winbuild Tests {posargs} mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs}