Merge branch 'main' into hopper-lru-cache

This commit is contained in:
Andrew Murray 2024-03-30 20:20:51 +11:00 committed by GitHub
commit 328052730d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 734 additions and 140 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

@ -267,8 +267,6 @@ def _cached_hopper(mode: str | None = None) -> Image.Image:
return Image.open("Tests/images/hopper.ppm") return Image.open("Tests/images/hopper.ppm")
if mode == "F": if mode == "F":
im = _cached_hopper("L").convert(mode) im = _cached_hopper("L").convert(mode)
elif mode[:4] == "I;16":
im = _cached_hopper("I").convert(mode)
else: else:
im = _cached_hopper().convert(mode) im = _cached_hopper().convert(mode)
return im return im

BIN
Tests/images/m13.fits Normal file

Binary file not shown.

366
Tests/images/m13_gzip.fits Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

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

@ -6,7 +6,7 @@ import pytest
from PIL import FitsImagePlugin, Image from PIL import FitsImagePlugin, Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE = "Tests/images/hopper.fits" TEST_FILE = "Tests/images/hopper.fits"
@ -22,6 +22,11 @@ def test_open() -> None:
assert_image_equal(im, hopper("L")) assert_image_equal(im, hopper("L"))
def test_gzip1() -> None:
with Image.open("Tests/images/m13_gzip.fits") as im:
assert_image_equal_tofile(im, "Tests/images/m13.fits")
def test_invalid_file() -> None: def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"

View File

@ -93,7 +93,7 @@ def test_exif(test_file: str) -> None:
def test_frame_size() -> None: def test_frame_size() -> None:
# This image has been hexedited to contain a different size # This image has been hexedited to contain a different size
# in the EXIF data of the second frame # in the SOF marker of the second frame
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
assert im.size == (640, 480) assert im.size == (640, 480)

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

@ -183,6 +183,14 @@ def test_trns_RGB(tmp_path: Path) -> None:
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
im_l.save(f) im_l.save(f)
im_la = im.convert("LA")
assert "transparency" not in im_la.info
im_la.save(f)
im_la = im.convert("La")
assert "transparency" not in im_la.info
assert im_la.getpixel((0, 0)) == (0, 0)
im_p = im.convert("P") im_p = im.convert("P")
assert "transparency" in im_p.info assert "transparency" in im_p.info
im_p.save(f) im_p.save(f)
@ -191,6 +199,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
assert "transparency" not in im_rgba.info assert "transparency" not in im_rgba.info
im_rgba.save(f) im_rgba.save(f)
im_rgba = im.convert("RGBa")
assert "transparency" not in im_rgba.info
assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
assert "transparency" not in im_p.info assert "transparency" not in im_p.info
im_p.save(f) im_p.save(f)

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

@ -1339,7 +1339,8 @@ FITS
.. versionadded:: 9.1.0 .. versionadded:: 9.1.0
Pillow identifies and reads FITS files, commonly used for astronomy. Pillow identifies and reads FITS files, commonly used for astronomy. Uncompressed and
GZIP_1 compressed images can be read.
FLI, FLC FLI, FLC
^^^^^^^^ ^^^^^^^^

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

@ -10,6 +10,7 @@
# #
from __future__ import annotations from __future__ import annotations
import gzip
import math import math
from . import Image, ImageFile from . import Image, ImageFile
@ -27,14 +28,32 @@ class FitsImageFile(ImageFile.ImageFile):
assert self.fp is not None assert self.fp is not None
headers: dict[bytes, bytes] = {} headers: dict[bytes, bytes] = {}
header_in_progress = False
decoder_name = ""
while True: while True:
header = self.fp.read(80) header = self.fp.read(80)
if not header: if not header:
msg = "Truncated FITS file" msg = "Truncated FITS file"
raise OSError(msg) raise OSError(msg)
keyword = header[:8].strip() keyword = header[:8].strip()
if keyword == b"END": if keyword in (b"SIMPLE", b"XTENSION"):
header_in_progress = True
elif headers and not header_in_progress:
# This is now a data unit
break break
elif keyword == b"END":
# Seek to the end of the header unit
self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
if not decoder_name:
decoder_name, offset, args = self._parse_headers(headers)
header_in_progress = False
continue
if decoder_name:
# Keep going to read past the headers
continue
value = header[8:].split(b"/")[0].strip() value = header[8:].split(b"/")[0].strip()
if value.startswith(b"="): if value.startswith(b"="):
value = value[1:].strip() value = value[1:].strip()
@ -43,32 +62,87 @@ class FitsImageFile(ImageFile.ImageFile):
raise SyntaxError(msg) raise SyntaxError(msg)
headers[keyword] = value headers[keyword] = value
naxis = int(headers[b"NAXIS"]) if not decoder_name:
if naxis == 0:
msg = "No image data" msg = "No image data"
raise ValueError(msg) raise ValueError(msg)
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
offset += self.fp.tell() - 80
self.tile = [(decoder_name, (0, 0) + self.size, offset, args)]
def _get_size(
self, headers: dict[bytes, bytes], prefix: bytes
) -> tuple[int, int] | None:
naxis = int(headers[prefix + b"NAXIS"])
if naxis == 0:
return None
if naxis == 1:
return 1, int(headers[prefix + b"NAXIS1"])
else:
return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
def _parse_headers(
self, headers: dict[bytes, bytes]
) -> tuple[str, int, tuple[str | int, ...]]:
prefix = b""
decoder_name = "raw"
offset = 0
if (
headers.get(b"XTENSION") == b"'BINTABLE'"
and headers.get(b"ZIMAGE") == b"T"
and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
):
no_prefix_size = self._get_size(headers, prefix) or (0, 0)
number_of_bits = int(headers[b"BITPIX"]) number_of_bits = int(headers[b"BITPIX"])
offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
prefix = b"Z"
decoder_name = "fits_gzip"
size = self._get_size(headers, prefix)
if not size:
return "", 0, ()
self._size = size
number_of_bits = int(headers[prefix + b"BITPIX"])
if number_of_bits == 8: if number_of_bits == 8:
self._mode = "L" self._mode = "L"
elif number_of_bits == 16: elif number_of_bits == 16:
self._mode = "I" self._mode = "I;16"
elif number_of_bits == 32: elif number_of_bits == 32:
self._mode = "I" self._mode = "I"
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self._mode = "F" self._mode = "F"
offset = math.ceil(self.fp.tell() / 2880) * 2880 args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))] return decoder_name, offset, args
class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
assert self.fd is not None
value = gzip.decompress(self.fd.read())
rows = []
offset = 0
number_of_bits = min(self.args[0] // 8, 4)
for y in range(self.state.ysize):
row = bytearray()
for x in range(self.state.xsize):
row += value[offset + (4 - number_of_bits) : offset + 4]
offset += 4
rows.append(row)
self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
return -1, 0
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registry # Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept) Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_decoder("fits_gzip", FitsGzipDecoder)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

@ -978,7 +978,7 @@ class Image:
# transparency handling # transparency handling
if has_transparency: if has_transparency:
if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or (
self.mode == "RGB" and mode == "RGBA" self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA")
): ):
# Use transparent conversion to promote from transparent # Use transparent conversion to promote from transparent
# color to an alpha channel. # color to an alpha channel.

View File

@ -24,14 +24,11 @@ import os
import struct import struct
from . import ( from . import (
ExifTags,
Image, Image,
ImageFile,
ImageSequence, ImageSequence,
JpegImagePlugin, JpegImagePlugin,
TiffImagePlugin, TiffImagePlugin,
) )
from ._binary import i16be as i16
from ._binary import o32le from ._binary import o32le
@ -109,7 +106,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self._after_jpeg_open() self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None): def _after_jpeg_open(self, mpheader=None):
self._initial_size = self.size
self.mpinfo = mpheader if mpheader is not None else self._getmp() self.mpinfo = mpheader if mpheader is not None else self._getmp()
self.n_frames = self.mpinfo[0xB001] self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [ self.__mpoffsets = [
@ -137,27 +133,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.fp = self._fp self.fp = self._fp
self.offset = self.__mpoffsets[frame] self.offset = self.__mpoffsets[frame]
original_exif = self.info.get("exif")
if "exif" in self.info:
del self.info["exif"]
self.fp.seek(self.offset + 2) # skip SOI marker self.fp.seek(self.offset + 2) # skip SOI marker
segment = self.fp.read(2) if not self.fp.read(2):
if not segment:
msg = "No data found for frame" msg = "No data found for frame"
raise ValueError(msg) raise ValueError(msg)
self._size = self._initial_size self.fp.seek(self.offset)
if i16(segment) == 0xFFE1: # APP1 JpegImagePlugin.JpegImageFile._open(self)
n = i16(self.fp.read(2)) - 2 if self.info.get("exif") != original_exif:
self.info["exif"] = ImageFile._safe_read(self.fp, n)
self._reload_exif() self._reload_exif()
mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
if mptype.startswith("Large Thumbnail"):
exif = self.getexif().get_ifd(ExifTags.IFD.Exif)
if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963])
elif "exif" in self.info:
del self.info["exif"]
self._reload_exif()
self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))]
self.__frame = frame self.__frame = frame
def tell(self): def tell(self):

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;
@ -499,26 +519,27 @@ rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) {
} }
/* /*
* Conversion of RGB + single transparent color to RGBA, * Conversion of RGB + single transparent color either to
* where any pixel that matches the color will have the * RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, or
* alpha channel set to 0 * RGBa or La, where any pixel matching the color will have all channels set to 0
*/ */
static void static void
rgbT2rgba(UINT8 *out, int xsize, int r, int g, int b) { rgbT2a(UINT8 *out, UINT8 *in, int xsize, int r, int g, int b, int premultiplied) {
#ifdef WORDS_BIGENDIAN #ifdef WORDS_BIGENDIAN
UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff; UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff;
UINT32 repl = trns & 0xffffff00; UINT32 repl = premultiplied ? 0 : (trns & 0xffffff00);
#else #else
UINT32 trns = (0xffU << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); UINT32 trns = (0xffU << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff);
UINT32 repl = trns & 0x00ffffff; UINT32 repl = premultiplied ? 0 : (trns & 0x00ffffff);
#endif #endif
int i; int i;
for (i = 0; i < xsize; i++, out += sizeof(trns)) { UINT8 *ref = in != NULL ? in : out;
for (i = 0; i < xsize; i++, ref += sizeof(trns), out += sizeof(trns)) {
UINT32 v; UINT32 v;
memcpy(&v, out, sizeof(v)); memcpy(&v, ref, sizeof(v));
if (v == trns) { if (v == trns) {
memcpy(out, &repl, sizeof(repl)); memcpy(out, &repl, sizeof(repl));
} }
@ -941,12 +962,17 @@ static struct {
{"RGB", "1", rgb2bit}, {"RGB", "1", rgb2bit},
{"RGB", "L", rgb2l}, {"RGB", "L", rgb2l},
{"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},
{"RGB", "F", rgb2f}, {"RGB", "F", rgb2f},
{"RGB", "BGR;15", rgb2bgr15}, {"RGB", "BGR;15", rgb2bgr15},
{"RGB", "BGR;16", rgb2bgr16}, {"RGB", "BGR;16", rgb2bgr16},
{"RGB", "BGR;24", rgb2bgr24}, {"RGB", "BGR;24", rgb2bgr24},
{"RGB", "RGBA", rgb2rgba}, {"RGB", "RGBA", rgb2rgba},
{"RGB", "RGBa", rgb2rgba},
{"RGB", "RGBX", rgb2rgba}, {"RGB", "RGBX", rgb2rgba},
{"RGB", "CMYK", rgb2cmyk}, {"RGB", "CMYK", rgb2cmyk},
{"RGB", "YCbCr", ImagingConvertRGB2YCbCr}, {"RGB", "YCbCr", ImagingConvertRGB2YCbCr},
@ -1681,14 +1707,27 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
ImagingSectionCookie cookie; ImagingSectionCookie cookie;
ImagingShuffler convert; ImagingShuffler convert;
Imaging imOut = NULL; Imaging imOut = NULL;
int premultiplied = 0;
// If the transparency matches pixels in the source image, not the converted image
UINT8 *source;
int source_transparency = 0;
int y; int y;
if (!imIn) { if (!imIn) {
return (Imaging)ImagingError_ModeError(); return (Imaging)ImagingError_ModeError();
} }
if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 0) { if (strcmp(imIn->mode, "RGB") == 0 && (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBa") == 0)) {
convert = rgb2rgba; convert = rgb2rgba;
if (strcmp(mode, "RGBa") == 0) {
premultiplied = 1;
}
} else if (strcmp(imIn->mode, "RGB") == 0 && (strcmp(mode, "LA") == 0 || strcmp(mode, "La") == 0)) {
convert = rgb2la;
source_transparency = 1;
if (strcmp(mode, "La") == 0) {
premultiplied = 1;
}
} else if ((strcmp(imIn->mode, "1") == 0 || } else if ((strcmp(imIn->mode, "1") == 0 ||
strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "I") == 0 ||
strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "I;16") == 0 ||
@ -1726,7 +1765,9 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
ImagingSectionEnter(&cookie); ImagingSectionEnter(&cookie);
for (y = 0; y < imIn->ysize; y++) { for (y = 0; y < imIn->ysize; y++) {
(*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize); (*convert)((UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize);
rgbT2rgba((UINT8 *)imOut->image[y], imIn->xsize, r, g, b);
source = source_transparency ? (UINT8 *)imIn->image[y] : NULL;
rgbT2a((UINT8 *)imOut->image[y], source, imIn->xsize, r, g, b, premultiplied);
} }
ImagingSectionLeave(&cookie); ImagingSectionLeave(&cookie);

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",