mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-11 16:52:29 +03:00
Merge branch 'main' into imagecms-typing
This commit is contained in:
commit
7a8417c683
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.23.3
|
cibuildwheel==3.0.0
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mypy==1.15.0
|
mypy==1.16.1
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
IceSpringPySideStubs-PySide6
|
IceSpringPySideStubs-PySide6
|
||||||
ipython
|
ipython
|
||||||
|
|
2
.github/workflows/test-windows.yml
vendored
2
.github/workflows/test-windows.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
|
||||||
architecture: ["x64"]
|
architecture: ["x64"]
|
||||||
include:
|
include:
|
||||||
# Test the oldest Python on 32-bit
|
# Test the oldest Python on 32-bit
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -43,6 +43,7 @@ jobs:
|
||||||
python-version: [
|
python-version: [
|
||||||
"pypy3.11",
|
"pypy3.11",
|
||||||
"pypy3.10",
|
"pypy3.10",
|
||||||
|
"3.14t",
|
||||||
"3.14",
|
"3.14",
|
||||||
"3.13t",
|
"3.13t",
|
||||||
"3.13",
|
"3.13",
|
||||||
|
@ -55,6 +56,7 @@ jobs:
|
||||||
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||||
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
|
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
|
||||||
# Free-threaded
|
# Free-threaded
|
||||||
|
- { python-version: "3.14t", disable-gil: true }
|
||||||
- { python-version: "3.13t", disable-gil: true }
|
- { python-version: "3.13t", disable-gil: true }
|
||||||
# M1 only available for 3.10+
|
# M1 only available for 3.10+
|
||||||
- { os: "macos-13", python-version: "3.9" }
|
- { os: "macos-13", python-version: "3.9" }
|
||||||
|
|
4
.github/workflows/wheels-dependencies.sh
vendored
4
.github/workflows/wheels-dependencies.sh
vendored
|
@ -39,8 +39,8 @@ ARCHIVE_SDIR=pillow-depends-main
|
||||||
# Package versions for fresh source builds
|
# Package versions for fresh source builds
|
||||||
FREETYPE_VERSION=2.13.3
|
FREETYPE_VERSION=2.13.3
|
||||||
HARFBUZZ_VERSION=11.2.1
|
HARFBUZZ_VERSION=11.2.1
|
||||||
LIBPNG_VERSION=1.6.48
|
LIBPNG_VERSION=1.6.49
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.1
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.8.1
|
XZ_VERSION=5.8.1
|
||||||
TIFF_VERSION=4.7.0
|
TIFF_VERSION=4.7.0
|
||||||
|
|
16
.github/workflows/wheels-test.ps1
vendored
16
.github/workflows/wheels-test.ps1
vendored
|
@ -9,17 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
|
||||||
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
|
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
|
||||||
}
|
}
|
||||||
$env:path += ";$pillow\winbuild\build\bin\"
|
$env:path += ";$pillow\winbuild\build\bin\"
|
||||||
& "$venv\Scripts\activate.ps1"
|
if (Test-Path $venv\Scripts\pypy.exe) {
|
||||||
|
$python = "pypy.exe"
|
||||||
|
} else {
|
||||||
|
$python = "python.exe"
|
||||||
|
}
|
||||||
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
||||||
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
|
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
|
||||||
& python -m pip install numpy
|
& $venv\Scripts\$python -m pip install numpy
|
||||||
}
|
}
|
||||||
cd $pillow
|
cd $pillow
|
||||||
& python -VV
|
& $venv\Scripts\$python -VV
|
||||||
if (!$?) { exit $LASTEXITCODE }
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
& python selftest.py
|
& $venv\Scripts\$python selftest.py
|
||||||
if (!$?) { exit $LASTEXITCODE }
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
& python -m pytest -vx Tests\check_wheel.py
|
& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py
|
||||||
if (!$?) { exit $LASTEXITCODE }
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
& python -m pytest -vx Tests
|
& $venv\Scripts\$python -m pytest -vx Tests
|
||||||
if (!$?) { exit $LASTEXITCODE }
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
|
|
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
|
@ -110,7 +110,6 @@ jobs:
|
||||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_SKIP: pp39-*
|
|
||||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
@ -188,7 +187,6 @@ jobs:
|
||||||
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_ENABLE: cpython-prerelease cpython-freethreading pypy
|
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||||
CIBW_SKIP: pp39-*
|
|
||||||
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
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .helper import is_pypy
|
||||||
def test_wheel_modules() -> None:
|
def test_wheel_modules() -> None:
|
||||||
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
# tkinter is not available in cibuildwheel installed CPython on Windows
|
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||||
try:
|
try:
|
||||||
import tkinter
|
import tkinter
|
||||||
|
|
Binary file not shown.
BIN
Tests/images/op_index.qoi
Normal file
BIN
Tests/images/op_index.qoi
Normal file
Binary file not shown.
BIN
Tests/images/p_4_planes.pcx
Normal file
BIN
Tests/images/p_4_planes.pcx
Normal file
Binary file not shown.
|
@ -47,7 +47,6 @@ def test_unknown_version() -> None:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||||
expected = r""
|
|
||||||
with pytest.raises(RuntimeError, match=expected):
|
with pytest.raises(RuntimeError, match=expected):
|
||||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,8 @@ import pytest
|
||||||
from PIL import BlpImagePlugin, Image
|
from PIL import BlpImagePlugin, Image
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
|
||||||
assert_image_equal_tofile,
|
assert_image_equal_tofile,
|
||||||
assert_image_similar,
|
assert_image_similar_tofile,
|
||||||
hopper,
|
hopper,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,18 +51,16 @@ def test_save(tmp_path: Path) -> None:
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
im.save(f, blp_version=version)
|
im.save(f, blp_version=version)
|
||||||
|
|
||||||
with Image.open(f) as reloaded:
|
assert_image_equal_tofile(im.convert("RGB"), f)
|
||||||
assert_image_equal(im.convert("RGB"), reloaded)
|
|
||||||
|
|
||||||
with Image.open("Tests/images/transparent.png") as im:
|
with Image.open("Tests/images/transparent.png") as im:
|
||||||
f = tmp_path / "temp.blp"
|
f = tmp_path / "temp.blp"
|
||||||
im.convert("P").save(f, blp_version=version)
|
im.convert("P").save(f, blp_version=version)
|
||||||
|
|
||||||
with Image.open(f) as reloaded:
|
assert_image_similar_tofile(im, f, 8)
|
||||||
assert_image_similar(im, reloaded, 8)
|
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError, match="Unsupported BLP image mode"):
|
||||||
im.save(f)
|
im.save(f)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -145,14 +145,16 @@ class TestFileJpeg:
|
||||||
assert k > 0.9
|
assert k > 0.9
|
||||||
# roundtrip, and check again
|
# roundtrip, and check again
|
||||||
im = self.roundtrip(im)
|
im = self.roundtrip(im)
|
||||||
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
|
cmyk = im.getpixel((0, 0))
|
||||||
|
assert isinstance(cmyk, tuple)
|
||||||
|
c, m, y, k = (x / 255.0 for x in cmyk)
|
||||||
assert c == 0.0
|
assert c == 0.0
|
||||||
assert m > 0.8
|
assert m > 0.8
|
||||||
assert y > 0.8
|
assert y > 0.8
|
||||||
assert k == 0.0
|
assert k == 0.0
|
||||||
c, m, y, k = (
|
cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
|
||||||
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
|
assert isinstance(cmyk, tuple)
|
||||||
)
|
k = cmyk[3] / 255.0
|
||||||
assert k > 0.9
|
assert k > 0.9
|
||||||
|
|
||||||
def test_rgb(self) -> None:
|
def test_rgb(self) -> None:
|
||||||
|
|
|
@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
im.save(f)
|
im.save(f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_p_4_planes() -> None:
|
||||||
|
with Image.open("Tests/images/p_4_planes.pcx") as im:
|
||||||
|
assert im.getpixel((0, 0)) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_bad_image_size() -> None:
|
def test_bad_image_size() -> None:
|
||||||
with open("Tests/images/pil184.pcx", "rb") as fp:
|
with open("Tests/images/pil184.pcx", "rb") as fp:
|
||||||
data = fp.read()
|
data = fp.read()
|
||||||
|
|
|
@ -100,11 +100,11 @@ class TestFilePng:
|
||||||
assert im.format == "PNG"
|
assert im.format == "PNG"
|
||||||
assert im.get_format_mimetype() == "image/png"
|
assert im.get_format_mimetype() == "image/png"
|
||||||
|
|
||||||
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
|
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
if mode in ("I", "I;16B"):
|
if mode == "I;16B":
|
||||||
reloaded = reloaded.convert(mode)
|
reloaded = reloaded.convert(mode)
|
||||||
assert_image_equal(reloaded, im)
|
assert_image_equal(reloaded, im)
|
||||||
|
|
||||||
|
@ -801,6 +801,16 @@ class TestFilePng:
|
||||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
|
||||||
|
def test_deprecation(self, tmp_path: Path) -> None:
|
||||||
|
test_file = tmp_path / "out.png"
|
||||||
|
|
||||||
|
im = hopper("I")
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
im.save(test_file)
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert_image_equal(im, reloaded.convert("I"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
|
|
|
@ -288,14 +288,16 @@ def test_non_integer_token(tmp_path: Path) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_header_token_too_long(tmp_path: Path) -> None:
|
@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890"))
|
||||||
|
def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
|
||||||
path = tmp_path / "temp.ppm"
|
path = tmp_path / "temp.ppm"
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6\n 01234567890")
|
f.write(data)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
|
with pytest.raises(ValueError) as e:
|
||||||
with Image.open(path):
|
with Image.open(path):
|
||||||
pass
|
pass
|
||||||
|
assert "Token too long in file header: " in repr(e)
|
||||||
|
|
||||||
|
|
||||||
def test_truncated_file(tmp_path: Path) -> None:
|
def test_truncated_file(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, QoiImagePlugin
|
from PIL import Image, QoiImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
|
@ -28,3 +30,28 @@ def test_invalid_file() -> None:
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
QoiImagePlugin.QoiImageFile(invalid_file)
|
QoiImagePlugin.QoiImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_op_index() -> None:
|
||||||
|
# QOI_OP_INDEX as the first chunk
|
||||||
|
with Image.open("Tests/images/op_index.qoi") as im:
|
||||||
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save(tmp_path: Path) -> None:
|
||||||
|
f = tmp_path / "temp.qoi"
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
im.save(f, colorspace="sRGB")
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, f)
|
||||||
|
|
||||||
|
for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"):
|
||||||
|
with Image.open(path) as im:
|
||||||
|
im.save(f)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, f)
|
||||||
|
|
||||||
|
im = hopper("P")
|
||||||
|
with pytest.raises(ValueError, match="Unsupported QOI image mode"):
|
||||||
|
im.save(f)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from PIL import (
|
||||||
ImageFile,
|
ImageFile,
|
||||||
JpegImagePlugin,
|
JpegImagePlugin,
|
||||||
TiffImagePlugin,
|
TiffImagePlugin,
|
||||||
|
TiffTags,
|
||||||
UnidentifiedImageError,
|
UnidentifiedImageError,
|
||||||
)
|
)
|
||||||
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
||||||
|
@ -900,6 +901,29 @@ class TestFileTiff:
|
||||||
assert description[0]["format"] == "image/tiff"
|
assert description[0]["format"] == "image/tiff"
|
||||||
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
|
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
|
||||||
|
|
||||||
|
def test_getxmp_undefined(self, tmp_path: Path) -> None:
|
||||||
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
ifd.tagtype[700] = TiffTags.UNDEFINED
|
||||||
|
with Image.open("Tests/images/lab.tif") as im_xmp:
|
||||||
|
ifd[700] = im_xmp.info["xmp"]
|
||||||
|
im.save(tmpfile, tiffinfo=ifd)
|
||||||
|
|
||||||
|
with Image.open(tmpfile) as im_reloaded:
|
||||||
|
if ElementTree is None:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
|
assert im_reloaded.getxmp() == {}
|
||||||
|
else:
|
||||||
|
assert "xmp" in im_reloaded.info
|
||||||
|
xmp = im_reloaded.getxmp()
|
||||||
|
|
||||||
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
assert description[0]["format"] == "image/tiff"
|
||||||
|
|
||||||
def test_get_photoshop_blocks(self) -> None:
|
def test_get_photoshop_blocks(self) -> None:
|
||||||
with Image.open("Tests/images/lab.tif") as im:
|
with Image.open("Tests/images/lab.tif") as im:
|
||||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
|
@ -671,6 +671,7 @@ class TestImage:
|
||||||
im_remapped = im.remap_palette(list(range(256)))
|
im_remapped = im.remap_palette(list(range(256)))
|
||||||
assert_image_equal(im, im_remapped)
|
assert_image_equal(im, im_remapped)
|
||||||
assert im.palette is not None
|
assert im.palette is not None
|
||||||
|
assert im_remapped.palette is not None
|
||||||
assert im.palette.palette == im_remapped.palette.palette
|
assert im.palette.palette == im_remapped.palette.palette
|
||||||
|
|
||||||
# Test illegal image mode
|
# Test illegal image mode
|
||||||
|
@ -973,6 +974,11 @@ class TestImage:
|
||||||
assert tag not in exif.get_ifd(0x8769)
|
assert tag not in exif.get_ifd(0x8769)
|
||||||
assert exif.get_ifd(0xA005)
|
assert exif.get_ifd(0xA005)
|
||||||
|
|
||||||
|
def test_exif_from_xmp_bytes(self) -> None:
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im.info["xmp"] = b'\xff tiff:Orientation="2"'
|
||||||
|
assert im.getexif()[274] == 2
|
||||||
|
|
||||||
def test_empty_xmp(self) -> None:
|
def test_empty_xmp(self) -> None:
|
||||||
with Image.open("Tests/images/hopper.gif") as im:
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
|
@ -989,7 +995,7 @@ class TestImage:
|
||||||
im = Image.new("RGB", (1, 1))
|
im = Image.new("RGB", (1, 1))
|
||||||
im.info["xmp"] = (
|
im.info["xmp"] = (
|
||||||
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
|
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
|
||||||
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00'
|
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00 '
|
||||||
)
|
)
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
|
|
|
@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
draw.rectangle(bbox, outline=0xFFFF)
|
draw.rectangle(bbox, outline=0xCDEF)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert im.getpixel((X0, Y0)) == 0xCDEF
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None:
|
||||||
pytest.skip("test image not found")
|
pytest.skip("test image not found")
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiff_mmap() -> None:
|
||||||
|
try:
|
||||||
|
with Image.open("Tests/images/crash_mmap.tif") as im:
|
||||||
|
im.seek(1)
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
im.seek(0)
|
||||||
|
im.load()
|
||||||
|
except FileNotFoundError:
|
||||||
|
if on_ci():
|
||||||
|
raise
|
||||||
|
pytest.skip("test image not found")
|
||||||
|
|
|
@ -193,6 +193,20 @@ Image.Image.get_child_images()
|
||||||
method uses an image's file pointer, and so child images could only be retrieved from
|
method uses an image's file pointer, and so child images could only be retrieved from
|
||||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||||
|
|
||||||
|
Saving I mode images as PNG
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 11.3.0
|
||||||
|
|
||||||
|
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
|
||||||
|
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
|
||||||
|
changing the data, this is now deprecated. Instead, the image can be converted to
|
||||||
|
another mode before saving::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
im = Image.new("I", (1, 1))
|
||||||
|
im.convert("I;16").save("out.png")
|
||||||
|
|
||||||
ImageCms.ImageCmsProfile.product_name and .product_info
|
ImageCms.ImageCmsProfile.product_name and .product_info
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -1082,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I
|
||||||
|
|
||||||
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
|
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
|
||||||
|
|
||||||
|
QOI
|
||||||
|
^^^
|
||||||
|
|
||||||
|
.. versionadded:: 9.5.0
|
||||||
|
|
||||||
|
Pillow reads and writes images in Quite OK Image format using a Python codec. If you
|
||||||
|
wish to write code specifically for this format, :pypi:`qoi` is an alternative library
|
||||||
|
that uses C to decode the image and interfaces with NumPy.
|
||||||
|
|
||||||
|
.. _qoi-saving:
|
||||||
|
|
||||||
|
Saving
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
|
||||||
|
|
||||||
|
**colorspace**
|
||||||
|
If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead
|
||||||
|
of all channels being linear.
|
||||||
|
|
||||||
SGI
|
SGI
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
@ -1578,15 +1598,6 @@ PSD
|
||||||
|
|
||||||
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
|
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
|
||||||
|
|
||||||
QOI
|
|
||||||
^^^
|
|
||||||
|
|
||||||
.. versionadded:: 9.5.0
|
|
||||||
|
|
||||||
Pillow reads images in Quite OK Image format using a Python decoder. If you wish to
|
|
||||||
write code specifically for this format, :pypi:`qoi` is an alternative library that
|
|
||||||
uses C to decode the image and interfaces with NumPy.
|
|
||||||
|
|
||||||
SUN
|
SUN
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,12 @@ These platforms are built and tested for every change.
|
||||||
| macOS 13 Ventura | 3.9 | x86-64 |
|
| macOS 13 Ventura | 3.9 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
|
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
|
||||||
| | PyPy3 | |
|
| | 3.14, PyPy3 | |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
|
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
|
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
|
||||||
| | 3.12, 3.13, PyPy3 | |
|
| | 3.12, 3.13, 3.14, PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.12 | arm64v8, ppc64le, |
|
| | 3.12 | arm64v8, ppc64le, |
|
||||||
| | | s390x |
|
| | | s390x |
|
||||||
|
@ -53,7 +53,7 @@ These platforms are built and tested for every change.
|
||||||
| Windows Server 2022 | 3.9 | x86 |
|
| Windows Server 2022 | 3.9 | x86 |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||||
| | PyPy3 | |
|
| | 3.14, PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.12 (MinGW) | x86-64 |
|
| | 3.12 (MinGW) | x86-64 |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
|
|
|
@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType
|
||||||
library). For earlier versions, TrueType support is only available as part of
|
library). For earlier versions, TrueType support is only available as part of
|
||||||
the imToolkit package.
|
the imToolkit package.
|
||||||
|
|
||||||
|
When measuring text sizes, this module will not break at newline characters. For
|
||||||
|
multiline text, see the :py:mod:`~PIL.ImageDraw` module.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
To protect against potential DOS attacks when using arbitrary strings as
|
To protect against potential DOS attacks when using arbitrary strings as
|
||||||
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
|
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
|
||||||
|
|
83
docs/releasenotes/11.3.0.rst
Normal file
83
docs/releasenotes/11.3.0.rst
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
11.3.0
|
||||||
|
------
|
||||||
|
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:cve:`YYYY-XXXXX`: TODO
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Backwards incompatible changes
|
||||||
|
==============================
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
Deprecations
|
||||||
|
============
|
||||||
|
|
||||||
|
Saving I mode images as PNG
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
|
||||||
|
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
|
||||||
|
changing the data, this is now deprecated. Instead, the image can be converted to
|
||||||
|
another mode before saving::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
im = Image.new("I", (1, 1))
|
||||||
|
im.convert("I;16").save("out.png")
|
||||||
|
|
||||||
|
API changes
|
||||||
|
===========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
API additions
|
||||||
|
=============
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
Added QOI saving
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Support has been added for saving QOI images. ``colorspace`` can be used to specify the
|
||||||
|
colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``.
|
||||||
|
By default, all channels will be linear.
|
||||||
|
|
||||||
|
Support using more screenshot utilities with ImageGrab on Linux
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle
|
||||||
|
on Linux in order to take a snapshot of the screen.
|
||||||
|
|
||||||
|
Do not build against libavif < 1
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building
|
||||||
|
from source, if a user happens to have an earlier libavif on their system, Pillow will
|
||||||
|
now ignore it.
|
||||||
|
|
||||||
|
Python 3.14 beta
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To help other projects prepare for Python 3.14, wheels are now built for the
|
||||||
|
3.14 beta as a preview. This is not official support for Python 3.14, but rather
|
||||||
|
an opportunity for you to test how Pillow works with the beta and report any
|
||||||
|
problems.
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
11.3.0
|
||||||
11.2.1
|
11.2.1
|
||||||
11.1.0
|
11.1.0
|
||||||
11.0.0
|
11.0.0
|
||||||
|
|
8
setup.py
8
setup.py
|
@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]:
|
||||||
args: list[str]
|
args: list[str]
|
||||||
env: dict[str, str]
|
env: dict[str, str]
|
||||||
expr: str
|
expr: str
|
||||||
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
|
if sys.platform.startswith(("linux", "gnu")):
|
||||||
if struct.calcsize("l") == 4:
|
if struct.calcsize("l") == 4:
|
||||||
machine = os.uname()[4] + "-32"
|
machine = os.uname()[4] + "-32"
|
||||||
else:
|
else:
|
||||||
|
@ -623,11 +623,7 @@ class pil_build_ext(build_ext):
|
||||||
|
|
||||||
for extension in self.extensions:
|
for extension in self.extensions:
|
||||||
extension.extra_compile_args = ["-Wno-nullability-completeness"]
|
extension.extra_compile_args = ["-Wno-nullability-completeness"]
|
||||||
elif (
|
elif sys.platform.startswith(("linux", "gnu", "freebsd")):
|
||||||
sys.platform.startswith("linux")
|
|
||||||
or sys.platform.startswith("gnu")
|
|
||||||
or sys.platform.startswith("freebsd")
|
|
||||||
):
|
|
||||||
for dirname in _find_library_dirs_ldconfig():
|
for dirname in _find_library_dirs_ldconfig():
|
||||||
_add_directory(library_dirs, dirname)
|
_add_directory(library_dirs, dirname)
|
||||||
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
|
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
|
||||||
|
|
|
@ -31,7 +31,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import IO, Any, Literal, NamedTuple, Union
|
from typing import IO, Any, Literal, NamedTuple, Union, cast
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
Image,
|
Image,
|
||||||
|
@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
if self._frame_palette:
|
if self._frame_palette:
|
||||||
if color * 3 + 3 > len(self._frame_palette.palette):
|
if color * 3 + 3 > len(self._frame_palette.palette):
|
||||||
color = 0
|
color = 0
|
||||||
return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
return cast(
|
||||||
|
tuple[int, int, int],
|
||||||
|
tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return (color, color, color)
|
return (color, color, color)
|
||||||
|
|
||||||
self.dispose = None
|
self.dispose = None
|
||||||
self.dispose_extent = frame_dispose_extent
|
self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent
|
||||||
if self.dispose_extent and self.disposal_method >= 2:
|
if self.dispose_extent and self.disposal_method >= 2:
|
||||||
try:
|
try:
|
||||||
if self.disposal_method == 2:
|
if self.disposal_method == 2:
|
||||||
|
|
|
@ -1511,7 +1511,7 @@ class Image:
|
||||||
return {}
|
return {}
|
||||||
if "xmp" not in self.info:
|
if "xmp" not in self.info:
|
||||||
return {}
|
return {}
|
||||||
root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00"))
|
root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 "))
|
||||||
return {get_name(root.tag): get_value(root)}
|
return {get_name(root.tag): get_value(root)}
|
||||||
|
|
||||||
def getexif(self) -> Exif:
|
def getexif(self) -> Exif:
|
||||||
|
@ -1542,10 +1542,11 @@ class Image:
|
||||||
# XMP tags
|
# XMP tags
|
||||||
if ExifTags.Base.Orientation not in self._exif:
|
if ExifTags.Base.Orientation not in self._exif:
|
||||||
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
||||||
|
pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])'
|
||||||
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
|
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
|
||||||
xmp_tags = xmp_tags.decode("utf-8")
|
pattern = rb'tiff:Orientation(="|>)([0-9])'
|
||||||
if xmp_tags:
|
if xmp_tags:
|
||||||
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
|
match = re.search(pattern, xmp_tags)
|
||||||
if match:
|
if match:
|
||||||
self._exif[ExifTags.Base.Orientation] = int(match[2])
|
self._exif[ExifTags.Base.Orientation] = int(match[2])
|
||||||
|
|
||||||
|
|
|
@ -365,22 +365,10 @@ class ImageDraw:
|
||||||
# use the fill as a mask
|
# use the fill as a mask
|
||||||
mask = Image.new("1", self.im.size)
|
mask = Image.new("1", self.im.size)
|
||||||
mask_ink = self._getink(1)[0]
|
mask_ink = self._getink(1)[0]
|
||||||
|
draw = Draw(mask)
|
||||||
fill_im = mask.copy()
|
|
||||||
draw = Draw(fill_im)
|
|
||||||
draw.draw.draw_polygon(xy, mask_ink, 1)
|
draw.draw.draw_polygon(xy, mask_ink, 1)
|
||||||
|
|
||||||
ink_im = mask.copy()
|
self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
|
||||||
draw = Draw(ink_im)
|
|
||||||
width = width * 2 - 1
|
|
||||||
draw.draw.draw_polygon(xy, mask_ink, 0, width)
|
|
||||||
|
|
||||||
mask.paste(ink_im, mask=fill_im)
|
|
||||||
|
|
||||||
im = Image.new(self.mode, self.im.size)
|
|
||||||
draw = Draw(im)
|
|
||||||
draw.draw.draw_polygon(xy, ink, 0, width)
|
|
||||||
self.im.paste(im.im, (0, 0) + im.size, mask.im)
|
|
||||||
|
|
||||||
def regular_polygon(
|
def regular_polygon(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -134,10 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None:
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
o = struct.unpack_from("I", data)[0]
|
o = struct.unpack_from("I", data)[0]
|
||||||
if data[16] != 0:
|
if data[16] == 0:
|
||||||
files = data[o:].decode("utf-16le").split("\0")
|
|
||||||
else:
|
|
||||||
files = data[o:].decode("mbcs").split("\0")
|
files = data[o:].decode("mbcs").split("\0")
|
||||||
|
else:
|
||||||
|
files = data[o:].decode("utf-16le").split("\0")
|
||||||
return files[: files.index("")]
|
return files[: files.index("")]
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
data = io.BytesIO(data)
|
data = io.BytesIO(data)
|
||||||
|
|
|
@ -762,8 +762,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
extra = info.get("extra", b"")
|
extra = info.get("extra", b"")
|
||||||
|
|
||||||
MAX_BYTES_IN_MARKER = 65533
|
MAX_BYTES_IN_MARKER = 65533
|
||||||
xmp = info.get("xmp")
|
if xmp := info.get("xmp"):
|
||||||
if xmp:
|
|
||||||
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
|
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
|
||||||
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
||||||
if len(xmp) > max_data_bytes_in_marker:
|
if len(xmp) > max_data_bytes_in_marker:
|
||||||
|
@ -772,8 +771,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
size = o16(2 + overhead_len + len(xmp))
|
size = o16(2 + overhead_len + len(xmp))
|
||||||
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
|
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
|
||||||
|
|
||||||
icc_profile = info.get("icc_profile")
|
if icc_profile := info.get("icc_profile"):
|
||||||
if icc_profile:
|
|
||||||
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
|
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
|
||||||
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
||||||
markers = []
|
markers = []
|
||||||
|
@ -831,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
|
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
|
||||||
# channels*size, this is a value that's been used in a django patch.
|
# channels*size, this is a value that's been used in a django patch.
|
||||||
# https://github.com/matthewwithanm/django-imagekit/issues/50
|
# https://github.com/matthewwithanm/django-imagekit/issues/50
|
||||||
bufsize = 0
|
|
||||||
if optimize or progressive:
|
if optimize or progressive:
|
||||||
# CMYK can be bigger
|
# CMYK can be bigger
|
||||||
if im.mode == "CMYK":
|
if im.mode == "CMYK":
|
||||||
|
@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
else:
|
else:
|
||||||
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
|
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
|
||||||
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
||||||
bufsize = max(bufsize, len(exif) + 5, len(extra) + 1)
|
bufsize = max(len(exif) + 5, len(extra) + 1)
|
||||||
|
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
|
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
|
||||||
|
|
|
@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
# header
|
# header
|
||||||
assert self.fp is not None
|
assert self.fp is not None
|
||||||
|
|
||||||
s = self.fp.read(128)
|
s = self.fp.read(68)
|
||||||
if not _accept(s):
|
if not _accept(s):
|
||||||
msg = "not a PCX file"
|
msg = "not a PCX file"
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
|
@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
logger.debug("BBox: %s %s %s %s", *bbox)
|
logger.debug("BBox: %s %s %s %s", *bbox)
|
||||||
|
|
||||||
|
offset = self.fp.tell() + 60
|
||||||
|
|
||||||
# format
|
# format
|
||||||
version = s[1]
|
version = s[1]
|
||||||
bits = s[3]
|
bits = s[3]
|
||||||
|
@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
break
|
break
|
||||||
if mode == "P":
|
if mode == "P":
|
||||||
self.palette = ImagePalette.raw("RGB", s[1:])
|
self.palette = ImagePalette.raw("RGB", s[1:])
|
||||||
self.fp.seek(128)
|
|
||||||
|
|
||||||
elif version == 5 and bits == 8 and planes == 3:
|
elif version == 5 and bits == 8 and planes == 3:
|
||||||
mode = "RGB"
|
mode = "RGB"
|
||||||
|
@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
||||||
bbox = (0, 0) + self.size
|
bbox = (0, 0) + self.size
|
||||||
logger.debug("size: %sx%s", *self.size)
|
logger.debug("size: %sx%s", *self.size)
|
||||||
|
|
||||||
self.tile = [
|
self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))]
|
||||||
ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -48,6 +48,7 @@ from ._binary import i32be as i32
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
from ._binary import o16be as o16
|
from ._binary import o16be as o16
|
||||||
from ._binary import o32be as o32
|
from ._binary import o32be as o32
|
||||||
|
from ._deprecate import deprecate
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
|
@ -1368,6 +1369,8 @@ def _save(
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
msg = f"cannot write mode {mode} as PNG"
|
msg = f"cannot write mode {mode} as PNG"
|
||||||
raise OSError(msg) from e
|
raise OSError(msg) from e
|
||||||
|
if outmode == "I":
|
||||||
|
deprecate("Saving I mode images as PNG", 13, stacklevel=4)
|
||||||
|
|
||||||
#
|
#
|
||||||
# write minimal PNG file
|
# write minimal PNG file
|
||||||
|
|
|
@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile):
|
||||||
msg = "Reached EOF while reading header"
|
msg = "Reached EOF while reading header"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
elif len(token) > 10:
|
elif len(token) > 10:
|
||||||
msg = f"Token too long in file header: {token.decode()}"
|
msg_too_long = b"Token too long in file header: %s" % token
|
||||||
raise ValueError(msg)
|
raise ValueError(msg_too_long)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i32be as i32
|
from ._binary import i32be as i32
|
||||||
|
from ._binary import o8
|
||||||
|
from ._binary import o32be as o32
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
|
@ -51,7 +54,7 @@ class QoiDecoder(ImageFile.PyDecoder):
|
||||||
assert self.fd is not None
|
assert self.fd is not None
|
||||||
|
|
||||||
self._previously_seen_pixels = {}
|
self._previously_seen_pixels = {}
|
||||||
self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
|
self._previous_pixel = bytearray((0, 0, 0, 255))
|
||||||
|
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
bands = Image.getmodebands(self.mode)
|
bands = Image.getmodebands(self.mode)
|
||||||
|
@ -110,6 +113,122 @@ class QoiDecoder(ImageFile.PyDecoder):
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
if im.mode == "RGB":
|
||||||
|
channels = 3
|
||||||
|
elif im.mode == "RGBA":
|
||||||
|
channels = 4
|
||||||
|
else:
|
||||||
|
msg = "Unsupported QOI image mode"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
|
||||||
|
|
||||||
|
fp.write(b"qoif")
|
||||||
|
fp.write(o32(im.size[0]))
|
||||||
|
fp.write(o32(im.size[1]))
|
||||||
|
fp.write(o8(channels))
|
||||||
|
fp.write(o8(colorspace))
|
||||||
|
|
||||||
|
ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
|
||||||
|
|
||||||
|
|
||||||
|
class QoiEncoder(ImageFile.PyEncoder):
|
||||||
|
_pushes_fd = True
|
||||||
|
_previous_pixel: tuple[int, int, int, int] | None = None
|
||||||
|
_previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
|
||||||
|
_run = 0
|
||||||
|
|
||||||
|
def _write_run(self) -> bytes:
|
||||||
|
data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
|
||||||
|
self._run = 0
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _delta(self, left: int, right: int) -> int:
|
||||||
|
result = (left - right) & 255
|
||||||
|
if result >= 128:
|
||||||
|
result -= 256
|
||||||
|
return result
|
||||||
|
|
||||||
|
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||||
|
assert self.im is not None
|
||||||
|
|
||||||
|
self._previously_seen_pixels = {0: (0, 0, 0, 0)}
|
||||||
|
self._previous_pixel = (0, 0, 0, 255)
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
w, h = self.im.size
|
||||||
|
bands = Image.getmodebands(self.mode)
|
||||||
|
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
pixel = self.im.getpixel((x, y))
|
||||||
|
if bands == 3:
|
||||||
|
pixel = (*pixel, 255)
|
||||||
|
|
||||||
|
if pixel == self._previous_pixel:
|
||||||
|
self._run += 1
|
||||||
|
if self._run == 62:
|
||||||
|
data += self._write_run()
|
||||||
|
else:
|
||||||
|
if self._run:
|
||||||
|
data += self._write_run()
|
||||||
|
|
||||||
|
r, g, b, a = pixel
|
||||||
|
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
|
||||||
|
if self._previously_seen_pixels.get(hash_value) == pixel:
|
||||||
|
data += o8(hash_value) # QOI_OP_INDEX
|
||||||
|
elif self._previous_pixel:
|
||||||
|
self._previously_seen_pixels[hash_value] = pixel
|
||||||
|
|
||||||
|
prev_r, prev_g, prev_b, prev_a = self._previous_pixel
|
||||||
|
if prev_a == a:
|
||||||
|
delta_r = self._delta(r, prev_r)
|
||||||
|
delta_g = self._delta(g, prev_g)
|
||||||
|
delta_b = self._delta(b, prev_b)
|
||||||
|
|
||||||
|
if (
|
||||||
|
-2 <= delta_r < 2
|
||||||
|
and -2 <= delta_g < 2
|
||||||
|
and -2 <= delta_b < 2
|
||||||
|
):
|
||||||
|
data += o8(
|
||||||
|
0b01000000
|
||||||
|
| (delta_r + 2) << 4
|
||||||
|
| (delta_g + 2) << 2
|
||||||
|
| (delta_b + 2)
|
||||||
|
) # QOI_OP_DIFF
|
||||||
|
else:
|
||||||
|
delta_gr = self._delta(delta_r, delta_g)
|
||||||
|
delta_gb = self._delta(delta_b, delta_g)
|
||||||
|
if (
|
||||||
|
-8 <= delta_gr < 8
|
||||||
|
and -32 <= delta_g < 32
|
||||||
|
and -8 <= delta_gb < 8
|
||||||
|
):
|
||||||
|
data += o8(
|
||||||
|
0b10000000 | (delta_g + 32)
|
||||||
|
) # QOI_OP_LUMA
|
||||||
|
data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
|
||||||
|
else:
|
||||||
|
data += o8(0b11111110) # QOI_OP_RGB
|
||||||
|
data += bytes(pixel[:3])
|
||||||
|
else:
|
||||||
|
data += o8(0b11111111) # QOI_OP_RGBA
|
||||||
|
data += bytes(pixel)
|
||||||
|
|
||||||
|
self._previous_pixel = pixel
|
||||||
|
|
||||||
|
if self._run:
|
||||||
|
data += self._write_run()
|
||||||
|
data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
|
||||||
|
|
||||||
|
return len(data), 0, data
|
||||||
|
|
||||||
|
|
||||||
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
|
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
|
||||||
Image.register_decoder("qoi", QoiDecoder)
|
Image.register_decoder("qoi", QoiDecoder)
|
||||||
Image.register_extension(QoiImageFile.format, ".qoi")
|
Image.register_extension(QoiImageFile.format, ".qoi")
|
||||||
|
|
||||||
|
Image.register_save(QoiImageFile.format, _save)
|
||||||
|
Image.register_encoder("qoi", QoiEncoder)
|
||||||
|
|
|
@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
return
|
return
|
||||||
self._seek(frame)
|
self._seek(frame)
|
||||||
if self._im is not None and (
|
if self._im is not None and (
|
||||||
self.im.size != self._tile_size or self.im.mode != self.mode
|
self.im.size != self._tile_size
|
||||||
|
or self.im.mode != self.mode
|
||||||
|
or self.readonly
|
||||||
):
|
):
|
||||||
# The core image will no longer be used
|
|
||||||
self._im = None
|
self._im = None
|
||||||
|
|
||||||
def _seek(self, frame: int) -> None:
|
def _seek(self, frame: int) -> None:
|
||||||
|
@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
self.fp.seek(self._frame_pos[frame])
|
self.fp.seek(self._frame_pos[frame])
|
||||||
self.tag_v2.load(self.fp)
|
self.tag_v2.load(self.fp)
|
||||||
if XMP in self.tag_v2:
|
if XMP in self.tag_v2:
|
||||||
self.info["xmp"] = self.tag_v2[XMP]
|
xmp = self.tag_v2[XMP]
|
||||||
|
if isinstance(xmp, tuple) and len(xmp) == 1:
|
||||||
|
xmp = xmp[0]
|
||||||
|
self.info["xmp"] = xmp
|
||||||
elif "xmp" in self.info:
|
elif "xmp" in self.info:
|
||||||
del self.info["xmp"]
|
del self.info["xmp"]
|
||||||
self._reload_exif()
|
self._reload_exif()
|
||||||
|
|
|
@ -12,6 +12,7 @@ def deprecate(
|
||||||
*,
|
*,
|
||||||
action: str | None = None,
|
action: str | None = None,
|
||||||
plural: bool = False,
|
plural: bool = False,
|
||||||
|
stacklevel: int = 3,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Deprecations helper.
|
Deprecations helper.
|
||||||
|
@ -67,5 +68,5 @@ def deprecate(
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
|
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=3,
|
stacklevel=stacklevel,
|
||||||
)
|
)
|
||||||
|
|
|
@ -338,12 +338,6 @@ static const char *no_palette = "image has no palette";
|
||||||
static const char *readonly = "image is readonly";
|
static const char *readonly = "image is readonly";
|
||||||
/* static const char* no_content = "image has no content"; */
|
/* static const char* no_content = "image has no content"; */
|
||||||
|
|
||||||
void *
|
|
||||||
ImagingError_OSError(void) {
|
|
||||||
PyErr_SetString(PyExc_OSError, "error when accessing file");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void *
|
void *
|
||||||
ImagingError_MemoryError(void) {
|
ImagingError_MemoryError(void) {
|
||||||
return PyErr_NoMemory();
|
return PyErr_NoMemory();
|
||||||
|
@ -369,11 +363,6 @@ ImagingError_ValueError(const char *message) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
ImagingError_Clear(void) {
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/* HELPERS */
|
/* HELPERS */
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
|
@ -3220,7 +3209,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
|
||||||
(int)p[3],
|
(int)p[3],
|
||||||
&ink,
|
&ink,
|
||||||
width,
|
width,
|
||||||
self->blend
|
self->blend,
|
||||||
|
NULL
|
||||||
) < 0) {
|
) < 0) {
|
||||||
free(xy);
|
free(xy);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -3358,7 +3348,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
|
||||||
int ink;
|
int ink;
|
||||||
int fill = 0;
|
int fill = 0;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) {
|
ImagingObject *maskp = NULL;
|
||||||
|
if (!PyArg_ParseTuple(
|
||||||
|
args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp
|
||||||
|
)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3388,8 +3381,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
|
||||||
|
|
||||||
free(xy);
|
free(xy);
|
||||||
|
|
||||||
if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) <
|
if (ImagingDrawPolygon(
|
||||||
0) {
|
self->image->image,
|
||||||
|
n,
|
||||||
|
ixy,
|
||||||
|
&ink,
|
||||||
|
fill,
|
||||||
|
width,
|
||||||
|
self->blend,
|
||||||
|
maskp ? maskp->image : NULL
|
||||||
|
) < 0) {
|
||||||
free(ixy);
|
free(ixy);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* for now, single block images */
|
/* for now, single block images */
|
||||||
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
if (im->blocks_count > 1) {
|
||||||
return IMAGING_ARROW_MEMORY_LAYOUT;
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) {
|
||||||
int length = im->xsize * im->ysize;
|
int length = im->xsize * im->ysize;
|
||||||
|
|
||||||
/* for now, single block images */
|
/* for now, single block images */
|
||||||
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
if (im->blocks_count > 1) {
|
||||||
return IMAGING_ARROW_MEMORY_LAYOUT;
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
|
||||||
int length = im->xsize * im->ysize;
|
int length = im->xsize * im->ysize;
|
||||||
|
|
||||||
/* for now, single block images */
|
/* for now, single block images */
|
||||||
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
if (im->blocks_count > 1) {
|
||||||
return IMAGING_ARROW_MEMORY_LAYOUT;
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ typedef struct {
|
||||||
} Edge;
|
} Edge;
|
||||||
|
|
||||||
/* Type used in "polygon*" functions */
|
/* Type used in "polygon*" functions */
|
||||||
typedef void (*hline_handler)(Imaging, int, int, int, int);
|
typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging);
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
point8(Imaging im, int x, int y, int ink) {
|
point8(Imaging im, int x, int y, int ink) {
|
||||||
|
@ -103,9 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
hline8(Imaging im, int x0, int y0, int x1, int ink) {
|
hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
||||||
int pixelwidth;
|
|
||||||
|
|
||||||
if (y0 >= 0 && y0 < im->ysize) {
|
if (y0 >= 0 && y0 < im->ysize) {
|
||||||
if (x0 < 0) {
|
if (x0 < 0) {
|
||||||
x0 = 0;
|
x0 = 0;
|
||||||
|
@ -118,16 +116,41 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) {
|
||||||
x1 = im->xsize - 1;
|
x1 = im->xsize - 1;
|
||||||
}
|
}
|
||||||
if (x0 <= x1) {
|
if (x0 <= x1) {
|
||||||
pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1;
|
int bigendian = -1;
|
||||||
memset(
|
if (strncmp(im->mode, "I;16", 4) == 0) {
|
||||||
im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth
|
bigendian =
|
||||||
);
|
(
|
||||||
|
#ifdef WORDS_BIGENDIAN
|
||||||
|
strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0
|
||||||
|
#else
|
||||||
|
strcmp(im->mode, "I;16B") == 0
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
if (mask == NULL && bigendian == -1) {
|
||||||
|
memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1));
|
||||||
|
} else {
|
||||||
|
UINT8 *p = im->image8[y0];
|
||||||
|
while (x0 <= x1) {
|
||||||
|
if (mask == NULL || mask->image8[y0][x0]) {
|
||||||
|
if (bigendian == -1) {
|
||||||
|
p[x0] = ink;
|
||||||
|
} else {
|
||||||
|
p[x0 * 2 + (bigendian ? 1 : 0)] = ink;
|
||||||
|
p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x0++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
hline32(Imaging im, int x0, int y0, int x1, int ink) {
|
hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
||||||
INT32 *p;
|
INT32 *p;
|
||||||
|
|
||||||
if (y0 >= 0 && y0 < im->ysize) {
|
if (y0 >= 0 && y0 < im->ysize) {
|
||||||
|
@ -143,13 +166,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) {
|
||||||
}
|
}
|
||||||
p = im->image32[y0];
|
p = im->image32[y0];
|
||||||
while (x0 <= x1) {
|
while (x0 <= x1) {
|
||||||
p[x0++] = ink;
|
if (mask == NULL || mask->image8[y0][x0]) {
|
||||||
|
p[x0] = ink;
|
||||||
|
}
|
||||||
|
x0++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
|
hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
|
||||||
unsigned int tmp;
|
unsigned int tmp;
|
||||||
|
|
||||||
if (y0 >= 0 && y0 < im->ysize) {
|
if (y0 >= 0 && y0 < im->ysize) {
|
||||||
|
@ -167,9 +193,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
|
||||||
UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4;
|
UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4;
|
||||||
UINT8 *in = (UINT8 *)&ink;
|
UINT8 *in = (UINT8 *)&ink;
|
||||||
while (x0 <= x1) {
|
while (x0 <= x1) {
|
||||||
|
if (mask == NULL || mask->image8[y0][x0]) {
|
||||||
out[0] = BLEND(in[3], out[0], in[0], tmp);
|
out[0] = BLEND(in[3], out[0], in[0], tmp);
|
||||||
out[1] = BLEND(in[3], out[1], in[1], tmp);
|
out[1] = BLEND(in[3], out[1], in[1], tmp);
|
||||||
out[2] = BLEND(in[3], out[2], in[2], tmp);
|
out[2] = BLEND(in[3], out[2], in[2], tmp);
|
||||||
|
}
|
||||||
x0++;
|
x0++;
|
||||||
out += 4;
|
out += 4;
|
||||||
}
|
}
|
||||||
|
@ -407,7 +435,14 @@ x_cmp(const void *x0, const void *x1) {
|
||||||
|
|
||||||
static void
|
static void
|
||||||
draw_horizontal_lines(
|
draw_horizontal_lines(
|
||||||
Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline
|
Imaging im,
|
||||||
|
int n,
|
||||||
|
Edge *e,
|
||||||
|
int ink,
|
||||||
|
int *x_pos,
|
||||||
|
int y,
|
||||||
|
hline_handler hline,
|
||||||
|
Imaging mask
|
||||||
) {
|
) {
|
||||||
int i;
|
int i;
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
|
@ -429,7 +464,7 @@ draw_horizontal_lines(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(*hline)(im, xmin, e[i].ymin, xmax, ink);
|
(*hline)(im, xmin, e[i].ymin, xmax, ink, mask);
|
||||||
*x_pos = xmax + 1;
|
*x_pos = xmax + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,7 +475,7 @@ draw_horizontal_lines(
|
||||||
*/
|
*/
|
||||||
static inline int
|
static inline int
|
||||||
polygon_generic(
|
polygon_generic(
|
||||||
Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha
|
Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask
|
||||||
) {
|
) {
|
||||||
Edge **edge_table;
|
Edge **edge_table;
|
||||||
float *xx;
|
float *xx;
|
||||||
|
@ -461,6 +496,7 @@ polygon_generic(
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int hasAlpha = hline == hline32rgba;
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
if (ymin > e[i].ymin) {
|
if (ymin > e[i].ymin) {
|
||||||
ymin = e[i].ymin;
|
ymin = e[i].ymin;
|
||||||
|
@ -470,7 +506,7 @@ polygon_generic(
|
||||||
}
|
}
|
||||||
if (e[i].ymin == e[i].ymax) {
|
if (e[i].ymin == e[i].ymax) {
|
||||||
if (hasAlpha != 1) {
|
if (hasAlpha != 1) {
|
||||||
(*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink);
|
(*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -558,7 +594,7 @@ polygon_generic(
|
||||||
// Line would be before the current position
|
// Line would be before the current position
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
|
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
|
||||||
if (x_end < x_pos) {
|
if (x_end < x_pos) {
|
||||||
// Line would be before the current position
|
// Line would be before the current position
|
||||||
continue;
|
continue;
|
||||||
|
@ -574,13 +610,13 @@ polygon_generic(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(*hline)(im, x_start, ymin, x_end, ink);
|
(*hline)(im, x_start, ymin, x_end, ink, mask);
|
||||||
x_pos = x_end + 1;
|
x_pos = x_end + 1;
|
||||||
}
|
}
|
||||||
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
|
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
|
||||||
} else {
|
} else {
|
||||||
for (i = 1; i < j; i += 2) {
|
for (i = 1; i < j; i += 2) {
|
||||||
(*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink);
|
(*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -590,21 +626,6 @@ polygon_generic(
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int
|
|
||||||
polygon8(Imaging im, int n, Edge *e, int ink, int eofill) {
|
|
||||||
return polygon_generic(im, n, e, ink, eofill, hline8, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
polygon32(Imaging im, int n, Edge *e, int ink, int eofill) {
|
|
||||||
return polygon_generic(im, n, e, ink, eofill, hline32, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) {
|
|
||||||
return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
add_edge(Edge *e, int x0, int y0, int x1, int y1) {
|
add_edge(Edge *e, int x0, int y0, int x1, int y1) {
|
||||||
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
|
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
|
||||||
|
@ -639,14 +660,13 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) {
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void (*point)(Imaging im, int x, int y, int ink);
|
void (*point)(Imaging im, int x, int y, int ink);
|
||||||
void (*hline)(Imaging im, int x0, int y0, int x1, int ink);
|
void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask);
|
||||||
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
|
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
|
||||||
int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill);
|
|
||||||
} DRAW;
|
} DRAW;
|
||||||
|
|
||||||
DRAW draw8 = {point8, hline8, line8, polygon8};
|
DRAW draw8 = {point8, hline8, line8};
|
||||||
DRAW draw32 = {point32, hline32, line32, polygon32};
|
DRAW draw32 = {point32, hline32, line32};
|
||||||
DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba};
|
DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba};
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/* Interface */
|
/* Interface */
|
||||||
|
@ -691,7 +711,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in
|
||||||
|
|
||||||
int
|
int
|
||||||
ImagingDrawWideLine(
|
ImagingDrawWideLine(
|
||||||
Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op
|
Imaging im,
|
||||||
|
int x0,
|
||||||
|
int y0,
|
||||||
|
int x1,
|
||||||
|
int y1,
|
||||||
|
const void *ink_,
|
||||||
|
int width,
|
||||||
|
int op,
|
||||||
|
Imaging mask
|
||||||
) {
|
) {
|
||||||
DRAW *draw;
|
DRAW *draw;
|
||||||
INT32 ink;
|
INT32 ink;
|
||||||
|
@ -731,7 +759,7 @@ ImagingDrawWideLine(
|
||||||
add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]);
|
add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]);
|
||||||
add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]);
|
add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]);
|
||||||
|
|
||||||
draw->polygon(im, 4, e, ink, 0);
|
polygon_generic(im, 4, e, ink, 0, draw->hline, mask);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -774,7 +802,7 @@ ImagingDrawRectangle(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (y = y0; y <= y1; y++) {
|
for (y = y0; y <= y1; y++) {
|
||||||
draw->hline(im, x0, y, x1, ink);
|
draw->hline(im, x0, y, x1, ink, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -783,8 +811,8 @@ ImagingDrawRectangle(
|
||||||
width = 1;
|
width = 1;
|
||||||
}
|
}
|
||||||
for (i = 0; i < width; i++) {
|
for (i = 0; i < width; i++) {
|
||||||
draw->hline(im, x0, y0 + i, x1, ink);
|
draw->hline(im, x0, y0 + i, x1, ink, NULL);
|
||||||
draw->hline(im, x0, y1 - i, x1, ink);
|
draw->hline(im, x0, y1 - i, x1, ink, NULL);
|
||||||
draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink);
|
draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink);
|
||||||
draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink);
|
draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink);
|
||||||
}
|
}
|
||||||
|
@ -795,7 +823,14 @@ ImagingDrawRectangle(
|
||||||
|
|
||||||
int
|
int
|
||||||
ImagingDrawPolygon(
|
ImagingDrawPolygon(
|
||||||
Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op
|
Imaging im,
|
||||||
|
int count,
|
||||||
|
int *xy,
|
||||||
|
const void *ink_,
|
||||||
|
int fill,
|
||||||
|
int width,
|
||||||
|
int op,
|
||||||
|
Imaging mask
|
||||||
) {
|
) {
|
||||||
int i, n, x0, y0, x1, y1;
|
int i, n, x0, y0, x1, y1;
|
||||||
DRAW *draw;
|
DRAW *draw;
|
||||||
|
@ -839,7 +874,7 @@ ImagingDrawPolygon(
|
||||||
if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) {
|
if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) {
|
||||||
add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]);
|
add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]);
|
||||||
}
|
}
|
||||||
draw->polygon(im, n, e, ink, 0);
|
polygon_generic(im, n, e, ink, 0, draw->hline, mask);
|
||||||
free(e);
|
free(e);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -861,11 +896,12 @@ ImagingDrawPolygon(
|
||||||
xy[i * 2 + 3],
|
xy[i * 2 + 3],
|
||||||
ink_,
|
ink_,
|
||||||
width,
|
width,
|
||||||
op
|
op,
|
||||||
|
mask
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ImagingDrawWideLine(
|
ImagingDrawWideLine(
|
||||||
im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op
|
im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1536,7 +1572,9 @@ ellipseNew(
|
||||||
ellipse_init(&st, a, b, width);
|
ellipse_init(&st, a, b, width);
|
||||||
int32_t X0, Y, X1;
|
int32_t X0, Y, X1;
|
||||||
while (ellipse_next(&st, &X0, &Y, &X1) != -1) {
|
while (ellipse_next(&st, &X0, &Y, &X1) != -1) {
|
||||||
draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
|
draw->hline(
|
||||||
|
im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1571,7 +1609,9 @@ clipEllipseNew(
|
||||||
int32_t X0, Y, X1;
|
int32_t X0, Y, X1;
|
||||||
int next_code;
|
int next_code;
|
||||||
while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) {
|
while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) {
|
||||||
draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
|
draw->hline(
|
||||||
|
im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
clip_ellipse_free(&st);
|
clip_ellipse_free(&st);
|
||||||
return next_code == -1 ? 0 : -1;
|
return next_code == -1 ? 0 : -1;
|
||||||
|
@ -1989,7 +2029,7 @@ ImagingDrawOutline(
|
||||||
|
|
||||||
DRAWINIT();
|
DRAWINIT();
|
||||||
|
|
||||||
draw->polygon(im, outline->count, outline->edges, ink, 0);
|
polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ ImagingSavePPM(Imaging im, const char *outfile) {
|
||||||
|
|
||||||
fp = fopen(outfile, "wb");
|
fp = fopen(outfile, "wb");
|
||||||
if (!fp) {
|
if (!fp) {
|
||||||
(void)ImagingError_OSError();
|
PyErr_SetString(PyExc_OSError, "error when accessing file");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -270,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie);
|
||||||
/* Exceptions */
|
/* Exceptions */
|
||||||
/* ---------- */
|
/* ---------- */
|
||||||
|
|
||||||
extern void *
|
|
||||||
ImagingError_OSError(void);
|
|
||||||
extern void *
|
extern void *
|
||||||
ImagingError_MemoryError(void);
|
ImagingError_MemoryError(void);
|
||||||
extern void *
|
extern void *
|
||||||
|
@ -280,8 +278,6 @@ extern void *
|
||||||
ImagingError_Mismatch(void); /* maps to ValueError by default */
|
ImagingError_Mismatch(void); /* maps to ValueError by default */
|
||||||
extern void *
|
extern void *
|
||||||
ImagingError_ValueError(const char *message);
|
ImagingError_ValueError(const char *message);
|
||||||
extern void
|
|
||||||
ImagingError_Clear(void);
|
|
||||||
|
|
||||||
/* Transform callbacks */
|
/* Transform callbacks */
|
||||||
/* ------------------- */
|
/* ------------------- */
|
||||||
|
@ -510,7 +506,15 @@ extern int
|
||||||
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op);
|
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op);
|
||||||
extern int
|
extern int
|
||||||
ImagingDrawWideLine(
|
ImagingDrawWideLine(
|
||||||
Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op
|
Imaging im,
|
||||||
|
int x0,
|
||||||
|
int y0,
|
||||||
|
int x1,
|
||||||
|
int y1,
|
||||||
|
const void *ink,
|
||||||
|
int width,
|
||||||
|
int op,
|
||||||
|
Imaging mask
|
||||||
);
|
);
|
||||||
extern int
|
extern int
|
||||||
ImagingDrawPieslice(
|
ImagingDrawPieslice(
|
||||||
|
@ -530,7 +534,14 @@ extern int
|
||||||
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
|
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
|
||||||
extern int
|
extern int
|
||||||
ImagingDrawPolygon(
|
ImagingDrawPolygon(
|
||||||
Imaging im, int points, int *xy, const void *ink, int fill, int width, int op
|
Imaging im,
|
||||||
|
int points,
|
||||||
|
int *xy,
|
||||||
|
const void *ink,
|
||||||
|
int fill,
|
||||||
|
int width,
|
||||||
|
int op,
|
||||||
|
Imaging mask
|
||||||
);
|
);
|
||||||
extern int
|
extern int
|
||||||
ImagingDrawRectangle(
|
ImagingDrawRectangle(
|
||||||
|
|
|
@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state->x >= state->bytes) {
|
if (state->x >= state->bytes) {
|
||||||
if (state->bytes % state->xsize && state->bytes > state->xsize) {
|
int bands;
|
||||||
int bands = state->bytes / state->xsize;
|
int xsize = 0;
|
||||||
int stride = state->bytes / bands;
|
int stride = 0;
|
||||||
|
if (state->bits == 2 || state->bits == 4) {
|
||||||
|
xsize = (state->xsize + 7) / 8;
|
||||||
|
bands = state->bits;
|
||||||
|
stride = state->bytes / state->bits;
|
||||||
|
} else {
|
||||||
|
xsize = state->xsize;
|
||||||
|
bands = state->bytes / state->xsize;
|
||||||
|
if (bands != 0) {
|
||||||
|
stride = state->bytes / bands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stride > xsize) {
|
||||||
int i;
|
int i;
|
||||||
for (i = 1; i < bands; i++) { // note -- skipping first band
|
for (i = 1; i < bands; i++) { // note -- skipping first band
|
||||||
memmove(
|
memmove(
|
||||||
&state->buffer[i * state->xsize],
|
&state->buffer[i * xsize], &state->buffer[i * stride], xsize
|
||||||
&state->buffer[i * stride],
|
|
||||||
state->xsize
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -645,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
|
||||||
return im;
|
return im;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImagingError_Clear();
|
PyErr_Clear();
|
||||||
|
|
||||||
// Try to allocate the image once more with smallest possible block size
|
// Try to allocate the image once more with smallest possible block size
|
||||||
MUTEX_LOCK(&ImagingDefaultArena.mutex);
|
MUTEX_LOCK(&ImagingDefaultArena.mutex);
|
||||||
|
|
|
@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
im->read_only = view.readonly;
|
||||||
im->destroy = mapping_destroy_buffer;
|
im->destroy = mapping_destroy_buffer;
|
||||||
|
|
||||||
Py_INCREF(target);
|
Py_INCREF(target);
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -3,7 +3,7 @@ requires =
|
||||||
tox>=4.2
|
tox>=4.2
|
||||||
env_list =
|
env_list =
|
||||||
lint
|
lint
|
||||||
py{py3, 313, 312, 311, 310, 39}
|
py{py3, 314, 313, 312, 311, 310, 39}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
|
|
|
@ -114,11 +114,11 @@ V = {
|
||||||
"FREETYPE": "2.13.3",
|
"FREETYPE": "2.13.3",
|
||||||
"FRIBIDI": "1.0.16",
|
"FRIBIDI": "1.0.16",
|
||||||
"HARFBUZZ": "11.2.1",
|
"HARFBUZZ": "11.2.1",
|
||||||
"JPEGTURBO": "3.1.0",
|
"JPEGTURBO": "3.1.1",
|
||||||
"LCMS2": "2.17",
|
"LCMS2": "2.17",
|
||||||
"LIBAVIF": "1.3.0",
|
"LIBAVIF": "1.3.0",
|
||||||
"LIBIMAGEQUANT": "4.3.4",
|
"LIBIMAGEQUANT": "4.3.4",
|
||||||
"LIBPNG": "1.6.48",
|
"LIBPNG": "1.6.49",
|
||||||
"LIBWEBP": "1.5.0",
|
"LIBWEBP": "1.5.0",
|
||||||
"OPENJPEG": "2.5.3",
|
"OPENJPEG": "2.5.3",
|
||||||
"TIFF": "4.7.0",
|
"TIFF": "4.7.0",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user