Merge branch 'main' into ios-build

This commit is contained in:
Andrew Murray 2025-06-25 11:00:08 +10:00 committed by GitHub
commit ec5e5d0791
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 60 additions and 25 deletions

View File

@ -60,7 +60,7 @@ jobs:
platform: macos platform: macos
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{12,13}*" build: "cp3{12,13,14}*"
macosx_deployment_target: "10.13" macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64" - name: "macOS 10.15 x86_64"
platform: macos platform: macos

View File

@ -100,11 +100,11 @@ class TestFilePng:
assert im.format == "PNG" assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png" assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im = hopper(mode) im = hopper(mode)
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
if mode in ("I", "I;16B"): if mode == "I;16B":
reloaded = reloaded.convert(mode) reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
@ -801,6 +801,16 @@ class TestFilePng:
with Image.open("Tests/images/truncated_end_chunk.png") as im: with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, "Tests/images/hopper.png")
def test_deprecation(self, tmp_path: Path) -> None:
test_file = tmp_path / "out.png"
im = hopper("I")
with pytest.warns(DeprecationWarning):
im.save(test_file)
with Image.open(test_file) as reloaded:
assert_image_equal(im, reloaded.convert("I"))
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")

View File

@ -193,6 +193,20 @@ Image.Image.get_child_images()
method uses an image's file pointer, and so child images could only be retrieved from method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance. an :py:class:`PIL.ImageFile.ImageFile` instance.
Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
Removed features Removed features
---------------- ----------------

View File

@ -23,10 +23,17 @@ TODO
Deprecations Deprecations
============ ============
TODO Saving I mode images as PNG
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
API changes API changes
=========== ===========

View File

@ -16,7 +16,6 @@ import subprocess
import sys import sys
import warnings import warnings
from collections.abc import Iterator 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
@ -148,7 +147,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: str, tp: Any = None) -> None: def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None:
if DEBUG: if DEBUG:
if tp: if tp:
print(s % tp) print(s % tp)
@ -522,11 +521,11 @@ class pil_build_ext(build_ext):
if root is None and pkg_config: if root is None and pkg_config:
if isinstance(lib_name, str): if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.") _dbg("Looking for `%s` using pkg-config.", lib_name)
root = pkg_config(lib_name) root = pkg_config(lib_name)
else: else:
for lib_name2 in lib_name: for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.") _dbg("Looking for `%s` using pkg-config.", lib_name2)
root = pkg_config(lib_name2) root = pkg_config(lib_name2)
if root: if root:
break break
@ -757,7 +756,7 @@ class pil_build_ext(build_ext):
best_path = os.path.join(directory, name) best_path = os.path.join(directory, name)
_dbg( _dbg(
"Best openjpeg version %s so far in %s", "Best openjpeg version %s so far in %s",
(best_version, best_path), (str(best_version), best_path),
) )
if best_version and _find_library_file(self, "openjp2"): if best_version and _find_library_file(self, "openjp2"):

View File

@ -248,6 +248,9 @@ class ImageCmsProfile:
low-level profile object low-level profile object
""" """
self.filename = None
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
if isinstance(profile, str): if isinstance(profile, str):
if sys.platform == "win32": if sys.platform == "win32":
@ -256,23 +259,18 @@ class ImageCmsProfile:
profile_bytes_path.decode("ascii") profile_bytes_path.decode("ascii")
except UnicodeDecodeError: except UnicodeDecodeError:
with open(profile, "rb") as f: with open(profile, "rb") as f:
self._set(core.profile_frombytes(f.read())) self.profile = core.profile_frombytes(f.read())
return return
self._set(core.profile_open(profile), profile) self.filename = profile
self.profile = core.profile_open(profile)
elif hasattr(profile, "read"): elif hasattr(profile, "read"):
self._set(core.profile_frombytes(profile.read())) self.profile = core.profile_frombytes(profile.read())
elif isinstance(profile, core.CmsProfile): elif isinstance(profile, core.CmsProfile):
self._set(profile) self.profile = profile
else: else:
msg = "Invalid type for Profile" # type: ignore[unreachable] msg = "Invalid type for Profile" # type: ignore[unreachable]
raise TypeError(msg) raise TypeError(msg)
def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None:
self.profile = profile
self.filename = filename
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
def tobytes(self) -> bytes: def tobytes(self) -> bytes:
""" """
Returns the profile in a format suitable for embedding in Returns the profile in a format suitable for embedding in

View File

@ -48,6 +48,7 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
from ._deprecate import deprecate
from ._util import DeferredError from ._util import DeferredError
TYPE_CHECKING = False TYPE_CHECKING = False
@ -1368,6 +1369,8 @@ def _save(
except KeyError as e: except KeyError as e:
msg = f"cannot write mode {mode} as PNG" msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e raise OSError(msg) from e
if outmode == "I":
deprecate("Saving I mode images as PNG", 13, stacklevel=4)
# #
# write minimal PNG file # write minimal PNG file

View File

@ -12,6 +12,7 @@ def deprecate(
*, *,
action: str | None = None, action: str | None = None,
plural: bool = False, plural: bool = False,
stacklevel: int = 3,
) -> None: ) -> None:
""" """
Deprecations helper. Deprecations helper.
@ -67,5 +68,5 @@ def deprecate(
warnings.warn( warnings.warn(
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=stacklevel,
) )

View File

@ -1032,7 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Encode Error, row %d\n", state->y)); TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN; state->errcode = IMAGING_CODEC_BROKEN;
if (!clientstate->fp) { if (clientstate->fp) {
TIFFCleanup(tiff);
clientstate->tiff = NULL;
} else {
free(clientstate->data); free(clientstate->data);
} }
return -1; return -1;

View File

@ -385,8 +385,8 @@ DEPS: dict[str, dict[str, Any]] = {
"bins": [r"*.dll"], "bins": [r"*.dll"],
}, },
"libavif": { "libavif": {
"url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz",
"filename": f"libavif-{V['LIBAVIF']}.zip", "filename": f"libavif-{V['LIBAVIF']}.tar.gz",
"license": "LICENSE", "license": "LICENSE",
"build": [ "build": [
"rustup update", "rustup update",