mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-07 22:04:46 +03:00
Merge branch 'python-pillow:main' into main
This commit is contained in:
commit
e7d4fa6486
|
@ -2,15 +2,14 @@
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
exclude_lines =
|
exclude_also =
|
||||||
# Have to re-enable the standard pragma:
|
# Don't complain if non-runnable code isn't run
|
||||||
pragma: no cover
|
|
||||||
|
|
||||||
# Don't complain if non-runnable code isn't run:
|
|
||||||
if 0:
|
if 0:
|
||||||
if __name__ == .__main__.:
|
if __name__ == .__main__.:
|
||||||
# Don't complain about debug code
|
# Don't complain about debug code
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
# Don't complain about compatibility code for missing optional dependencies
|
||||||
|
except ImportError
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
omit =
|
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:
|
env:
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
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
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
.ci/build.sh
|
.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_ARCHS: ${{ matrix.cibw_arch }}
|
||||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||||
CIBW_CACHE_PATH: "C:\\cibw"
|
CIBW_CACHE_PATH: "C:\\cibw"
|
||||||
|
CIBW_SKIP: pp38-*
|
||||||
CIBW_TEST_SKIP: "*-win_arm64"
|
CIBW_TEST_SKIP: "*-win_arm64"
|
||||||
CIBW_TEST_COMMAND: 'docker run --rm
|
CIBW_TEST_COMMAND: 'docker run --rm
|
||||||
-v {project}:C:\pillow
|
-v {project}:C:\pillow
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.1.7
|
rev: v0.1.9
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.12.0
|
rev: 23.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
|
|
29
CHANGES.rst
29
CHANGES.rst
|
@ -2,9 +2,36 @@
|
||||||
Changelog (Pillow)
|
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
|
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
||||||
|
|
||||||
Pillow is the friendly PIL fork. It 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:
|
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.:
|
and copy into `dist`. Check and upload them e.g.:
|
||||||
```bash
|
```bash
|
||||||
python3 -m twine check --strict dist/*
|
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
|
## 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 = "Tests/images/uncompressed_l.dds"
|
||||||
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
|
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
|
||||||
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.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"
|
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),
|
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
|
||||||
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
|
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
|
||||||
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
|
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
|
||||||
|
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
|
||||||
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
("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")
|
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_unsupported_bitcount():
|
||||||
"test_file",
|
|
||||||
(
|
|
||||||
"Tests/images/unsupported_bitcount_rgb.dds",
|
|
||||||
"Tests/images/unsupported_bitcount_luminance.dds",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_unsupported_bitcount(test_file):
|
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
with Image.open(test_file):
|
with Image.open("Tests/images/unsupported_bitcount.dds"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,23 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, IptcImagePlugin
|
from PIL import Image, IptcImagePlugin
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
TEST_FILE = "Tests/images/iptc.jpg"
|
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():
|
def test_getiptcinfo_jpg_none():
|
||||||
# Arrange
|
# Arrange
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
|
@ -78,24 +90,28 @@ def test_i():
|
||||||
c = b"a"
|
c = b"a"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
ret = IptcImagePlugin.i(c)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
ret = IptcImagePlugin.i(c)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert ret == 97
|
assert ret == 97
|
||||||
|
|
||||||
|
|
||||||
def test_dump():
|
def test_dump(monkeypatch):
|
||||||
# Arrange
|
# Arrange
|
||||||
c = b"abc"
|
c = b"abc"
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
old_stdout = sys.stdout
|
mystdout = StringIO()
|
||||||
sys.stdout = mystdout = StringIO()
|
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
IptcImagePlugin.dump(c)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
IptcImagePlugin.dump(c)
|
||||||
# Reset stdout
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert mystdout.getvalue() == "61 62 63 \n"
|
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
|
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(
|
@pytest.mark.parametrize(
|
||||||
"test_image_path",
|
"test_image_path",
|
||||||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
[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]
|
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
|
||||||
|
|
||||||
# experimental API
|
# experimental API
|
||||||
im = self.roundtrip(hopper(), subsampling=-1) # default
|
for subsampling in (-1, 3): # (default, invalid)
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||||
im = self.roundtrip(hopper(), subsampling=0) # 4:4:4
|
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
for subsampling in (0, "4:4:4"):
|
||||||
im = self.roundtrip(hopper(), subsampling=1) # 4:2:2
|
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||||
im = self.roundtrip(hopper(), subsampling=2) # 4:2:0
|
for subsampling in (1, "4:2:2"):
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||||
im = self.roundtrip(hopper(), subsampling=3) # default (undefined)
|
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||||
assert getsampling(im) == (2, 2, 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")
|
# RGB colorspace
|
||||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
for subsampling in (-1, 0, "4:4:4"):
|
||||||
im = self.roundtrip(hopper(), subsampling="4:2:2")
|
# "4:4:4" doesn't really make sense for RGB, but the conversion
|
||||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
# to an integer happens at a higher level
|
||||||
im = self.roundtrip(hopper(), subsampling="4:2:0")
|
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||||
im = self.roundtrip(hopper(), subsampling="4:1:1")
|
for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
with pytest.raises(OSError):
|
||||||
|
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
self.roundtrip(hopper(), subsampling="1:1:1")
|
self.roundtrip(hopper(), subsampling="1:1:1")
|
||||||
|
@ -840,6 +856,10 @@ class TestFileJpeg:
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert im._getexif()[306] == "2017:03:13 23:03:09"
|
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(
|
@mark_if_feature_version(
|
||||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -484,13 +484,13 @@ class TestFileTiff:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
exif[256] = 100
|
exif[264] = 100
|
||||||
|
|
||||||
im.save(outfile, exif=exif)
|
im.save(outfile, exif=exif)
|
||||||
|
|
||||||
with Image.open(outfile) as im:
|
with Image.open(outfile) as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif[256] == 100
|
assert exif[264] == 100
|
||||||
|
|
||||||
def test_reload_exif_after_seek(self):
|
def test_reload_exif_after_seek(self):
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
|
@ -612,6 +612,14 @@ class TestFileTiff:
|
||||||
|
|
||||||
assert_image_equal_tofile(im, tmpfile)
|
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):
|
def test_strip_raw(self):
|
||||||
infile = "Tests/images/tiff_strip_raw.tif"
|
infile = "Tests/images/tiff_strip_raw.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
|
@ -773,6 +781,27 @@ class TestFileTiff:
|
||||||
4001,
|
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):
|
def test_close_on_load_exclusive(self, tmp_path):
|
||||||
# similar to test_fd_leak, but runs on unixlike os
|
# similar to test_fd_leak, but runs on unixlike os
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = str(tmp_path / "temp.tif")
|
||||||
|
|
|
@ -123,6 +123,7 @@ def test_write_metadata(tmp_path):
|
||||||
"""Test metadata writing through the python code"""
|
"""Test metadata writing through the python code"""
|
||||||
with Image.open("Tests/images/hopper.tif") as img:
|
with Image.open("Tests/images/hopper.tif") as img:
|
||||||
f = str(tmp_path / "temp.tiff")
|
f = str(tmp_path / "temp.tiff")
|
||||||
|
del img.tag[278]
|
||||||
img.save(f, tiffinfo=img.tag)
|
img.save(f, tiffinfo=img.tag)
|
||||||
|
|
||||||
original = img.tag_v2.named()
|
original = img.tag_v2.named()
|
||||||
|
@ -159,9 +160,11 @@ def test_change_stripbytecounts_tag_type(tmp_path):
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = str(tmp_path / "temp.tiff")
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
info = im.tag_v2
|
info = im.tag_v2
|
||||||
|
del info[278]
|
||||||
|
|
||||||
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
|
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
|
||||||
im = im.resize((500, 500))
|
im = im.resize((500, 500))
|
||||||
|
info[TiffImagePlugin.IMAGEWIDTH] = im.width
|
||||||
|
|
||||||
# STRIPBYTECOUNTS can be a SHORT or a LONG
|
# STRIPBYTECOUNTS can be a SHORT or a LONG
|
||||||
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
|
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
|
||||||
|
|
|
@ -1016,6 +1016,11 @@ class TestImage:
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
assert str(e) == "buffer overrun when reading image file"
|
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):
|
def test_close_graceful(self, caplog):
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
copy = im.copy()
|
copy = im.copy()
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import struct
|
||||||
import pytest
|
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
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(
|
original_core = ImageFont.core
|
||||||
features.check_module("freetype2"),
|
|
||||||
reason="PILfont superseded if FreeType is supported",
|
|
||||||
)
|
def setup_module():
|
||||||
|
if features.check_module("freetype2"):
|
||||||
|
ImageFont.core = _util.DeferredError(ImportError)
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module():
|
||||||
|
ImageFont.core = original_core
|
||||||
|
|
||||||
|
|
||||||
def test_default_font():
|
def test_default_font():
|
||||||
|
@ -44,3 +52,25 @@ def test_textbbox():
|
||||||
default_font = ImageFont.load_default()
|
default_font = ImageFont.load_default()
|
||||||
assert d.textlength("test", font=default_font) == 24
|
assert d.textlength("test", font=default_font) == 24
|
||||||
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
|
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")
|
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(
|
@pytest.mark.parametrize(
|
||||||
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
||||||
)
|
)
|
||||||
|
|
|
@ -66,7 +66,7 @@ def test_deferred_error():
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
thing = _util.DeferredError(ValueError("Some error text"))
|
thing = _util.DeferredError.new(ValueError("Some error text"))
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
||||||
|
|
||||||
Pillow is the friendly PIL fork. It 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
|
Like PIL, Pillow is licensed under the open source PIL
|
||||||
Software License:
|
Software License:
|
||||||
|
|
|
@ -69,6 +69,14 @@ can be found here.
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
:mod:`~PIL.ImageMode` Module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: PIL.ImageMode
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
:mod:`~PIL.ImageTransform` Module
|
:mod:`~PIL.ImageTransform` Module
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ master_doc = "index"
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = "Pillow (PIL Fork)"
|
project = "Pillow (PIL Fork)"
|
||||||
copyright = (
|
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"
|
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
|
error codes returned by a codec's ``decode()`` method, which ImageFile already does
|
||||||
automatically.
|
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
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -487,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
**exif**
|
**exif**
|
||||||
If present, the image will be stored with the provided raw EXIF data.
|
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**
|
**subsampling**
|
||||||
If present, sets the subsampling for the encoder.
|
If present, sets the subsampling for the encoder.
|
||||||
|
|
||||||
|
|
|
@ -542,7 +542,7 @@ Reading from URL
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from urllib.request import urlopen
|
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))
|
img = Image.open(urlopen(url))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,19 @@ Internal Modules
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
: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
|
:mod:`~PIL._util` Module
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
10.2.0
|
10.2.0
|
||||||
------
|
------
|
||||||
|
|
||||||
Backwards Incompatible Changes
|
|
||||||
==============================
|
|
||||||
|
|
||||||
TODO
|
|
||||||
^^^^
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
============
|
============
|
||||||
|
|
||||||
|
@ -20,10 +12,14 @@ ImageFile.raise_oserror
|
||||||
error codes returned by a codec's ``decode()`` method, which ImageFile already does
|
error codes returned by a codec's ``decode()`` method, which ImageFile already does
|
||||||
automatically.
|
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
|
API Changes
|
||||||
===========
|
===========
|
||||||
|
@ -46,6 +42,14 @@ Added DdsImagePlugin enums
|
||||||
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
|
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
|
||||||
enums have been added to :py:class:`PIL.DdsImagePlugin`.
|
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
|
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
|
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``.
|
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
|
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
|
Support has also been added to read DX10 BC1 and BC4, whether UNORM or
|
||||||
TYPELESS.
|
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
|
Optimized ImageStat.Stat count and extrema
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
|
Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
|
||||||
:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
|
: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
|
histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on
|
||||||
on average and ``st.extrema`` is 12x as fast on average.
|
average and ``st.extrema`` is 12 times as fast on average.
|
||||||
|
|
||||||
Encoder errors now report error detail as string
|
Encoder errors now report error detail as string
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
:py:exc:`OSError` exceptions from image encoders now include a textual description of
|
:py:exc:`OSError` exceptions from image encoders now include a textual description of
|
||||||
the error instead of a numeric error code.
|
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-cov",
|
||||||
"pytest-timeout",
|
"pytest-timeout",
|
||||||
]
|
]
|
||||||
|
typing = [
|
||||||
|
'typing-extensions; python_version < "3.10"',
|
||||||
|
]
|
||||||
xmp = [
|
xmp = [
|
||||||
"defusedxml",
|
"defusedxml",
|
||||||
]
|
]
|
||||||
|
@ -143,9 +146,6 @@ exclude = [
|
||||||
'^src/PIL/DdsImagePlugin.py$',
|
'^src/PIL/DdsImagePlugin.py$',
|
||||||
'^src/PIL/FpxImagePlugin.py$',
|
'^src/PIL/FpxImagePlugin.py$',
|
||||||
'^src/PIL/Image.py$',
|
'^src/PIL/Image.py$',
|
||||||
'^src/PIL/ImageCms.py$',
|
|
||||||
'^src/PIL/ImageFile.py$',
|
|
||||||
'^src/PIL/ImageFont.py$',
|
|
||||||
'^src/PIL/ImageMath.py$',
|
'^src/PIL/ImageMath.py$',
|
||||||
'^src/PIL/ImageMorph.py$',
|
'^src/PIL/ImageMorph.py$',
|
||||||
'^src/PIL/ImageQt.py$',
|
'^src/PIL/ImageQt.py$',
|
||||||
|
|
|
@ -18,6 +18,7 @@ from enum import IntEnum, IntFlag
|
||||||
|
|
||||||
from . import Image, ImageFile, ImagePalette
|
from . import Image, ImageFile, ImagePalette
|
||||||
from ._binary import i32le as i32
|
from ._binary import i32le as i32
|
||||||
|
from ._binary import o8
|
||||||
from ._binary import o32le as o32
|
from ._binary import o32le as o32
|
||||||
|
|
||||||
# Magic ("DDS ")
|
# Magic ("DDS ")
|
||||||
|
@ -341,6 +342,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||||
self._size = (width, height)
|
self._size = (width, height)
|
||||||
|
extents = (0, 0) + self.size
|
||||||
|
|
||||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||||
struct.unpack("<11I", header.read(44)) # reserved
|
struct.unpack("<11I", header.read(44)) # reserved
|
||||||
|
@ -351,22 +353,16 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
rawmode = None
|
rawmode = None
|
||||||
if pfflags & DDPF.RGB:
|
if pfflags & DDPF.RGB:
|
||||||
# Texture contains uncompressed RGB data
|
# Texture contains uncompressed RGB data
|
||||||
masks = struct.unpack("<4I", header.read(16))
|
if pfflags & DDPF.ALPHAPIXELS:
|
||||||
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:
|
|
||||||
self._mode = "RGBA"
|
self._mode = "RGBA"
|
||||||
rawmode = (
|
mask_count = 4
|
||||||
masks[0x000000FF]
|
|
||||||
+ masks[0x0000FF00]
|
|
||||||
+ masks[0x00FF0000]
|
|
||||||
+ masks[0xFF000000]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
|
self._mode = "RGB"
|
||||||
raise OSError(msg)
|
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:
|
elif pfflags & DDPF.LUMINANCE:
|
||||||
if bitcount == 8:
|
if bitcount == 8:
|
||||||
self._mode = "L"
|
self._mode = "L"
|
||||||
|
@ -464,7 +460,6 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
msg = f"Unknown pixel format flags {pfflags}"
|
msg = f"Unknown pixel format flags {pfflags}"
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
extents = (0, 0) + self.size
|
|
||||||
if n:
|
if n:
|
||||||
self.tile = [
|
self.tile = [
|
||||||
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
|
||||||
|
@ -476,6 +471,39 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
pass
|
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):
|
def _save(im, fp, filename):
|
||||||
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
if im.mode not in ("RGB", "RGBA", "L", "LA"):
|
||||||
msg = f"cannot write mode {im.mode} as DDS"
|
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_open(DdsImageFile.format, DdsImageFile, _accept)
|
||||||
|
Image.register_decoder("dds_rgb", DdsRgbDecoder)
|
||||||
Image.register_save(DdsImageFile.format, _save)
|
Image.register_save(DdsImageFile.format, _save)
|
||||||
Image.register_extension(DdsImageFile.format, ".dds")
|
Image.register_extension(DdsImageFile.format, ".dds")
|
||||||
|
|
|
@ -92,7 +92,7 @@ try:
|
||||||
raise ImportError(msg)
|
raise ImportError(msg)
|
||||||
|
|
||||||
except ImportError as v:
|
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
|
# Explanations for ways that we know we might have an import error
|
||||||
if str(v).startswith("Module use of python"):
|
if str(v).startswith("Module use of python"):
|
||||||
# The _imaging C module is present, but not compiled for
|
# The _imaging C module is present, but not compiled for
|
||||||
|
@ -530,15 +530,19 @@ class Image:
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return 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):
|
def __exit__(self, *args):
|
||||||
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
|
if hasattr(self, "fp"):
|
||||||
if getattr(self, "_fp", False):
|
if getattr(self, "_exclusive_fp", False):
|
||||||
if self._fp != self.fp:
|
self._close_fp()
|
||||||
self._fp.close()
|
self.fp = None
|
||||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
|
||||||
if self.fp:
|
|
||||||
self.fp.close()
|
|
||||||
self.fp = None
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""
|
"""
|
||||||
|
@ -554,12 +558,7 @@ class Image:
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "fp"):
|
if hasattr(self, "fp"):
|
||||||
try:
|
try:
|
||||||
if getattr(self, "_fp", False):
|
self._close_fp()
|
||||||
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
|
self.fp = None
|
||||||
except Exception as msg:
|
except Exception as msg:
|
||||||
logger.debug("Error closing: %s", msg)
|
logger.debug("Error closing: %s", msg)
|
||||||
|
@ -2644,7 +2643,7 @@ class Image:
|
||||||
resample=Resampling.NEAREST,
|
resample=Resampling.NEAREST,
|
||||||
fill=1,
|
fill=1,
|
||||||
fillcolor=None,
|
fillcolor=None,
|
||||||
):
|
) -> Image:
|
||||||
"""
|
"""
|
||||||
Transforms this image. This method creates a new image with the
|
Transforms this image. This method creates a new image with the
|
||||||
given size, and the same mode as the original, and copies data
|
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.
|
Opens and identifies the given image file.
|
||||||
|
|
||||||
|
@ -3416,7 +3415,7 @@ def merge(mode, bands):
|
||||||
# Plugin registry
|
# 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
|
Register an image file plugin. This function should not be used
|
||||||
in application code.
|
in application code.
|
||||||
|
@ -3470,7 +3469,7 @@ def register_save_all(id, driver):
|
||||||
SAVE_ALL[id.upper()] = 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
|
Registers an image extension. This function should not be
|
||||||
used in application code.
|
used in application code.
|
||||||
|
|
|
@ -28,7 +28,7 @@ except ImportError as ex:
|
||||||
# anything in core.
|
# anything in core.
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
_imagingcms = DeferredError(ex)
|
_imagingcms = DeferredError.new(ex)
|
||||||
|
|
||||||
DESCRIPTION = """
|
DESCRIPTION = """
|
||||||
pyCMS
|
pyCMS
|
||||||
|
|
|
@ -32,7 +32,7 @@ import io
|
||||||
import itertools
|
import itertools
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
from typing import NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
from ._deprecate import deprecate
|
from ._deprecate import deprecate
|
||||||
|
@ -94,7 +94,7 @@ class _Tile(NamedTuple):
|
||||||
encoder_name: str
|
encoder_name: str
|
||||||
extents: tuple[int, int, int, int]
|
extents: tuple[int, int, int, int]
|
||||||
offset: int
|
offset: int
|
||||||
args: tuple | str | None
|
args: tuple[Any, ...] | str | None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter):
|
||||||
if hasattr(table, "shape"):
|
if hasattr(table, "shape"):
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
except ImportError: # pragma: no cover
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if numpy and isinstance(table, numpy.ndarray):
|
if numpy and isinstance(table, numpy.ndarray):
|
||||||
|
|
|
@ -34,7 +34,7 @@ import warnings
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import BinaryIO
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
from ._util import is_directory, is_path
|
from ._util import is_directory, is_path
|
||||||
|
@ -53,7 +53,7 @@ try:
|
||||||
except ImportError as ex:
|
except ImportError as ex:
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
core = DeferredError(ex)
|
core = DeferredError.new(ex)
|
||||||
|
|
||||||
|
|
||||||
def _string_length_check(text):
|
def _string_length_check(text):
|
||||||
|
@ -150,6 +150,7 @@ class ImageFont:
|
||||||
:py:mod:`PIL.Image.core` interface module.
|
:py:mod:`PIL.Image.core` interface module.
|
||||||
"""
|
"""
|
||||||
_string_length_check(text)
|
_string_length_check(text)
|
||||||
|
Image._decompression_bomb_check(self.font.getsize(text))
|
||||||
return self.font.getmask(text, mode)
|
return self.font.getmask(text, mode)
|
||||||
|
|
||||||
def getbbox(self, text, *args, **kwargs):
|
def getbbox(self, text, *args, **kwargs):
|
||||||
|
@ -192,7 +193,7 @@ class FreeTypeFont:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
font: bytes | str | Path | IO | None = None,
|
font: bytes | str | Path | BinaryIO | None = None,
|
||||||
size: float = 10,
|
size: float = 10,
|
||||||
index: int = 0,
|
index: int = 0,
|
||||||
encoding: str = "",
|
encoding: str = "",
|
||||||
|
|
|
@ -14,17 +14,26 @@
|
||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
class Transform(Image.ImageTransformHandler):
|
class Transform(Image.ImageTransformHandler):
|
||||||
def __init__(self, data):
|
method: Image.Transform
|
||||||
|
|
||||||
|
def __init__(self, data: Sequence[int]) -> None:
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def getdata(self):
|
def getdata(self) -> tuple[int, Sequence[int]]:
|
||||||
return self.method, self.data
|
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
|
# can be overridden
|
||||||
method, data = self.getdata()
|
method, data = self.getdata()
|
||||||
return image.transform(size, method, data, **options)
|
return image.transform(size, method, data, **options)
|
||||||
|
|
|
@ -16,31 +16,46 @@
|
||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
from io import BytesIO
|
||||||
import tempfile
|
from typing import Sequence
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
from ._binary import i32be as i32
|
from ._binary import i32be as i32
|
||||||
|
from ._deprecate import deprecate
|
||||||
|
|
||||||
COMPRESSION = {1: "raw", 5: "jpeg"}
|
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
|
# Helpers
|
||||||
|
|
||||||
|
|
||||||
|
def _i(c: bytes) -> int:
|
||||||
|
return i32((b"\0\0\0\0" + c)[-4:])
|
||||||
|
|
||||||
|
|
||||||
def _i8(c: int | bytes) -> int:
|
def _i8(c: int | bytes) -> int:
|
||||||
return c if isinstance(c, int) else c[0]
|
return c if isinstance(c, int) else c[0]
|
||||||
|
|
||||||
|
|
||||||
def i(c):
|
def i(c: bytes) -> int:
|
||||||
return i32((PAD + c)[-4:])
|
""".. 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:
|
for i in c:
|
||||||
print("%02x" % _i8(i), end=" ")
|
print("%02x" % _i8(i), end=" ")
|
||||||
print()
|
print()
|
||||||
|
@ -55,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
format = "IPTC"
|
format = "IPTC"
|
||||||
format_description = "IPTC/NAA"
|
format_description = "IPTC/NAA"
|
||||||
|
|
||||||
def getint(self, key):
|
def getint(self, key: tuple[int, int]) -> int:
|
||||||
return i(self.info[key])
|
return _i(self.info[key])
|
||||||
|
|
||||||
def field(self):
|
def field(self) -> tuple[tuple[int, int] | None, int]:
|
||||||
#
|
#
|
||||||
# get a IPTC field header
|
# get a IPTC field header
|
||||||
s = self.fp.read(5)
|
s = self.fp.read(5)
|
||||||
|
@ -80,13 +95,13 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
elif size == 128:
|
elif size == 128:
|
||||||
size = 0
|
size = 0
|
||||||
elif size > 128:
|
elif size > 128:
|
||||||
size = i(self.fp.read(size - 128))
|
size = _i(self.fp.read(size - 128))
|
||||||
else:
|
else:
|
||||||
size = i16(s, 3)
|
size = i16(s, 3)
|
||||||
|
|
||||||
return tag, size
|
return tag, size
|
||||||
|
|
||||||
def _open(self):
|
def _open(self) -> None:
|
||||||
# load descriptive fields
|
# load descriptive fields
|
||||||
while True:
|
while True:
|
||||||
offset = self.fp.tell()
|
offset = self.fp.tell()
|
||||||
|
@ -131,27 +146,22 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
# tile
|
# tile
|
||||||
if tag == (8, 10):
|
if tag == (8, 10):
|
||||||
self.tile = [
|
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
|
||||||
("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
|
|
||||||
]
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
||||||
return ImageFile.ImageFile.load(self)
|
return ImageFile.ImageFile.load(self)
|
||||||
|
|
||||||
type, tile, box = self.tile[0]
|
offset, compression = self.tile[0][2:]
|
||||||
|
|
||||||
encoding, offset = tile
|
|
||||||
|
|
||||||
self.fp.seek(offset)
|
self.fp.seek(offset)
|
||||||
|
|
||||||
# Copy image data to temporary file
|
# Copy image data to temporary file
|
||||||
o_fd, outfile = tempfile.mkstemp(text=False)
|
o = BytesIO()
|
||||||
o = os.fdopen(o_fd)
|
if compression == "raw":
|
||||||
if encoding == "raw":
|
|
||||||
# To simplify access to the extracted file,
|
# To simplify access to the extracted file,
|
||||||
# prepend a PPM header
|
# 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:
|
while True:
|
||||||
type, size = self.field()
|
type, size = self.field()
|
||||||
if type != (8, 10):
|
if type != (8, 10):
|
||||||
|
@ -162,17 +172,10 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
break
|
break
|
||||||
o.write(s)
|
o.write(s)
|
||||||
size -= len(s)
|
size -= len(s)
|
||||||
o.close()
|
|
||||||
|
|
||||||
try:
|
with Image.open(o) as _im:
|
||||||
with Image.open(outfile) as _im:
|
_im.load()
|
||||||
_im.load()
|
self.im = _im.im
|
||||||
self.im = _im.im
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
os.unlink(outfile)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
Image.register_open(IptcImageFile.format, IptcImageFile)
|
Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||||
|
@ -188,8 +191,6 @@ def getiptcinfo(im):
|
||||||
:returns: A dictionary containing IPTC information, or None if
|
:returns: A dictionary containing IPTC information, or None if
|
||||||
no IPTC information block was found.
|
no IPTC information block was found.
|
||||||
"""
|
"""
|
||||||
import io
|
|
||||||
|
|
||||||
from . import JpegImagePlugin, TiffImagePlugin
|
from . import JpegImagePlugin, TiffImagePlugin
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
|
@ -224,7 +225,7 @@ def getiptcinfo(im):
|
||||||
|
|
||||||
# parse the IPTC information chunk
|
# parse the IPTC information chunk
|
||||||
im.info = {}
|
im.info = {}
|
||||||
im.fp = io.BytesIO(data)
|
im.fp = BytesIO(data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
im._open()
|
im._open()
|
||||||
|
|
|
@ -87,10 +87,12 @@ def APP(self, marker):
|
||||||
self.info["dpi"] = jfif_density
|
self.info["dpi"] = jfif_density
|
||||||
self.info["jfif_unit"] = jfif_unit
|
self.info["jfif_unit"] = jfif_unit
|
||||||
self.info["jfif_density"] = jfif_density
|
self.info["jfif_density"] = jfif_density
|
||||||
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
|
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
|
||||||
if "exif" not in self.info:
|
# extract EXIF information
|
||||||
# extract EXIF information (incomplete)
|
if "exif" in self.info:
|
||||||
self.info["exif"] = s # FIXME: value will change
|
self.info["exif"] += s[6:]
|
||||||
|
else:
|
||||||
|
self.info["exif"] = s
|
||||||
self._exif_offset = self.fp.tell() - n + 6
|
self._exif_offset = self.fp.tell() - n + 6
|
||||||
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
||||||
# extract FlashPix information (incomplete)
|
# extract FlashPix information (incomplete)
|
||||||
|
@ -783,6 +785,7 @@ def _save(im, fp, filename):
|
||||||
progressive,
|
progressive,
|
||||||
info.get("smooth", 0),
|
info.get("smooth", 0),
|
||||||
optimize,
|
optimize,
|
||||||
|
info.get("keep_rgb", False),
|
||||||
info.get("streamtype", 0),
|
info.get("streamtype", 0),
|
||||||
dpi[0],
|
dpi[0],
|
||||||
dpi[1],
|
dpi[1],
|
||||||
|
|
|
@ -43,7 +43,7 @@ except ImportError as ex:
|
||||||
# anything in core.
|
# anything in core.
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
FFI = ffi = DeferredError(ex)
|
FFI = ffi = DeferredError.new(ex)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -1704,25 +1704,27 @@ def _save(im, fp, filename):
|
||||||
colormap += [0] * (256 - colors)
|
colormap += [0] * (256 - colors)
|
||||||
ifd[COLORMAP] = colormap
|
ifd[COLORMAP] = colormap
|
||||||
# data orientation
|
# data orientation
|
||||||
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
|
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
|
||||||
# aim for given strip size (64 KB by default) when using libtiff writer
|
stride = len(bits) * ((w * bits[0] + 7) // 8)
|
||||||
if libtiff:
|
if ROWSPERSTRIP not in ifd:
|
||||||
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
|
# aim for given strip size (64 KB by default) when using libtiff writer
|
||||||
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
|
if libtiff:
|
||||||
# JPEG encoder expects multiple of 8 rows
|
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
|
||||||
if compression == "jpeg":
|
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
|
||||||
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
|
# JPEG encoder expects multiple of 8 rows
|
||||||
else:
|
if compression == "jpeg":
|
||||||
rows_per_strip = im.size[1]
|
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
|
||||||
if rows_per_strip == 0:
|
else:
|
||||||
rows_per_strip = 1
|
rows_per_strip = h
|
||||||
strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
|
if rows_per_strip == 0:
|
||||||
strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
|
rows_per_strip = 1
|
||||||
ifd[ROWSPERSTRIP] = rows_per_strip
|
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:
|
if strip_byte_counts >= 2**16:
|
||||||
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
|
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
|
||||||
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
|
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(
|
ifd[STRIPOFFSETS] = tuple(
|
||||||
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
|
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
|
import os
|
||||||
from pathlib import Path
|
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))
|
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."""
|
"""Checks if an object is a string, and that it points to a directory."""
|
||||||
return is_path(f) and os.path.isdir(f)
|
return is_path(f) and os.path.isdir(f)
|
||||||
|
|
||||||
|
|
||||||
class DeferredError:
|
class DeferredError:
|
||||||
def __init__(self, ex):
|
def __init__(self, ex: BaseException):
|
||||||
self.ex = ex
|
self.ex = ex
|
||||||
|
|
||||||
def __getattr__(self, elt):
|
def __getattr__(self, elt: str) -> NoReturn:
|
||||||
raise self.ex
|
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
|
# Master version for Pillow
|
||||||
from __future__ import annotations
|
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].sy0 = S16(B16(glyphdata, 14));
|
||||||
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
|
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
|
||||||
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
|
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) {
|
if (self->glyphs[i].dy0 < y0) {
|
||||||
y0 = self->glyphs[i].dy0;
|
y0 = self->glyphs[i].dy0;
|
||||||
}
|
}
|
||||||
|
@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_font_getmask(ImagingFontObject *self, PyObject *args) {
|
_font_getmask(ImagingFontObject *self, PyObject *args) {
|
||||||
Imaging im;
|
Imaging im;
|
||||||
Imaging bitmap;
|
Imaging bitmap = NULL;
|
||||||
int x, b;
|
int x, b;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int status;
|
int status;
|
||||||
|
@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
||||||
PyObject *encoded_string;
|
PyObject *encoded_string;
|
||||||
|
|
||||||
unsigned char *text;
|
unsigned char *text;
|
||||||
char *mode = "";
|
char *mode;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
|
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
||||||
b = self->baseline;
|
b = self->baseline;
|
||||||
for (x = 0; text[i]; i++) {
|
for (x = 0; text[i]; i++) {
|
||||||
glyph = &self->glyphs[text[i]];
|
glyph = &self->glyphs[text[i]];
|
||||||
bitmap =
|
if (i == 0 || text[i] != text[i - 1]) {
|
||||||
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
|
ImagingDelete(bitmap);
|
||||||
if (!bitmap) {
|
bitmap =
|
||||||
goto failed;
|
ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
|
||||||
|
if (!bitmap) {
|
||||||
|
goto failed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
status = ImagingPaste(
|
status = ImagingPaste(
|
||||||
im,
|
im,
|
||||||
|
@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
|
||||||
glyph->dy0 + b,
|
glyph->dy0 + b,
|
||||||
glyph->dx1 + x,
|
glyph->dx1 + x,
|
||||||
glyph->dy1 + b);
|
glyph->dy1 + b);
|
||||||
ImagingDelete(bitmap);
|
|
||||||
if (status < 0) {
|
if (status < 0) {
|
||||||
goto failed;
|
goto failed;
|
||||||
}
|
}
|
||||||
x = x + glyph->dx;
|
x = x + glyph->dx;
|
||||||
b = b + glyph->dy;
|
b = b + glyph->dy;
|
||||||
}
|
}
|
||||||
|
ImagingDelete(bitmap);
|
||||||
free(text);
|
free(text);
|
||||||
return PyImagingNew(im);
|
return PyImagingNew(im);
|
||||||
|
|
||||||
failed:
|
failed:
|
||||||
|
ImagingDelete(bitmap);
|
||||||
free(text);
|
free(text);
|
||||||
ImagingDelete(im);
|
ImagingDelete(im);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|
|
@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
Py_ssize_t progressive = 0;
|
Py_ssize_t progressive = 0;
|
||||||
Py_ssize_t smooth = 0;
|
Py_ssize_t smooth = 0;
|
||||||
Py_ssize_t optimize = 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 streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
|
||||||
Py_ssize_t xdpi = 0, ydpi = 0;
|
Py_ssize_t xdpi = 0, ydpi = 0;
|
||||||
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
|
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(
|
if (!PyArg_ParseTuple(
|
||||||
args,
|
args,
|
||||||
"ss|nnnnnnnnnnOz#y#y#",
|
"ss|nnnnpnnnnnnOz#y#y#",
|
||||||
&mode,
|
&mode,
|
||||||
&rawmode,
|
&rawmode,
|
||||||
&quality,
|
&quality,
|
||||||
&progressive,
|
&progressive,
|
||||||
&smooth,
|
&smooth,
|
||||||
&optimize,
|
&optimize,
|
||||||
|
&keep_rgb,
|
||||||
&streamtype,
|
&streamtype,
|
||||||
&xdpi,
|
&xdpi,
|
||||||
&ydpi,
|
&ydpi,
|
||||||
|
@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
|
|
||||||
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
|
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)->quality = quality;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
|
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
|
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
|
||||||
|
|
|
@ -74,6 +74,9 @@ typedef struct {
|
||||||
/* Optimize Huffman tables (slow) */
|
/* Optimize Huffman tables (slow) */
|
||||||
int optimize;
|
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) */
|
/* Stream type (0=full, 1=tables only, 2=image only) */
|
||||||
int streamtype;
|
int streamtype;
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
||||||
/* Compressor configuration */
|
/* Compressor configuration */
|
||||||
jpeg_set_defaults(&context->cinfo);
|
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 */
|
/* Use custom quantization tables */
|
||||||
if (context->qtables) {
|
if (context->qtables) {
|
||||||
int i;
|
int i;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user