mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
Merge branch 'main' into hopper-lru-cache
This commit is contained in:
commit
328052730d
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
|
@ -48,6 +48,21 @@ Thank you.
|
|||
* Python:
|
||||
* 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.
|
||||
|
||||
|
|
2
.github/workflows/wheels-dependencies.sh
vendored
2
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.3.1
|
||||
HARFBUZZ_VERSION=8.4.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.2
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
|
|
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -14,6 +15,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
|
|
@ -5,6 +5,15 @@ Changelog (Pillow)
|
|||
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
|
||||
[scaramallion, radarhere]
|
||||
|
||||
|
|
|
@ -267,8 +267,6 @@ def _cached_hopper(mode: str | None = None) -> Image.Image:
|
|||
return Image.open("Tests/images/hopper.ppm")
|
||||
if mode == "F":
|
||||
im = _cached_hopper("L").convert(mode)
|
||||
elif mode[:4] == "I;16":
|
||||
im = _cached_hopper("I").convert(mode)
|
||||
else:
|
||||
im = _cached_hopper().convert(mode)
|
||||
return im
|
||||
|
|
BIN
Tests/images/m13.fits
Normal file
BIN
Tests/images/m13.fits
Normal file
Binary file not shown.
366
Tests/images/m13_gzip.fits
Normal file
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 |
|
@ -117,9 +117,10 @@ def test_unsupported_module() -> None:
|
|||
features.version_module(module)
|
||||
|
||||
|
||||
def test_pilinfo() -> None:
|
||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||
def test_pilinfo(supported_formats) -> None:
|
||||
buf = io.StringIO()
|
||||
features.pilinfo(buf)
|
||||
features.pilinfo(buf, supported_formats=supported_formats)
|
||||
out = buf.getvalue()
|
||||
lines = out.splitlines()
|
||||
assert lines[0] == "-" * 68
|
||||
|
@ -129,9 +130,15 @@ def test_pilinfo() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
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 = (
|
||||
"\n"
|
||||
+ "-" * 68
|
||||
|
@ -142,4 +149,4 @@ def test_pilinfo() -> None:
|
|||
+ "-" * 68
|
||||
+ "\n"
|
||||
)
|
||||
assert jpeg in out
|
||||
assert supported_formats == (jpeg in out)
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
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"
|
||||
|
||||
|
@ -22,6 +22,11 @@ def test_open() -> None:
|
|||
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:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
|
|
@ -93,7 +93,7 @@ def test_exif(test_file: str) -> None:
|
|||
|
||||
def test_frame_size() -> None:
|
||||
# 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:
|
||||
assert im.size == (640, 480)
|
||||
|
||||
|
|
|
@ -33,36 +33,38 @@ from .helper import (
|
|||
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:
|
||||
@pytest.mark.parametrize(
|
||||
"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",
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_image_modes_success(self, mode: str) -> None:
|
||||
Image.new(mode, (1, 1))
|
||||
|
||||
|
@ -1042,6 +1044,35 @@ class TestImage:
|
|||
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):
|
||||
pass
|
||||
|
||||
|
|
|
@ -183,6 +183,14 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
||||
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")
|
||||
assert "transparency" in im_p.info
|
||||
im_p.save(f)
|
||||
|
@ -191,6 +199,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert "transparency" not in im_rgba.info
|
||||
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)
|
||||
assert "transparency" not in im_p.info
|
||||
im_p.save(f)
|
||||
|
|
|
@ -4,9 +4,16 @@ import os
|
|||
import subprocess
|
||||
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()
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Pillow ")
|
||||
|
@ -15,9 +22,15 @@ def test_main() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
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 = (
|
||||
os.linesep
|
||||
+ "-" * 68
|
||||
|
@ -31,4 +44,4 @@ def test_main() -> None:
|
|||
+ "-" * 68
|
||||
+ os.linesep
|
||||
)
|
||||
assert jpeg in out
|
||||
assert report == (jpeg not in out)
|
||||
|
|
|
@ -11,41 +11,12 @@ backend_class = build_wheel.__self__.__class__
|
|||
class _CustomBuildMetaBackend(backend_class):
|
||||
def run_setup(self, setup_script="setup.py"):
|
||||
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)
|
||||
|
||||
def build_wheel(
|
||||
|
@ -54,5 +25,15 @@ class _CustomBuildMetaBackend(backend_class):
|
|||
self.config_settings = config_settings
|
||||
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
|
||||
|
|
|
@ -1339,7 +1339,8 @@ FITS
|
|||
|
||||
.. 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
|
||||
^^^^^^^^
|
||||
|
|
|
@ -266,9 +266,10 @@ After navigating to the Pillow directory, run::
|
|||
Build Options
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
||||
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
|
||||
sets the number of CPUs to use, or can disable parallel building by
|
||||
* Config setting: ``-C parallel=n``. Can also be given
|
||||
with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
||||
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
|
||||
available, as many as are present.
|
||||
|
||||
|
@ -293,14 +294,13 @@ Build Options
|
|||
used to compile the standard Pillow wheels. Compiling libraqm requires
|
||||
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
|
||||
automated build systems that configure the proper paths in the
|
||||
environment variables (e.g. Buildroot).
|
||||
|
||||
* Build flag: ``-C debug=true``. Adds a debugging flag to the include and
|
||||
library search process to dump all paths searched for and found to
|
||||
stdout.
|
||||
* 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 stdout.
|
||||
|
||||
|
||||
Sample usage::
|
||||
|
|
26
setup.py
26
setup.py
|
@ -27,6 +27,9 @@ def get_version():
|
|||
return locals()["__version__"]
|
||||
|
||||
|
||||
configuration = {}
|
||||
|
||||
|
||||
PILLOW_VERSION = get_version()
|
||||
FREETYPE_ROOT = None
|
||||
HARFBUZZ_ROOT = None
|
||||
|
@ -333,15 +336,24 @@ class pil_build_ext(build_ext):
|
|||
+ [("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):
|
||||
self.disable_platform_guessing = None
|
||||
self.disable_platform_guessing = self.check_configuration(
|
||||
"platform-guessing", "disable"
|
||||
)
|
||||
self.add_imaging_libs = ""
|
||||
build_ext.initialize_options(self)
|
||||
for x in self.feature:
|
||||
setattr(self, f"disable_{x}", None)
|
||||
setattr(self, f"enable_{x}", None)
|
||||
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
|
||||
setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
|
||||
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):
|
||||
build_ext.finalize_options(self)
|
||||
|
@ -987,6 +999,12 @@ ext_modules = [
|
|||
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:
|
||||
setup(
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import math
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
@ -27,14 +28,32 @@ class FitsImageFile(ImageFile.ImageFile):
|
|||
assert self.fp is not None
|
||||
|
||||
headers: dict[bytes, bytes] = {}
|
||||
header_in_progress = False
|
||||
decoder_name = ""
|
||||
while True:
|
||||
header = self.fp.read(80)
|
||||
if not header:
|
||||
msg = "Truncated FITS file"
|
||||
raise OSError(msg)
|
||||
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
|
||||
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()
|
||||
if value.startswith(b"="):
|
||||
value = value[1:].strip()
|
||||
|
@ -43,32 +62,87 @@ class FitsImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError(msg)
|
||||
headers[keyword] = value
|
||||
|
||||
naxis = int(headers[b"NAXIS"])
|
||||
if naxis == 0:
|
||||
if not decoder_name:
|
||||
msg = "No image data"
|
||||
raise ValueError(msg)
|
||||
elif naxis == 1:
|
||||
self._size = 1, int(headers[b"NAXIS1"])
|
||||
else:
|
||||
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
|
||||
|
||||
number_of_bits = int(headers[b"BITPIX"])
|
||||
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"])
|
||||
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:
|
||||
self._mode = "L"
|
||||
elif number_of_bits == 16:
|
||||
self._mode = "I"
|
||||
self._mode = "I;16"
|
||||
elif number_of_bits == 32:
|
||||
self._mode = "I"
|
||||
elif number_of_bits in (-32, -64):
|
||||
self._mode = "F"
|
||||
|
||||
offset = math.ceil(self.fp.tell() / 2880) * 2880
|
||||
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
|
||||
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
|
||||
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
|
||||
|
||||
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
|
||||
Image.register_decoder("fits_gzip", FitsGzipDecoder)
|
||||
|
||||
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])
|
||||
|
|
|
@ -978,7 +978,7 @@ class Image:
|
|||
# transparency handling
|
||||
if has_transparency:
|
||||
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
|
||||
# color to an alpha channel.
|
||||
|
|
|
@ -24,14 +24,11 @@ import os
|
|||
import struct
|
||||
|
||||
from . import (
|
||||
ExifTags,
|
||||
Image,
|
||||
ImageFile,
|
||||
ImageSequence,
|
||||
JpegImagePlugin,
|
||||
TiffImagePlugin,
|
||||
)
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import o32le
|
||||
|
||||
|
||||
|
@ -109,7 +106,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
self._after_jpeg_open()
|
||||
|
||||
def _after_jpeg_open(self, mpheader=None):
|
||||
self._initial_size = self.size
|
||||
self.mpinfo = mpheader if mpheader is not None else self._getmp()
|
||||
self.n_frames = self.mpinfo[0xB001]
|
||||
self.__mpoffsets = [
|
||||
|
@ -137,27 +133,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
self.fp = self._fp
|
||||
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
|
||||
segment = self.fp.read(2)
|
||||
if not segment:
|
||||
if not self.fp.read(2):
|
||||
msg = "No data found for frame"
|
||||
raise ValueError(msg)
|
||||
self._size = self._initial_size
|
||||
if i16(segment) == 0xFFE1: # APP1
|
||||
n = i16(self.fp.read(2)) - 2
|
||||
self.info["exif"] = ImageFile._safe_read(self.fp, n)
|
||||
self.fp.seek(self.offset)
|
||||
JpegImagePlugin.JpegImageFile._open(self)
|
||||
if self.info.get("exif") != original_exif:
|
||||
self._reload_exif()
|
||||
|
||||
mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
|
||||
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.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
|
||||
self.__frame = frame
|
||||
|
||||
def tell(self):
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from .features import pilinfo
|
||||
|
||||
pilinfo()
|
||||
pilinfo(supported_formats="--report" not in sys.argv)
|
||||
|
|
|
@ -230,6 +230,9 @@ def pilinfo(out=None, supported_formats=True):
|
|||
"""
|
||||
Prints information about this installation of Pillow.
|
||||
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:
|
||||
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:]:
|
||||
print(f" {py_version.strip()}", 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(
|
||||
f"Python modules loaded from {os.path.dirname(Image.__file__)}",
|
||||
f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}",
|
||||
file=out,
|
||||
)
|
||||
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,
|
||||
)
|
||||
print("-" * 68, file=out)
|
||||
|
|
5
src/PIL/report.py
Normal file
5
src/PIL/report.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .features import pilinfo
|
||||
|
||||
pilinfo(supported_formats=False)
|
|
@ -1578,7 +1578,17 @@ if (PySequence_Check(op)) { \
|
|||
int bigendian = 0;
|
||||
if (image->type == IMAGING_TYPE_SPECIAL) {
|
||||
// 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++) {
|
||||
set_value_to_item(seq, i);
|
||||
|
|
|
@ -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
|
||||
rgb2f(UINT8 *out_, const UINT8 *in, int xsize) {
|
||||
int x;
|
||||
|
@ -499,26 +519,27 @@ rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) {
|
|||
}
|
||||
|
||||
/*
|
||||
* Conversion of RGB + single transparent color to RGBA,
|
||||
* where any pixel that matches the color will have the
|
||||
* alpha channel set to 0
|
||||
* Conversion of RGB + single transparent color either to
|
||||
* RGBA or LA, where any pixel matching the color will have the alpha channel set to 0, or
|
||||
* RGBa or La, where any pixel matching the color will have all channels set to 0
|
||||
*/
|
||||
|
||||
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
|
||||
UINT32 trns = ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff;
|
||||
UINT32 repl = trns & 0xffffff00;
|
||||
UINT32 repl = premultiplied ? 0 : (trns & 0xffffff00);
|
||||
#else
|
||||
UINT32 trns = (0xffU << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff);
|
||||
UINT32 repl = trns & 0x00ffffff;
|
||||
UINT32 repl = premultiplied ? 0 : (trns & 0x00ffffff);
|
||||
#endif
|
||||
|
||||
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;
|
||||
memcpy(&v, out, sizeof(v));
|
||||
memcpy(&v, ref, sizeof(v));
|
||||
if (v == trns) {
|
||||
memcpy(out, &repl, sizeof(repl));
|
||||
}
|
||||
|
@ -941,12 +962,17 @@ static struct {
|
|||
{"RGB", "1", rgb2bit},
|
||||
{"RGB", "L", rgb2l},
|
||||
{"RGB", "LA", rgb2la},
|
||||
{"RGB", "La", rgb2la},
|
||||
{"RGB", "I", rgb2i},
|
||||
{"RGB", "I;16", rgb2i16l},
|
||||
{"RGB", "I;16L", rgb2i16l},
|
||||
{"RGB", "I;16B", rgb2i16b},
|
||||
{"RGB", "F", rgb2f},
|
||||
{"RGB", "BGR;15", rgb2bgr15},
|
||||
{"RGB", "BGR;16", rgb2bgr16},
|
||||
{"RGB", "BGR;24", rgb2bgr24},
|
||||
{"RGB", "RGBA", rgb2rgba},
|
||||
{"RGB", "RGBa", rgb2rgba},
|
||||
{"RGB", "RGBX", rgb2rgba},
|
||||
{"RGB", "CMYK", rgb2cmyk},
|
||||
{"RGB", "YCbCr", ImagingConvertRGB2YCbCr},
|
||||
|
@ -1681,14 +1707,27 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
|
|||
ImagingSectionCookie cookie;
|
||||
ImagingShuffler convert;
|
||||
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;
|
||||
|
||||
if (!imIn) {
|
||||
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;
|
||||
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 ||
|
||||
strcmp(imIn->mode, "I") == 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);
|
||||
for (y = 0; y < imIn->ysize; y++) {
|
||||
(*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);
|
||||
|
||||
|
|
|
@ -87,11 +87,18 @@ are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:
|
|||
winbuild\build\build_env.cmd
|
||||
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
|
||||
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
|
||||
--------------
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ V = {
|
|||
"BROTLI": "1.1.0",
|
||||
"FREETYPE": "2.13.2",
|
||||
"FRIBIDI": "1.0.13",
|
||||
"HARFBUZZ": "8.3.1",
|
||||
"HARFBUZZ": "8.4.0",
|
||||
"JPEGTURBO": "3.0.2",
|
||||
"LCMS2": "2.16",
|
||||
"LIBPNG": "1.6.43",
|
||||
|
|
Loading…
Reference in New Issue
Block a user