Merge branch 'main' into types-cms2

This commit is contained in:
Andrew Murray 2024-03-31 07:22:40 +11:00 committed by GitHub
commit 46b0b0e57d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 231 additions and 97 deletions

View File

@ -48,6 +48,21 @@ Thank you.
* Python: * Python:
* Pillow: * Pillow:
```text
Please paste here the output of running:
python3 -m PIL.report
or
python3 -m PIL --report
Or the output of the following Python code:
from PIL import report
# or
from PIL import features
features.pilinfo(supported_formats=False)
```
<!-- <!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

View File

@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.1 HARFBUZZ_VERSION=8.4.0
LIBPNG_VERSION=1.6.43 LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.2 JPEGTURBO_VERSION=3.0.2
OPENJPEG_VERSION=2.5.2 OPENJPEG_VERSION=2.5.2

View File

@ -5,6 +5,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake" - "winbuild/fribidi.cmake"
@ -14,6 +15,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake" - "winbuild/fribidi.cmake"

View File

@ -5,6 +5,15 @@ Changelog (Pillow)
10.3.0 (unreleased) 10.3.0 (unreleased)
------------------- -------------------
- Determine MPO size from markers, not EXIF data #7884
[radarhere]
- Improved conversion from RGB to RGBa, LA and La #7888
[radarhere]
- Support FITS images with GZIP_1 compression #7894
[radarhere]
- Use I;16 mode for 9-bit JPEG 2000 images #7900 - Use I;16 mode for 9-bit JPEG 2000 images #7900
[scaramallion, radarhere] [scaramallion, radarhere]

View File

@ -263,8 +263,6 @@ def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image
if im is None: if im is None:
if mode == "F": if mode == "F":
im = hopper("L").convert(mode) im = hopper("L").convert(mode)
elif mode[:4] == "I;16":
im = hopper("I").convert(mode)
else: else:
im = hopper().convert(mode) im = hopper().convert(mode)
cache[mode] = im cache[mode] = im

View File

@ -117,9 +117,10 @@ def test_unsupported_module() -> None:
features.version_module(module) features.version_module(module)
def test_pilinfo() -> None: @pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats) -> None:
buf = io.StringIO() buf = io.StringIO()
features.pilinfo(buf) features.pilinfo(buf, supported_formats=supported_formats)
out = buf.getvalue() out = buf.getvalue()
lines = out.splitlines() lines = out.splitlines()
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
@ -129,9 +130,15 @@ def test_pilinfo() -> None:
while lines[0].startswith(" "): while lines[0].startswith(" "):
lines = lines[1:] lines = lines[1:]
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Python modules loaded from ") assert lines[1].startswith("Python executable is")
assert lines[2].startswith("Binary modules loaded from ") lines = lines[2:]
assert lines[3] == "-" * 68 if lines[0].startswith("Environment Python files loaded from"):
lines = lines[1:]
assert lines[0].startswith("System Python files loaded from")
assert lines[1] == "-" * 68
assert lines[2].startswith("Python Pillow modules loaded from ")
assert lines[3].startswith("Binary Pillow modules loaded from ")
assert lines[4] == "-" * 68
jpeg = ( jpeg = (
"\n" "\n"
+ "-" * 68 + "-" * 68
@ -142,4 +149,4 @@ def test_pilinfo() -> None:
+ "-" * 68 + "-" * 68
+ "\n" + "\n"
) )
assert jpeg in out assert supported_formats == (jpeg in out)

View File

@ -825,7 +825,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "When the image resolution is unknown, 72 [dpi] is designated."
# https://exiv2.org/tags.html # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None: def test_invalid_exif(self) -> None:

View File

@ -33,36 +33,38 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
# name, pixel size
image_modes = (
("1", 1),
("L", 1),
("LA", 4),
("La", 4),
("P", 1),
("PA", 4),
("F", 4),
("I", 4),
("I;16", 2),
("I;16L", 2),
("I;16B", 2),
("I;16N", 2),
("RGB", 4),
("RGBA", 4),
("RGBa", 4),
("RGBX", 4),
("BGR;15", 2),
("BGR;16", 2),
("BGR;24", 3),
("CMYK", 4),
("YCbCr", 4),
("HSV", 4),
("LAB", 4),
)
image_mode_names = [name for name, _ in image_modes]
class TestImage: class TestImage:
@pytest.mark.parametrize( @pytest.mark.parametrize("mode", image_mode_names)
"mode",
(
"1",
"P",
"PA",
"L",
"LA",
"La",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBX",
"RGBA",
"RGBa",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"LAB",
"HSV",
),
)
def test_image_modes_success(self, mode: str) -> None: def test_image_modes_success(self, mode: str) -> None:
Image.new(mode, (1, 1)) Image.new(mode, (1, 1))
@ -1042,6 +1044,35 @@ class TestImage:
assert im.fp is None assert im.fp is None
class TestImageBytes:
@pytest.mark.parametrize("mode", image_mode_names)
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", image_mode_names)
def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
reloaded = Image.new(mode, im.size)
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
im = Image.new(mode, (2, 2))
source_bytes = bytes(range(im.width * im.height * pixelsize))
im.frombytes(source_bytes)
reloaded = Image.new(mode, im.size)
reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded)
class MockEncoder(ImageFile.PyEncoder): class MockEncoder(ImageFile.PyEncoder):
pass pass

View File

@ -4,9 +4,16 @@ import os
import subprocess import subprocess
import sys import sys
import pytest
def test_main() -> None:
out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") @pytest.mark.parametrize(
"args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
)
def test_main(args, report) -> None:
args = [sys.executable, "-m"] + args
out = subprocess.check_output(args).decode("utf-8")
lines = out.splitlines() lines = out.splitlines()
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Pillow ") assert lines[1].startswith("Pillow ")
@ -15,9 +22,15 @@ def test_main() -> None:
while lines[0].startswith(" "): while lines[0].startswith(" "):
lines = lines[1:] lines = lines[1:]
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Python modules loaded from ") assert lines[1].startswith("Python executable is")
assert lines[2].startswith("Binary modules loaded from ") lines = lines[2:]
assert lines[3] == "-" * 68 if lines[0].startswith("Environment Python files loaded from"):
lines = lines[1:]
assert lines[0].startswith("System Python files loaded from")
assert lines[1] == "-" * 68
assert lines[2].startswith("Python Pillow modules loaded from ")
assert lines[3].startswith("Binary Pillow modules loaded from ")
assert lines[4] == "-" * 68
jpeg = ( jpeg = (
os.linesep os.linesep
+ "-" * 68 + "-" * 68
@ -31,4 +44,4 @@ def test_main() -> None:
+ "-" * 68 + "-" * 68
+ os.linesep + os.linesep
) )
assert jpeg in out assert report == (jpeg not in out)

View File

@ -11,41 +11,12 @@ backend_class = build_wheel.__self__.__class__
class _CustomBuildMetaBackend(backend_class): class _CustomBuildMetaBackend(backend_class):
def run_setup(self, setup_script="setup.py"): def run_setup(self, setup_script="setup.py"):
if self.config_settings: if self.config_settings:
for key, values in self.config_settings.items():
if not isinstance(values, list):
values = [values]
for value in values:
sys.argv.append(f"--pillow-configuration={key}={value}")
def config_has(key, value):
settings = self.config_settings.get(key)
if settings:
if not isinstance(settings, list):
settings = [settings]
return value in settings
flags = []
for dependency in (
"zlib",
"jpeg",
"tiff",
"freetype",
"raqm",
"lcms",
"webp",
"webpmux",
"jpeg2000",
"imagequant",
"xcb",
):
if config_has(dependency, "enable"):
flags.append("--enable-" + dependency)
elif config_has(dependency, "disable"):
flags.append("--disable-" + dependency)
for dependency in ("raqm", "fribidi"):
if config_has(dependency, "vendor"):
flags.append("--vendor-" + dependency)
if self.config_settings.get("platform-guessing") == "disable":
flags.append("--disable-platform-guessing")
if self.config_settings.get("debug") == "true":
flags.append("--debug")
if flags:
sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:]
return super().run_setup(setup_script) return super().run_setup(setup_script)
def build_wheel( def build_wheel(
@ -54,5 +25,15 @@ class _CustomBuildMetaBackend(backend_class):
self.config_settings = config_settings self.config_settings = config_settings
return super().build_wheel(wheel_directory, config_settings, metadata_directory) return super().build_wheel(wheel_directory, config_settings, metadata_directory)
def build_editable(
self, wheel_directory, config_settings=None, metadata_directory=None
):
self.config_settings = config_settings
return super().build_editable(
wheel_directory, config_settings, metadata_directory
)
build_wheel = _CustomBuildMetaBackend().build_wheel
_backend = _CustomBuildMetaBackend()
build_wheel = _backend.build_wheel
build_editable = _backend.build_editable

View File

@ -266,9 +266,10 @@ After navigating to the Pillow directory, run::
Build Options Build Options
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use * Config setting: ``-C parallel=n``. Can also be given
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
sets the number of CPUs to use, or can disable parallel building by multiprocessing to build the extension. Setting ``-C parallel=n``
sets the number of CPUs to use to ``n``, or can disable parallel building by
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present. available, as many as are present.
@ -293,14 +294,13 @@ Build Options
used to compile the standard Pillow wheels. Compiling libraqm requires used to compile the standard Pillow wheels. Compiling libraqm requires
a C99-compliant compiler. a C99-compliant compiler.
* Build flag: ``-C platform-guessing=disable``. Skips all of the * Config setting: ``-C platform-guessing=disable``. Skips all of the
platform dependent guessing of include and library directories for platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the automated build systems that configure the proper paths in the
environment variables (e.g. Buildroot). environment variables (e.g. Buildroot).
* Build flag: ``-C debug=true``. Adds a debugging flag to the include and * Config setting: ``-C debug=true``. Adds a debugging flag to the include and
library search process to dump all paths searched for and found to library search process to dump all paths searched for and found to stdout.
stdout.
Sample usage:: Sample usage::

View File

@ -27,6 +27,9 @@ def get_version():
return locals()["__version__"] return locals()["__version__"]
configuration = {}
PILLOW_VERSION = get_version() PILLOW_VERSION = get_version()
FREETYPE_ROOT = None FREETYPE_ROOT = None
HARFBUZZ_ROOT = None HARFBUZZ_ROOT = None
@ -333,15 +336,24 @@ class pil_build_ext(build_ext):
+ [("add-imaging-libs=", None, "Add libs to _imaging build")] + [("add-imaging-libs=", None, "Add libs to _imaging build")]
) )
@staticmethod
def check_configuration(option, value):
return True if value in configuration.get(option, []) else None
def initialize_options(self): def initialize_options(self):
self.disable_platform_guessing = None self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable"
)
self.add_imaging_libs = "" self.add_imaging_libs = ""
build_ext.initialize_options(self) build_ext.initialize_options(self)
for x in self.feature: for x in self.feature:
setattr(self, f"disable_{x}", None) setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
setattr(self, f"enable_{x}", None) setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
for x in ("raqm", "fribidi"): for x in ("raqm", "fribidi"):
setattr(self, f"vendor_{x}", None) setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor"))
if self.check_configuration("debug", "true"):
self.debug = True
self.parallel = configuration.get("parallel", [None])[-1]
def finalize_options(self): def finalize_options(self):
build_ext.finalize_options(self) build_ext.finalize_options(self)
@ -987,6 +999,12 @@ ext_modules = [
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
] ]
# parse configuration from _custom_build/backend.py
while sys.argv[-1].startswith("--pillow-configuration="):
_, key, value = sys.argv.pop().split("=", 2)
configuration.setdefault(key, []).append(value)
try: try:
setup( setup(
cmdclass={"build_ext": pil_build_ext}, cmdclass={"build_ext": pil_build_ext},

View File

@ -37,7 +37,7 @@ You can get the subsampling of a JPEG with the
:func:`.JpegImagePlugin.get_sampling` function. :func:`.JpegImagePlugin.get_sampling` function.
In JPEG compressed data a JPEG marker is used instead of an EXIF tag. In JPEG compressed data a JPEG marker is used instead of an EXIF tag.
(ref.: https://exiv2.org/tags.html) (ref.: https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html)
Quantization tables Quantization tables

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys
from .features import pilinfo from .features import pilinfo
pilinfo() pilinfo(supported_formats="--report" not in sys.argv)

View File

@ -230,6 +230,9 @@ def pilinfo(out=None, supported_formats=True):
""" """
Prints information about this installation of Pillow. Prints information about this installation of Pillow.
This function can be called with ``python3 -m PIL``. This function can be called with ``python3 -m PIL``.
It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report``
to have "supported_formats" set to ``False``, omitting the list of all supported
image file formats.
:param out: :param out:
The output stream to print to. Defaults to ``sys.stdout`` if ``None``. The output stream to print to. Defaults to ``sys.stdout`` if ``None``.
@ -249,12 +252,17 @@ def pilinfo(out=None, supported_formats=True):
for py_version in py_version[1:]: for py_version in py_version[1:]:
print(f" {py_version.strip()}", file=out) print(f" {py_version.strip()}", file=out)
print("-" * 68, file=out) print("-" * 68, file=out)
print(f"Python executable is {sys.executable or 'unknown'}", file=out)
if sys.prefix != sys.base_prefix:
print(f"Environment Python files loaded from {sys.prefix}", file=out)
print(f"System Python files loaded from {sys.base_prefix}", file=out)
print("-" * 68, file=out)
print( print(
f"Python modules loaded from {os.path.dirname(Image.__file__)}", f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}",
file=out, file=out,
) )
print( print(
f"Binary modules loaded from {os.path.dirname(Image.core.__file__)}", f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}",
file=out, file=out,
) )
print("-" * 68, file=out) print("-" * 68, file=out)

5
src/PIL/report.py Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from .features import pilinfo
pilinfo(supported_formats=False)

View File

@ -1578,7 +1578,17 @@ if (PySequence_Check(op)) { \
int bigendian = 0; int bigendian = 0;
if (image->type == IMAGING_TYPE_SPECIAL) { if (image->type == IMAGING_TYPE_SPECIAL) {
// I;16* // I;16*
bigendian = strcmp(image->mode, "I;16B") == 0; if (strcmp(image->mode, "I;16N") == 0) {
#ifdef WORDS_BIGENDIAN
bigendian = 1;
#else
bigendian = 0;
#endif
} else if (strcmp(image->mode, "I;16B") == 0) {
bigendian = 1;
} else {
bigendian = 0;
}
} }
for (i = x = y = 0; i < n; i++) { for (i = x = y = 0; i < n; i++) {
set_value_to_item(seq, i); set_value_to_item(seq, i);

View File

@ -250,6 +250,26 @@ rgb2i(UINT8 *out_, const UINT8 *in, int xsize) {
} }
} }
static void
rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) {
int x;
for (x = 0; x < xsize; x++, in += 4) {
UINT8 v = CLIP16(L24(in) >> 16);
*out_++ = v;
*out_++ = v >> 8;
}
}
static void
rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) {
int x;
for (x = 0; x < xsize; x++, in += 4) {
UINT8 v = CLIP16(L24(in) >> 16);
*out_++ = v >> 8;
*out_++ = v;
}
}
static void static void
rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { rgb2f(UINT8 *out_, const UINT8 *in, int xsize) {
int x; int x;
@ -944,6 +964,14 @@ static struct {
{"RGB", "LA", rgb2la}, {"RGB", "LA", rgb2la},
{"RGB", "La", rgb2la}, {"RGB", "La", rgb2la},
{"RGB", "I", rgb2i}, {"RGB", "I", rgb2i},
{"RGB", "I;16", rgb2i16l},
{"RGB", "I;16L", rgb2i16l},
{"RGB", "I;16B", rgb2i16b},
#ifdef WORDS_BIGENDIAN
{"RGB", "I;16N", rgb2i16b},
#else
{"RGB", "I;16N", rgb2i16l},
#endif
{"RGB", "F", rgb2f}, {"RGB", "F", rgb2f},
{"RGB", "BGR;15", rgb2bgr15}, {"RGB", "BGR;15", rgb2bgr15},
{"RGB", "BGR;16", rgb2bgr16}, {"RGB", "BGR;16", rgb2bgr16},

View File

@ -87,11 +87,18 @@ are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:
winbuild\build\build_env.cmd winbuild\build\build_env.cmd
python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor .
To build a wheel instead, run:: You can also install Pillow in `editable mode`_::
winbuild\build\build_env.cmd
python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor -e .
To build a binary wheel instead, run::
winbuild\build\build_env.cmd winbuild\build\build_env.cmd
python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor .
.. _editable mode: https://setuptools.pypa.io/en/stable/userguide/development_mode.html
Testing Pillow Testing Pillow
-------------- --------------

View File

@ -113,7 +113,7 @@ V = {
"BROTLI": "1.1.0", "BROTLI": "1.1.0",
"FREETYPE": "2.13.2", "FREETYPE": "2.13.2",
"FRIBIDI": "1.0.13", "FRIBIDI": "1.0.13",
"HARFBUZZ": "8.3.1", "HARFBUZZ": "8.4.0",
"JPEGTURBO": "3.0.2", "JPEGTURBO": "3.0.2",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.43", "LIBPNG": "1.6.43",