mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-07 13:54:45 +03:00
Merge branch 'python-pillow:main' into main
This commit is contained in:
commit
e7d4fa6486
|
@ -2,15 +2,14 @@
|
|||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma:
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
exclude_also =
|
||||
# Don't complain if non-runnable code isn't run
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
# Don't complain about debug code
|
||||
if DEBUG:
|
||||
# Don't complain about compatibility code for missing optional dependencies
|
||||
except ImportError
|
||||
|
||||
[run]
|
||||
omit =
|
||||
|
|
18
.github/problem-matchers/gcc.json
vendored
Normal file
18
.github/problem-matchers/gcc.json
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "gcc-problem-matcher",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -86,6 +86,10 @@ jobs:
|
|||
env:
|
||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Register gcc problem matcher
|
||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
|
||||
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
.ci/build.sh
|
||||
|
|
1
.github/workflows/wheels.yml
vendored
1
.github/workflows/wheels.yml
vendored
|
@ -143,6 +143,7 @@ jobs:
|
|||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_SKIP: pp38-*
|
||||
CIBW_TEST_SKIP: "*-win_arm64"
|
||||
CIBW_TEST_COMMAND: 'docker run --rm
|
||||
-v {project}:C:\pillow
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.7
|
||||
rev: v0.1.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.12.0
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
|
|
29
CHANGES.rst
29
CHANGES.rst
|
@ -2,9 +2,36 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.2.0 (unreleased)
|
||||
10.2.0 (2024-01-02)
|
||||
-------------------
|
||||
|
||||
- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
|
||||
[bgilbert, radarhere]
|
||||
|
||||
- Trim glyph size in ImageFont.getmask() #7669, #7672
|
||||
[radarhere, nulano]
|
||||
|
||||
- Deprecate IptcImagePlugin helpers #7664
|
||||
[nulano, hugovk, radarhere]
|
||||
|
||||
- Allow uncompressed TIFF images to be saved in chunks #7650
|
||||
[radarhere]
|
||||
|
||||
- Concatenate multiple JPEG EXIF markers #7496
|
||||
[radarhere]
|
||||
|
||||
- Changed IPTC tile tuple to match other plugins #7661
|
||||
[radarhere]
|
||||
|
||||
- Do not assign new fp attribute when exiting context manager #7566
|
||||
[radarhere]
|
||||
|
||||
- Support arbitrary masks for uncompressed RGB DDS images #7589
|
||||
[radarhere, akx]
|
||||
|
||||
- Support setting ROWSPERSTRIP tag #7654
|
||||
[radarhere]
|
||||
|
||||
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
|
||||
[radarhere]
|
||||
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
|||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
|
||||
|
||||
Like PIL, Pillow is licensed under the open source HPND License:
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related
|
|||
and copy into `dist`. Check and upload them e.g.:
|
||||
```bash
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.0*
|
||||
python3 -m twine upload dist/pillow-5.2.0*
|
||||
```
|
||||
|
||||
## Publicize Release
|
||||
|
|
BIN
Tests/images/bgr15.dds
Normal file
BIN
Tests/images/bgr15.dds
Normal file
Binary file not shown.
BIN
Tests/images/bgr15.png
Normal file
BIN
Tests/images/bgr15.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
Tests/images/multiple_exif.jpg
Normal file
BIN
Tests/images/multiple_exif.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 364 B |
Binary file not shown.
|
@ -32,6 +32,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
|
|||
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
|
||||
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
|
||||
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
|
||||
TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
|
||||
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
||||
|
||||
|
||||
|
@ -249,6 +250,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
|
|||
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
|
||||
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
|
||||
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
|
||||
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
|
||||
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
||||
],
|
||||
)
|
||||
|
@ -341,16 +343,9 @@ def test_palette():
|
|||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
(
|
||||
"Tests/images/unsupported_bitcount_rgb.dds",
|
||||
"Tests/images/unsupported_bitcount_luminance.dds",
|
||||
),
|
||||
)
|
||||
def test_unsupported_bitcount(test_file):
|
||||
def test_unsupported_bitcount():
|
||||
with pytest.raises(OSError):
|
||||
with Image.open(test_file):
|
||||
with Image.open("Tests/images/unsupported_bitcount.dds"):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -6,11 +6,23 @@ import pytest
|
|||
|
||||
from PIL import Image, IptcImagePlugin
|
||||
|
||||
from .helper import hopper
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
TEST_FILE = "Tests/images/iptc.jpg"
|
||||
|
||||
|
||||
def test_open():
|
||||
expected = Image.new("L", (1, 1))
|
||||
|
||||
f = BytesIO(
|
||||
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
|
||||
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
|
||||
)
|
||||
with Image.open(f) as im:
|
||||
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_getiptcinfo_jpg_none():
|
||||
# Arrange
|
||||
with hopper() as im:
|
||||
|
@ -78,24 +90,28 @@ def test_i():
|
|||
c = b"a"
|
||||
|
||||
# Act
|
||||
ret = IptcImagePlugin.i(c)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
ret = IptcImagePlugin.i(c)
|
||||
|
||||
# Assert
|
||||
assert ret == 97
|
||||
|
||||
|
||||
def test_dump():
|
||||
def test_dump(monkeypatch):
|
||||
# Arrange
|
||||
c = b"abc"
|
||||
# Temporarily redirect stdout
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = mystdout = StringIO()
|
||||
mystdout = StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
# Act
|
||||
IptcImagePlugin.dump(c)
|
||||
|
||||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
with pytest.warns(DeprecationWarning):
|
||||
IptcImagePlugin.dump(c)
|
||||
|
||||
# Assert
|
||||
assert mystdout.getvalue() == "61 62 63 \n"
|
||||
|
||||
|
||||
def test_pad_deprecation():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert IptcImagePlugin.PAD == b"\0\0\0\0"
|
||||
|
|
|
@ -142,6 +142,19 @@ class TestFileJpeg:
|
|||
)
|
||||
assert k > 0.9
|
||||
|
||||
def test_rgb(self):
|
||||
def getchannels(im):
|
||||
return tuple(v[0] for v in im.layer)
|
||||
|
||||
im = hopper()
|
||||
im_ycbcr = self.roundtrip(im)
|
||||
assert getchannels(im_ycbcr) == (1, 2, 3)
|
||||
assert_image_similar(im, im_ycbcr, 17)
|
||||
|
||||
im_rgb = self.roundtrip(im, keep_rgb=True)
|
||||
assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B"))
|
||||
assert_image_similar(im, im_rgb, 12)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_image_path",
|
||||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||
|
@ -423,25 +436,28 @@ class TestFileJpeg:
|
|||
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
|
||||
|
||||
# experimental API
|
||||
im = self.roundtrip(hopper(), subsampling=-1) # default
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling=0) # 4:4:4
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling=1) # 4:2:2
|
||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling=2) # 4:2:0
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling=3) # default (undefined)
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
for subsampling in (-1, 3): # (default, invalid)
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
for subsampling in (0, "4:4:4"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
for subsampling in (1, "4:2:2"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||
for subsampling in (2, "4:2:0", "4:1:1"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
|
||||
im = self.roundtrip(hopper(), subsampling="4:4:4")
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling="4:2:2")
|
||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling="4:2:0")
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
im = self.roundtrip(hopper(), subsampling="4:1:1")
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
# RGB colorspace
|
||||
for subsampling in (-1, 0, "4:4:4"):
|
||||
# "4:4:4" doesn't really make sense for RGB, but the conversion
|
||||
# to an integer happens at a higher level
|
||||
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
|
||||
with pytest.raises(OSError):
|
||||
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
self.roundtrip(hopper(), subsampling="1:1:1")
|
||||
|
@ -840,6 +856,10 @@ class TestFileJpeg:
|
|||
# Act / Assert
|
||||
assert im._getexif()[306] == "2017:03:13 23:03:09"
|
||||
|
||||
def test_multiple_exif(self):
|
||||
with Image.open("Tests/images/multiple_exif.jpg") as im:
|
||||
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
|
|
@ -484,13 +484,13 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||
exif = im.getexif()
|
||||
exif[256] = 100
|
||||
exif[264] = 100
|
||||
|
||||
im.save(outfile, exif=exif)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
exif = im.getexif()
|
||||
assert exif[256] == 100
|
||||
assert exif[264] == 100
|
||||
|
||||
def test_reload_exif_after_seek(self):
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
|
@ -612,6 +612,14 @@ class TestFileTiff:
|
|||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_rowsperstrip(self, tmp_path):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
im.save(outfile, tiffinfo={278: 256})
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert im.tag_v2[278] == 256
|
||||
|
||||
def test_strip_raw(self):
|
||||
infile = "Tests/images/tiff_strip_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
|
@ -773,6 +781,27 @@ class TestFileTiff:
|
|||
4001,
|
||||
]
|
||||
|
||||
def test_tiff_chunks(self, tmp_path):
|
||||
tmpfile = str(tmp_path / "temp.tif")
|
||||
|
||||
im = hopper()
|
||||
with open(tmpfile, "wb") as fp:
|
||||
for y in range(0, 128, 32):
|
||||
chunk = im.crop((0, y, 128, y + 32))
|
||||
if y == 0:
|
||||
chunk.save(
|
||||
fp,
|
||||
"TIFF",
|
||||
tiffinfo={
|
||||
TiffImagePlugin.IMAGEWIDTH: 128,
|
||||
TiffImagePlugin.IMAGELENGTH: 128,
|
||||
},
|
||||
)
|
||||
else:
|
||||
fp.write(chunk.tobytes())
|
||||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_close_on_load_exclusive(self, tmp_path):
|
||||
# similar to test_fd_leak, but runs on unixlike os
|
||||
tmpfile = str(tmp_path / "temp.tif")
|
||||
|
|
|
@ -123,6 +123,7 @@ def test_write_metadata(tmp_path):
|
|||
"""Test metadata writing through the python code"""
|
||||
with Image.open("Tests/images/hopper.tif") as img:
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
del img.tag[278]
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
||||
original = img.tag_v2.named()
|
||||
|
@ -159,9 +160,11 @@ def test_change_stripbytecounts_tag_type(tmp_path):
|
|||
out = str(tmp_path / "temp.tiff")
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
info = im.tag_v2
|
||||
del info[278]
|
||||
|
||||
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
|
||||
im = im.resize((500, 500))
|
||||
info[TiffImagePlugin.IMAGEWIDTH] = im.width
|
||||
|
||||
# STRIPBYTECOUNTS can be a SHORT or a LONG
|
||||
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
|
||||
|
|
|
@ -1016,6 +1016,11 @@ class TestImage:
|
|||
except OSError as e:
|
||||
assert str(e) == "buffer overrun when reading image file"
|
||||
|
||||
def test_exit_fp(self):
|
||||
with Image.new("L", (1, 1)) as im:
|
||||
pass
|
||||
assert not hasattr(im, "fp")
|
||||
|
||||
def test_close_graceful(self, caplog):
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
copy = im.copy()
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
from __future__ import annotations
|
||||
import struct
|
||||
import pytest
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, features
|
||||
from PIL import Image, ImageDraw, ImageFont, features, _util
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
features.check_module("freetype2"),
|
||||
reason="PILfont superseded if FreeType is supported",
|
||||
)
|
||||
original_core = ImageFont.core
|
||||
|
||||
|
||||
def setup_module():
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
|
||||
|
||||
def teardown_module():
|
||||
ImageFont.core = original_core
|
||||
|
||||
|
||||
def test_default_font():
|
||||
|
@ -44,3 +52,25 @@ def test_textbbox():
|
|||
default_font = ImageFont.load_default()
|
||||
assert d.textlength("test", font=default_font) == 24
|
||||
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
|
||||
|
||||
|
||||
def test_decompression_bomb():
|
||||
glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256)
|
||||
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
|
||||
|
||||
font = ImageFont.ImageFont()
|
||||
font._load_pilfont_data(fp, Image.new("L", (256, 256)))
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
font.getmask("A" * 1_000_000)
|
||||
|
||||
|
||||
@pytest.mark.timeout(4)
|
||||
def test_oom():
|
||||
glyph = struct.pack(
|
||||
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
|
||||
)
|
||||
fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
|
||||
|
||||
font = ImageFont.ImageFont()
|
||||
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
|
||||
font.getmask("A" * 1_000_000)
|
||||
|
|
|
@ -57,15 +57,6 @@ def test_str_to_img():
|
|||
assert_image_equal_tofile(A, "Tests/images/morph_a.png")
|
||||
|
||||
|
||||
def create_lut():
|
||||
for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
|
||||
lb = ImageMorph.LutBuilder(op_name=op)
|
||||
lut = lb.build_lut()
|
||||
with open(f"Tests/images/{op}.lut", "wb") as f:
|
||||
f.write(lut)
|
||||
|
||||
|
||||
# create_lut()
|
||||
@pytest.mark.parametrize(
|
||||
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
||||
)
|
||||
|
|
|
@ -66,7 +66,7 @@ def test_deferred_error():
|
|||
# Arrange
|
||||
|
||||
# Act
|
||||
thing = _util.DeferredError(ValueError("Some error text"))
|
||||
thing = _util.DeferredError.new(ValueError("Some error text"))
|
||||
|
||||
# Assert
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
|||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source PIL
|
||||
Software License:
|
||||
|
|
|
@ -69,6 +69,14 @@ can be found here.
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.ImageMode` Module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: PIL.ImageMode
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.ImageTransform` Module
|
||||
---------------------------------
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ master_doc = "index"
|
|||
# General information about the project.
|
||||
project = "Pillow (PIL Fork)"
|
||||
copyright = (
|
||||
"1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
|
||||
"1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
|
||||
)
|
||||
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
|
||||
|
||||
|
|
|
@ -44,6 +44,17 @@ ImageFile.raise_oserror
|
|||
error codes returned by a codec's ``decode()`` method, which ImageFile already does
|
||||
automatically.
|
||||
|
||||
IptcImageFile helper functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 10.2.0
|
||||
|
||||
The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
|
||||
``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
|
||||
12.0.0 (2025-10-15). These are undocumented helper functions intended
|
||||
for internal use, so there is no replacement. They can each be replaced
|
||||
by a single line of code using builtin functions in Python.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -487,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
**exif**
|
||||
If present, the image will be stored with the provided raw EXIF data.
|
||||
|
||||
**keep_rgb**
|
||||
By default, libjpeg converts images with an RGB color space to YCbCr.
|
||||
If this option is present and true, those images will be stored as RGB
|
||||
instead.
|
||||
|
||||
When this option is enabled, attempting to chroma-subsample RGB images
|
||||
with the ``subsampling`` option will raise an :py:exc:`OSError`.
|
||||
|
||||
.. versionadded:: 10.2.0
|
||||
|
||||
**subsampling**
|
||||
If present, sets the subsampling for the encoder.
|
||||
|
||||
|
|
|
@ -542,7 +542,7 @@ Reading from URL
|
|||
|
||||
from PIL import Image
|
||||
from urllib.request import urlopen
|
||||
url = "https://python-pillow.org/images/pillow-logo.png"
|
||||
url = "https://python-pillow.org/assets/images/pillow-logo.png"
|
||||
img = Image.open(urlopen(url))
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,19 @@ Internal Modules
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL._typing` Module
|
||||
--------------------------
|
||||
|
||||
.. module:: PIL._typing
|
||||
|
||||
Provides a convenient way to import type hints that are not available
|
||||
on some Python versions.
|
||||
|
||||
.. py:data:: TypeGuard
|
||||
:value: typing.TypeGuard
|
||||
|
||||
See :py:obj:`typing.TypeGuard`.
|
||||
|
||||
:mod:`~PIL._util` Module
|
||||
------------------------
|
||||
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
10.2.0
|
||||
------
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
|
@ -20,10 +12,14 @@ ImageFile.raise_oserror
|
|||
error codes returned by a codec's ``decode()`` method, which ImageFile already does
|
||||
automatically.
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
IptcImageFile helper functions
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
|
||||
``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
|
||||
12.0.0 (2025-10-15). These are undocumented helper functions intended
|
||||
for internal use, so there is no replacement. They can each be replaced
|
||||
by a single line of code using builtin functions in Python.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
@ -46,6 +42,14 @@ Added DdsImagePlugin enums
|
|||
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
|
||||
enums have been added to :py:class:`PIL.DdsImagePlugin`.
|
||||
|
||||
JPEG RGB color space
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB
|
||||
images in the RGB color space instead of being converted to YCbCr automatically by
|
||||
libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with
|
||||
the ``subsampling`` option will raise an :py:exc:`OSError`.
|
||||
|
||||
JPEG restart marker interval
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -73,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed
|
|||
This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It
|
||||
can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
|
||||
|
||||
A decompression bomb check has also been added to
|
||||
:py:meth:`PIL.ImageFont.ImageFont.getmask`.
|
||||
|
||||
ImageFont.getmask: Trim glyph size
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To protect against potential DOS attacks when using PIL fonts,
|
||||
:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that
|
||||
they do not extend beyond the bitmap image.
|
||||
|
||||
ImageMath.eval: Restricted environment keys
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -92,16 +106,56 @@ Support has been added to read the BC4U format of DDS images.
|
|||
Support has also been added to read DX10 BC1 and BC4, whether UNORM or
|
||||
TYPELESS.
|
||||
|
||||
Support arbitrary masks for uncompressed RGB DDS images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
All masks are now supported when reading DDS images with uncompressed RGB data,
|
||||
allowing for bit counts other than 24 and 32.
|
||||
|
||||
Saving TIFF tag RowsPerStrip
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by
|
||||
the user, rather than always being calculated by Pillow.
|
||||
|
||||
Optimized ImageColor.getrgb and getcolor
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and
|
||||
:py:attr:`~PIL.ImageColor.getcolor` are now cached using
|
||||
:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times
|
||||
as fast and ``getcolor`` are 5.1 - 19.6 times as fast.
|
||||
|
||||
Optimized ImageMode.getmode
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using
|
||||
:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as
|
||||
fast.
|
||||
|
||||
Optimized ImageStat.Stat count and extrema
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
|
||||
:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
|
||||
histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast
|
||||
on average and ``st.extrema`` is 12x as fast on average.
|
||||
histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on
|
||||
average and ``st.extrema`` is 12 times as fast on average.
|
||||
|
||||
Encoder errors now report error detail as string
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:py:exc:`OSError` exceptions from image encoders now include a textual description of
|
||||
the error instead of a numeric error code.
|
||||
|
||||
Type hints
|
||||
^^^^^^^^^^
|
||||
|
||||
Work has begun to add type annotations to Pillow, including:
|
||||
|
||||
* :py:mod:`~PIL.ContainerIO`
|
||||
* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile`
|
||||
* :py:mod:`~PIL.ImageChops`
|
||||
* :py:mod:`~PIL.ImageMode`
|
||||
* :py:mod:`~PIL.ImageSequence`
|
||||
* :py:mod:`~PIL.ImageTransform`
|
||||
* :py:mod:`~PIL.TarIO`
|
||||
|
|
|
@ -65,6 +65,9 @@ tests = [
|
|||
"pytest-cov",
|
||||
"pytest-timeout",
|
||||
]
|
||||
typing = [
|
||||
'typing-extensions; python_version < "3.10"',
|
||||
]
|
||||
xmp = [
|
||||
"defusedxml",
|
||||
]
|
||||
|
@ -143,9 +146,6 @@ exclude = [
|
|||
'^src/PIL/DdsImagePlugin.py$',
|
||||
'^src/PIL/FpxImagePlugin.py$',
|
||||
'^src/PIL/Image.py$',
|
||||
'^src/PIL/ImageCms.py$',
|
||||
'^src/PIL/ImageFile.py$',
|
||||
'^src/PIL/ImageFont.py$',
|
||||
'^src/PIL/ImageMath.py$',
|
||||
'^src/PIL/ImageMorph.py$',
|
||||
'^src/PIL/ImageQt.py$',
|
||||
|
|
|
@ -18,6 +18,7 @@ from enum import IntEnum, IntFlag
|
|||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i32le as i32
|
||||
from ._binary import o8
|
||||
from ._binary import o32le as o32
|
||||
|
||||
# Magic ("DDS ")
|
||||
|
@ -341,6 +342,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
|
||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||
self._size = (width, height)
|
||||
extents = (0, 0) + self.size
|
||||
|
||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||
struct.unpack("<11I", header.read(44)) # reserved
|
||||
|
@ -351,22 +353,16 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
rawmode = None
|
||||
if pfflags & DDPF.RGB:
|
||||
# Texture contains uncompressed RGB data
|
||||
masks = struct.unpack("<4I", header.read(16))
|
||||
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
|
||||
if bitcount == 24:
|
||||
self._mode = "RGB"
|
||||
rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
|
||||
elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
|
||||
if pfflags & DDPF.ALPHAPIXELS:
|
||||
self._mode = "RGBA"
|
||||
rawmode = (
|
||||
masks[0x000000FF]
|
||||
+ masks[0x0000FF00]
|
||||
+ masks[0x00FF0000]
|
||||
+ masks[0xFF000000]
|
||||
)
|
||||
mask_count = 4
|
||||
else:
|
||||
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
|
||||
raise OSError(msg)
|
||||
self._mode = "RGB"
|
||||
mask_count = 3
|
||||
|
||||
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
|
||||
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
|
||||
return
|
||||
elif pfflags & DDPF.LUMINANCE:
|
||||
if bitcount == 8:
|
||||
self._mode = "L"
|
||||
|
@ -464,7 +460,6 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
msg = f"Unknown pixel format flags {pfflags}"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
extents = (0, 0) + self.size
|
||||
if n:
|
||||
self.tile = [
|
||||
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
||||
|
@ -476,6 +471,39 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
|
||||
|
||||
class DdsRgbDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
bitcount, masks = self.args
|
||||
|
||||
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
|
||||
# Calculate how many zeros each mask is padded with
|
||||
mask_offsets = []
|
||||
# And the maximum value of each channel without the padding
|
||||
mask_totals = []
|
||||
for mask in masks:
|
||||
offset = 0
|
||||
if mask != 0:
|
||||
while mask >> (offset + 1) << (offset + 1) == mask:
|
||||
offset += 1
|
||||
mask_offsets.append(offset)
|
||||
mask_totals.append(mask >> offset)
|
||||
|
||||
data = bytearray()
|
||||
bytecount = bitcount // 8
|
||||
while len(data) < self.state.xsize * self.state.ysize * len(masks):
|
||||
value = int.from_bytes(self.fd.read(bytecount), "little")
|
||||
for i, mask in enumerate(masks):
|
||||
masked_value = value & mask
|
||||
# Remove the zero padding, and scale it to 8 bits
|
||||
data += o8(
|
||||
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
|
||||
)
|
||||
self.set_as_raw(bytes(data))
|
||||
return -1, 0
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||
msg = f"cannot write mode {im.mode} as DDS"
|
||||
|
@ -533,5 +561,6 @@ def _accept(prefix):
|
|||
|
||||
|
||||
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
||||
Image.register_decoder("dds_rgb", DdsRgbDecoder)
|
||||
Image.register_save(DdsImageFile.format, _save)
|
||||
Image.register_extension(DdsImageFile.format, ".dds")
|
||||
|
|
|
@ -92,7 +92,7 @@ try:
|
|||
raise ImportError(msg)
|
||||
|
||||
except ImportError as v:
|
||||
core = DeferredError(ImportError("The _imaging C module is not installed."))
|
||||
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
|
||||
# Explanations for ways that we know we might have an import error
|
||||
if str(v).startswith("Module use of python"):
|
||||
# The _imaging C module is present, but not compiled for
|
||||
|
@ -530,15 +530,19 @@ class Image:
|
|||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def _close_fp(self):
|
||||
if getattr(self, "_fp", False):
|
||||
if self._fp != self.fp:
|
||||
self._fp.close()
|
||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
|
||||
def __exit__(self, *args):
|
||||
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
|
||||
if getattr(self, "_fp", False):
|
||||
if self._fp != self.fp:
|
||||
self._fp.close()
|
||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
if hasattr(self, "fp"):
|
||||
if getattr(self, "_exclusive_fp", False):
|
||||
self._close_fp()
|
||||
self.fp = None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
@ -554,12 +558,7 @@ class Image:
|
|||
"""
|
||||
if hasattr(self, "fp"):
|
||||
try:
|
||||
if getattr(self, "_fp", False):
|
||||
if self._fp != self.fp:
|
||||
self._fp.close()
|
||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
self._close_fp()
|
||||
self.fp = None
|
||||
except Exception as msg:
|
||||
logger.debug("Error closing: %s", msg)
|
||||
|
@ -2644,7 +2643,7 @@ class Image:
|
|||
resample=Resampling.NEAREST,
|
||||
fill=1,
|
||||
fillcolor=None,
|
||||
):
|
||||
) -> Image:
|
||||
"""
|
||||
Transforms this image. This method creates a new image with the
|
||||
given size, and the same mode as the original, and copies data
|
||||
|
@ -3191,7 +3190,7 @@ def _decompression_bomb_check(size):
|
|||
)
|
||||
|
||||
|
||||
def open(fp, mode="r", formats=None):
|
||||
def open(fp, mode="r", formats=None) -> Image:
|
||||
"""
|
||||
Opens and identifies the given image file.
|
||||
|
||||
|
@ -3416,7 +3415,7 @@ def merge(mode, bands):
|
|||
# Plugin registry
|
||||
|
||||
|
||||
def register_open(id, factory, accept=None):
|
||||
def register_open(id, factory, accept=None) -> None:
|
||||
"""
|
||||
Register an image file plugin. This function should not be used
|
||||
in application code.
|
||||
|
@ -3470,7 +3469,7 @@ def register_save_all(id, driver):
|
|||
SAVE_ALL[id.upper()] = driver
|
||||
|
||||
|
||||
def register_extension(id, extension):
|
||||
def register_extension(id, extension) -> None:
|
||||
"""
|
||||
Registers an image extension. This function should not be
|
||||
used in application code.
|
||||
|
|
|
@ -28,7 +28,7 @@ except ImportError as ex:
|
|||
# anything in core.
|
||||
from ._util import DeferredError
|
||||
|
||||
_imagingcms = DeferredError(ex)
|
||||
_imagingcms = DeferredError.new(ex)
|
||||
|
||||
DESCRIPTION = """
|
||||
pyCMS
|
||||
|
|
|
@ -32,7 +32,7 @@ import io
|
|||
import itertools
|
||||
import struct
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from . import Image
|
||||
from ._deprecate import deprecate
|
||||
|
@ -94,7 +94,7 @@ class _Tile(NamedTuple):
|
|||
encoder_name: str
|
||||
extents: tuple[int, int, int, int]
|
||||
offset: int
|
||||
args: tuple | str | None
|
||||
args: tuple[Any, ...] | str | None
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter):
|
|||
if hasattr(table, "shape"):
|
||||
try:
|
||||
import numpy
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if numpy and isinstance(table, numpy.ndarray):
|
||||
|
|
|
@ -34,7 +34,7 @@ import warnings
|
|||
from enum import IntEnum
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
from typing import BinaryIO
|
||||
|
||||
from . import Image
|
||||
from ._util import is_directory, is_path
|
||||
|
@ -53,7 +53,7 @@ try:
|
|||
except ImportError as ex:
|
||||
from ._util import DeferredError
|
||||
|
||||
core = DeferredError(ex)
|
||||
core = DeferredError.new(ex)
|
||||
|
||||
|
||||
def _string_length_check(text):
|
||||
|
@ -150,6 +150,7 @@ class ImageFont:
|
|||
:py:mod:`PIL.Image.core` interface module.
|
||||
"""
|
||||
_string_length_check(text)
|
||||
Image._decompression_bomb_check(self.font.getsize(text))
|
||||
return self.font.getmask(text, mode)
|
||||
|
||||
def getbbox(self, text, *args, **kwargs):
|
||||
|
@ -192,7 +193,7 @@ class FreeTypeFont:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
font: bytes | str | Path | IO | None = None,
|
||||
font: bytes | str | Path | BinaryIO | None = None,
|
||||
size: float = 10,
|
||||
index: int = 0,
|
||||
encoding: str = "",
|
||||
|
|
|
@ -14,17 +14,26 @@
|
|||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from . import Image
|
||||
|
||||
|
||||
class Transform(Image.ImageTransformHandler):
|
||||
def __init__(self, data):
|
||||
method: Image.Transform
|
||||
|
||||
def __init__(self, data: Sequence[int]) -> None:
|
||||
self.data = data
|
||||
|
||||
def getdata(self):
|
||||
def getdata(self) -> tuple[int, Sequence[int]]:
|
||||
return self.method, self.data
|
||||
|
||||
def transform(self, size, image, **options):
|
||||
def transform(
|
||||
self,
|
||||
size: tuple[int, int],
|
||||
image: Image.Image,
|
||||
**options: dict[str, str | int | tuple[int, ...] | list[int]],
|
||||
) -> Image.Image:
|
||||
# can be overridden
|
||||
method, data = self.getdata()
|
||||
return image.transform(size, method, data, **options)
|
||||
|
|
|
@ -16,31 +16,46 @@
|
|||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from io import BytesIO
|
||||
from typing import Sequence
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
from ._deprecate import deprecate
|
||||
|
||||
COMPRESSION = {1: "raw", 5: "jpeg"}
|
||||
|
||||
PAD = b"\0\0\0\0"
|
||||
|
||||
def __getattr__(name: str) -> bytes:
|
||||
if name == "PAD":
|
||||
deprecate("IptcImagePlugin.PAD", 12)
|
||||
return b"\0\0\0\0"
|
||||
msg = f"module '{__name__}' has no attribute '{name}'"
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
|
||||
|
||||
def _i(c: bytes) -> int:
|
||||
return i32((b"\0\0\0\0" + c)[-4:])
|
||||
|
||||
|
||||
def _i8(c: int | bytes) -> int:
|
||||
return c if isinstance(c, int) else c[0]
|
||||
|
||||
|
||||
def i(c):
|
||||
return i32((PAD + c)[-4:])
|
||||
def i(c: bytes) -> int:
|
||||
""".. deprecated:: 10.2.0"""
|
||||
deprecate("IptcImagePlugin.i", 12)
|
||||
return _i(c)
|
||||
|
||||
|
||||
def dump(c):
|
||||
def dump(c: Sequence[int | bytes]) -> None:
|
||||
""".. deprecated:: 10.2.0"""
|
||||
deprecate("IptcImagePlugin.dump", 12)
|
||||
for i in c:
|
||||
print("%02x" % _i8(i), end=" ")
|
||||
print()
|
||||
|
@ -55,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
format = "IPTC"
|
||||
format_description = "IPTC/NAA"
|
||||
|
||||
def getint(self, key):
|
||||
return i(self.info[key])
|
||||
def getint(self, key: tuple[int, int]) -> int:
|
||||
return _i(self.info[key])
|
||||
|
||||
def field(self):
|
||||
def field(self) -> tuple[tuple[int, int] | None, int]:
|
||||
#
|
||||
# get a IPTC field header
|
||||
s = self.fp.read(5)
|
||||
|
@ -80,13 +95,13 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
elif size == 128:
|
||||
size = 0
|
||||
elif size > 128:
|
||||
size = i(self.fp.read(size - 128))
|
||||
size = _i(self.fp.read(size - 128))
|
||||
else:
|
||||
size = i16(s, 3)
|
||||
|
||||
return tag, size
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
# load descriptive fields
|
||||
while True:
|
||||
offset = self.fp.tell()
|
||||
|
@ -131,27 +146,22 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
|
||||
# tile
|
||||
if tag == (8, 10):
|
||||
self.tile = [
|
||||
("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
|
||||
]
|
||||
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
|
||||
|
||||
def load(self):
|
||||
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
type, tile, box = self.tile[0]
|
||||
|
||||
encoding, offset = tile
|
||||
offset, compression = self.tile[0][2:]
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# Copy image data to temporary file
|
||||
o_fd, outfile = tempfile.mkstemp(text=False)
|
||||
o = os.fdopen(o_fd)
|
||||
if encoding == "raw":
|
||||
o = BytesIO()
|
||||
if compression == "raw":
|
||||
# To simplify access to the extracted file,
|
||||
# prepend a PPM header
|
||||
o.write("P5\n%d %d\n255\n" % self.size)
|
||||
o.write(b"P5\n%d %d\n255\n" % self.size)
|
||||
while True:
|
||||
type, size = self.field()
|
||||
if type != (8, 10):
|
||||
|
@ -162,17 +172,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
break
|
||||
o.write(s)
|
||||
size -= len(s)
|
||||
o.close()
|
||||
|
||||
try:
|
||||
with Image.open(outfile) as _im:
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
except OSError:
|
||||
pass
|
||||
with Image.open(o) as _im:
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
|
||||
|
||||
Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||
|
@ -188,8 +191,6 @@ def getiptcinfo(im):
|
|||
:returns: A dictionary containing IPTC information, or None if
|
||||
no IPTC information block was found.
|
||||
"""
|
||||
import io
|
||||
|
||||
from . import JpegImagePlugin, TiffImagePlugin
|
||||
|
||||
data = None
|
||||
|
@ -224,7 +225,7 @@ def getiptcinfo(im):
|
|||
|
||||
# parse the IPTC information chunk
|
||||
im.info = {}
|
||||
im.fp = io.BytesIO(data)
|
||||
im.fp = BytesIO(data)
|
||||
|
||||
try:
|
||||
im._open()
|
||||
|
|
|
@ -87,10 +87,12 @@ def APP(self, marker):
|
|||
self.info["dpi"] = jfif_density
|
||||
self.info["jfif_unit"] = jfif_unit
|
||||
self.info["jfif_density"] = jfif_density
|
||||
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
|
||||
if "exif" not in self.info:
|
||||
# extract EXIF information (incomplete)
|
||||
self.info["exif"] = s # FIXME: value will change
|
||||
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
|
||||
# extract EXIF information
|
||||
if "exif" in self.info:
|
||||
self.info["exif"] += s[6:]
|
||||
else:
|
||||
self.info["exif"] = s
|
||||
self._exif_offset = self.fp.tell() - n + 6
|
||||
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
||||
# extract FlashPix information (incomplete)
|
||||
|
@ -783,6 +785,7 @@ def _save(im, fp, filename):
|
|||
progressive,
|
||||
info.get("smooth", 0),
|
||||
optimize,
|
||||
info.get("keep_rgb", False),
|
||||
info.get("streamtype", 0),
|
||||
dpi[0],
|
||||
dpi[1],
|
||||
|
|
|
@ -43,7 +43,7 @@ except ImportError as ex:
|
|||
# anything in core.
|
||||
from ._util import DeferredError
|
||||
|
||||
FFI = ffi = DeferredError(ex)
|
||||
FFI = ffi = DeferredError.new(ex)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1704,25 +1704,27 @@ def _save(im, fp, filename):
|
|||
colormap += [0] * (256 - colors)
|
||||
ifd[COLORMAP] = colormap
|
||||
# data orientation
|
||||
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
|
||||
# aim for given strip size (64 KB by default) when using libtiff writer
|
||||
if libtiff:
|
||||
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
|
||||
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
|
||||
# JPEG encoder expects multiple of 8 rows
|
||||
if compression == "jpeg":
|
||||
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
|
||||
else:
|
||||
rows_per_strip = im.size[1]
|
||||
if rows_per_strip == 0:
|
||||
rows_per_strip = 1
|
||||
strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
|
||||
strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
|
||||
ifd[ROWSPERSTRIP] = rows_per_strip
|
||||
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
|
||||
stride = len(bits) * ((w * bits[0] + 7) // 8)
|
||||
if ROWSPERSTRIP not in ifd:
|
||||
# aim for given strip size (64 KB by default) when using libtiff writer
|
||||
if libtiff:
|
||||
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
|
||||
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
|
||||
# JPEG encoder expects multiple of 8 rows
|
||||
if compression == "jpeg":
|
||||
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
|
||||
else:
|
||||
rows_per_strip = h
|
||||
if rows_per_strip == 0:
|
||||
rows_per_strip = 1
|
||||
ifd[ROWSPERSTRIP] = rows_per_strip
|
||||
strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
|
||||
strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
|
||||
if strip_byte_counts >= 2**16:
|
||||
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
|
||||
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
|
||||
stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
|
||||
stride * h - strip_byte_counts * (strips_per_image - 1),
|
||||
)
|
||||
ifd[STRIPOFFSETS] = tuple(
|
||||
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
|
||||
|
|
5
src/PIL/_imagingcms.pyi
Normal file
5
src/PIL/_imagingcms.pyi
Normal file
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
5
src/PIL/_imagingft.pyi
Normal file
5
src/PIL/_imagingft.pyi
Normal file
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
18
src/PIL/_typing.py
Normal file
18
src/PIL/_typing.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeGuard
|
||||
else:
|
||||
try:
|
||||
from typing_extensions import TypeGuard
|
||||
except ImportError:
|
||||
from typing import Any
|
||||
|
||||
class TypeGuard: # type: ignore[no-redef]
|
||||
def __class_getitem__(cls, item: Any) -> type[bool]:
|
||||
return bool
|
||||
|
||||
|
||||
__all__ = ["TypeGuard"]
|
|
@ -2,20 +2,31 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, NoReturn
|
||||
|
||||
from ._typing import TypeGuard
|
||||
|
||||
|
||||
def is_path(f):
|
||||
def is_path(f: Any) -> TypeGuard[bytes | str | Path]:
|
||||
return isinstance(f, (bytes, str, Path))
|
||||
|
||||
|
||||
def is_directory(f):
|
||||
def is_directory(f: Any) -> TypeGuard[bytes | str | Path]:
|
||||
"""Checks if an object is a string, and that it points to a directory."""
|
||||
return is_path(f) and os.path.isdir(f)
|
||||
|
||||
|
||||
class DeferredError:
|
||||
def __init__(self, ex):
|
||||
def __init__(self, ex: BaseException):
|
||||
self.ex = ex
|
||||
|
||||
def __getattr__(self, elt):
|
||||
def __getattr__(self, elt: str) -> NoReturn:
|
||||
raise self.ex
|
||||
|
||||
@staticmethod
|
||||
def new(ex: BaseException) -> Any:
|
||||
"""
|
||||
Creates an object that raises the wrapped exception ``ex`` when used,
|
||||
and casts it to :py:obj:`~typing.Any` type.
|
||||
"""
|
||||
return DeferredError(ex)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Master version for Pillow
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "10.2.0.dev0"
|
||||
__version__ = "10.3.0.dev0"
|
||||
|
|
|
@ -2649,6 +2649,26 @@ _font_new(PyObject *self_, PyObject *args) {
|
|||
self->glyphs[i].sy0 = S16(B16(glyphdata, 14));
|
||||
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
|
||||
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
|
||||
|
||||
// Do not allow glyphs to extend beyond bitmap image
|
||||
// Helps prevent DOS by stopping cropped images being larger than the original
|
||||
if (self->glyphs[i].sx0 < 0) {
|
||||
self->glyphs[i].dx0 -= self->glyphs[i].sx0;
|
||||
self->glyphs[i].sx0 = 0;
|
||||
}
|
||||
if (self->glyphs[i].sy0 < 0) {
|
||||
self->glyphs[i].dy0 -= self->glyphs[i].sy0;
|
||||
self->glyphs[i].sy0 = 0;
|
||||
}
|
||||
if (self->glyphs[i].sx1 > self->bitmap->xsize) {
|
||||
self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize;
|
||||
self->glyphs[i].sx1 = self->bitmap->xsize;
|
||||
}
|
||||
if (self->glyphs[i].sy1 > self->bitmap->ysize) {
|
||||
self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize;
|
||||
self->glyphs[i].sy1 = self->bitmap->ysize;
|
||||
}
|
||||
|
||||
if (self->glyphs[i].dy0 < y0) {
|
||||
y0 = self->glyphs[i].dy0;
|
||||
}
|
||||
|
@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
|
|||
static PyObject *
|
||||
_font_getmask(ImagingFontObject *self, PyObject *args) {
|
||||
Imaging im;
|
||||
Imaging bitmap;
|
||||
Imaging bitmap = NULL;
|
||||
int x, b;
|
||||
int i = 0;
|
||||
int status;
|
||||
|
@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
|||
PyObject *encoded_string;
|
||||
|
||||
unsigned char *text;
|
||||
char *mode = "";
|
||||
char *mode;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
|
||||
return NULL;
|
||||
|
@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
|||
b = self->baseline;
|
||||
for (x = 0; text[i]; i++) {
|
||||
glyph = &self->glyphs[text[i]];
|
||||
bitmap =
|
||||
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
|
||||
if (!bitmap) {
|
||||
goto failed;
|
||||
if (i == 0 || text[i] != text[i - 1]) {
|
||||
ImagingDelete(bitmap);
|
||||
bitmap =
|
||||
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
|
||||
if (!bitmap) {
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
status = ImagingPaste(
|
||||
im,
|
||||
|
@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
|||
glyph->dy0 + b,
|
||||
glyph->dx1 + x,
|
||||
glyph->dy1 + b);
|
||||
ImagingDelete(bitmap);
|
||||
if (status < 0) {
|
||||
goto failed;
|
||||
}
|
||||
x = x + glyph->dx;
|
||||
b = b + glyph->dy;
|
||||
}
|
||||
ImagingDelete(bitmap);
|
||||
free(text);
|
||||
return PyImagingNew(im);
|
||||
|
||||
failed:
|
||||
ImagingDelete(bitmap);
|
||||
free(text);
|
||||
ImagingDelete(im);
|
||||
Py_RETURN_NONE;
|
||||
|
|
|
@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
|||
Py_ssize_t progressive = 0;
|
||||
Py_ssize_t smooth = 0;
|
||||
Py_ssize_t optimize = 0;
|
||||
int keep_rgb = 0;
|
||||
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
|
||||
Py_ssize_t xdpi = 0, ydpi = 0;
|
||||
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
|
||||
|
@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
|||
|
||||
if (!PyArg_ParseTuple(
|
||||
args,
|
||||
"ss|nnnnnnnnnnOz#y#y#",
|
||||
"ss|nnnnpnnnnnnOz#y#y#",
|
||||
&mode,
|
||||
&rawmode,
|
||||
&quality,
|
||||
&progressive,
|
||||
&smooth,
|
||||
&optimize,
|
||||
&keep_rgb,
|
||||
&streamtype,
|
||||
&xdpi,
|
||||
&ydpi,
|
||||
|
@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
|||
|
||||
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
|
||||
|
||||
((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
|
||||
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
|
||||
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
|
||||
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
|
||||
|
|
|
@ -74,6 +74,9 @@ typedef struct {
|
|||
/* Optimize Huffman tables (slow) */
|
||||
int optimize;
|
||||
|
||||
/* Disable automatic conversion of RGB images to YCbCr if nonzero */
|
||||
int keep_rgb;
|
||||
|
||||
/* Stream type (0=full, 1=tables only, 2=image only) */
|
||||
int streamtype;
|
||||
|
||||
|
|
|
@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
|||
/* Compressor configuration */
|
||||
jpeg_set_defaults(&context->cinfo);
|
||||
|
||||
/* Prevent RGB -> YCbCr conversion */
|
||||
if (context->keep_rgb) {
|
||||
switch (context->cinfo.in_color_space) {
|
||||
case JCS_RGB:
|
||||
#ifdef JCS_EXTENSIONS
|
||||
case JCS_EXT_RGBX:
|
||||
#endif
|
||||
switch (context->subsampling) {
|
||||
case -1: /* Default */
|
||||
case 0: /* No subsampling */
|
||||
break;
|
||||
default:
|
||||
/* Would subsample the green and blue
|
||||
channels, which doesn't make sense */
|
||||
state->errcode = IMAGING_CODEC_CONFIG;
|
||||
return -1;
|
||||
}
|
||||
jpeg_set_colorspace(&context->cinfo, JCS_RGB);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Use custom quantization tables */
|
||||
if (context->qtables) {
|
||||
int i;
|
||||
|
|
Loading…
Reference in New Issue
Block a user