Merge branch 'main' into apng
|
@ -32,10 +32,10 @@ install:
|
||||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||||
- 7z x pillow-test-images.zip -oc:\
|
- 7z x pillow-test-images.zip -oc:\
|
||||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
|
||||||
- 7z x nasm-win64.zip -oc:\
|
- 7z x nasm-win64.zip -oc:\
|
||||||
- choco install ghostscript --version=10.3.0
|
- choco install ghostscript --version=10.3.1
|
||||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
|
||||||
- cd c:\pillow\winbuild\
|
- cd c:\pillow\winbuild\
|
||||||
- ps: |
|
- ps: |
|
||||||
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.18.1
|
cibuildwheel==2.19.1
|
||||||
|
|
6
.github/workflows/macos-install.sh
vendored
|
@ -7,11 +7,15 @@ brew install \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
libimagequant \
|
libimagequant \
|
||||||
libjpeg \
|
libjpeg \
|
||||||
libraqm \
|
|
||||||
libtiff \
|
libtiff \
|
||||||
little-cms2 \
|
little-cms2 \
|
||||||
openjpeg \
|
openjpeg \
|
||||||
webp
|
webp
|
||||||
|
if [[ "$ImageOS" == "macos13" ]]; then
|
||||||
|
brew install --ignore-dependencies libraqm
|
||||||
|
else
|
||||||
|
brew install libraqm
|
||||||
|
fi
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
# TODO Update condition when cffi supports 3.13
|
# TODO Update condition when cffi supports 3.13
|
||||||
|
|
2
.github/workflows/test-windows.yml
vendored
|
@ -86,7 +86,7 @@ jobs:
|
||||||
choco install nasm --no-progress
|
choco install nasm --no-progress
|
||||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
choco install ghostscript --version=10.3.0 --no-progress
|
choco install ghostscript --version=10.3.1 --no-progress
|
||||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
|
|
10
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,9 +16,9 @@ ARCHIVE_SDIR=pillow-depends-main
|
||||||
|
|
||||||
# Package versions for fresh source builds
|
# Package versions for fresh source builds
|
||||||
FREETYPE_VERSION=2.13.2
|
FREETYPE_VERSION=2.13.2
|
||||||
HARFBUZZ_VERSION=8.4.0
|
HARFBUZZ_VERSION=8.5.0
|
||||||
LIBPNG_VERSION=1.6.43
|
LIBPNG_VERSION=1.6.43
|
||||||
JPEGTURBO_VERSION=3.0.2
|
JPEGTURBO_VERSION=3.0.3
|
||||||
OPENJPEG_VERSION=2.5.2
|
OPENJPEG_VERSION=2.5.2
|
||||||
XZ_VERSION=5.4.5
|
XZ_VERSION=5.4.5
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.6.0
|
||||||
|
@ -33,9 +33,9 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||||
else
|
else
|
||||||
ZLIB_VERSION=1.2.8
|
ZLIB_VERSION=1.2.8
|
||||||
fi
|
fi
|
||||||
LIBWEBP_VERSION=1.3.2
|
LIBWEBP_VERSION=1.4.0
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.16.1
|
LIBXCB_VERSION=1.17.0
|
||||||
BROTLI_VERSION=1.1.0
|
BROTLI_VERSION=1.1.0
|
||||||
|
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||||
|
@ -70,7 +70,7 @@ function build {
|
||||||
fi
|
fi
|
||||||
build_new_zlib
|
build_new_zlib
|
||||||
|
|
||||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
if [ -n "$IS_MACOS" ]; then
|
if [ -n "$IS_MACOS" ]; then
|
||||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||||
|
|
|
@ -3,7 +3,7 @@ version: 2
|
||||||
formats: [pdf]
|
formats: [pdf]
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-lts-latest
|
||||||
tools:
|
tools:
|
||||||
python: "3"
|
python: "3"
|
||||||
jobs:
|
jobs:
|
||||||
|
|
30
CHANGES.rst
|
@ -5,6 +5,36 @@ Changelog (Pillow)
|
||||||
10.4.0 (unreleased)
|
10.4.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Read IM and TIFF images as RGB, rather than RGBX #7997
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Clarify ImageDraw2 error message when size is missing #8165
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support unpacking more rawmodes to RGBA palettes #7966
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Removed support for Qt 5 #8159
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
|
||||||
|
[mamg22, radarhere]
|
||||||
|
|
||||||
|
- Improved consistency of XMP handling #8069
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use pkg-config to help find libwebp and raqm #8142
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Accept 't' suffix for libtiff version #8126, #8129
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Deprecate ImageDraw.getdraw hints parameter #8124
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
- Added ImageDraw circle() #8085
|
- Added ImageDraw circle() #8085
|
||||||
[void4, hugovk, radarhere]
|
[void4, hugovk, radarhere]
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ def test_direct() -> None:
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
access = PyAccess.new(im, False)
|
access = PyAccess.new(im, False)
|
||||||
|
|
||||||
|
assert access is not None
|
||||||
assert caccess[(0, 0)] == access[(0, 0)]
|
assert caccess[(0, 0)] == access[(0, 0)]
|
||||||
|
|
||||||
print(f"Size: {im.width}x{im.height}")
|
print(f"Size: {im.width}x{im.height}")
|
||||||
|
|
|
@ -18,7 +18,7 @@ from typing import Any, Callable, Sequence
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, ImageMath, features
|
from PIL import Image, ImageFile, ImageMath, features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@ class PillowLeakTestCase:
|
||||||
# helpers
|
# helpers
|
||||||
|
|
||||||
|
|
||||||
def fromstring(data: bytes) -> Image.Image:
|
def fromstring(data: bytes) -> ImageFile.ImageFile:
|
||||||
return Image.open(BytesIO(data))
|
return Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -354,10 +354,10 @@ class TestColorLut3DCoreAPI:
|
||||||
class TestColorLut3DFilter:
|
class TestColorLut3DFilter:
|
||||||
def test_wrong_args(self) -> None:
|
def test_wrong_args(self) -> None:
|
||||||
with pytest.raises(ValueError, match="should be either an integer"):
|
with pytest.raises(ValueError, match="should be either an integer"):
|
||||||
ImageFilter.Color3DLUT("small", [1])
|
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="should be either an integer"):
|
with pytest.raises(ValueError, match="should be either an integer"):
|
||||||
ImageFilter.Color3DLUT((11, 11), [1])
|
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
||||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||||
|
|
|
@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
||||||
|
|
||||||
|
|
||||||
class TestDecompressionBomb:
|
class TestDecompressionBomb:
|
||||||
def teardown_method(self, method) -> None:
|
def teardown_method(self) -> None:
|
||||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||||
|
|
||||||
def test_no_warning_small_file(self) -> None:
|
def test_no_warning_small_file(self) -> None:
|
||||||
|
|
|
@ -38,7 +38,9 @@ def test_version() -> None:
|
||||||
assert function(name) == version
|
assert function(name) == version
|
||||||
if name != "PIL":
|
if name != "PIL":
|
||||||
if name == "zlib" and version is not None:
|
if name == "zlib" and version is not None:
|
||||||
version = version.replace(".zlib-ng", "")
|
version = re.sub(".zlib-ng$", "", version)
|
||||||
|
elif name == "libtiff" and version is not None:
|
||||||
|
version = re.sub("t$", "", version)
|
||||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||||
|
|
||||||
for module in features.modules:
|
for module in features.modules:
|
||||||
|
@ -124,7 +126,7 @@ def test_unsupported_module() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||||
def test_pilinfo(supported_formats) -> None:
|
def test_pilinfo(supported_formats: bool) -> None:
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
features.pilinfo(buf, supported_formats=supported_formats)
|
features.pilinfo(buf, supported_formats=supported_formats)
|
||||||
out = buf.getvalue()
|
out = buf.getvalue()
|
||||||
|
|
|
@ -140,7 +140,7 @@ def test_load_dib() -> None:
|
||||||
(124, "g/pal8v5.bmp"),
|
(124, "g/pal8v5.bmp"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dib_header_size(header_size, path):
|
def test_dib_header_size(header_size: int, path: str) -> None:
|
||||||
image_path = "Tests/images/bmp/" + path
|
image_path = "Tests/images/bmp/" + path
|
||||||
with open(image_path, "rb") as fp:
|
with open(image_path, "rb") as fp:
|
||||||
data = fp.read()[14:]
|
data = fp.read()[14:]
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import BufrStubImagePlugin, Image
|
from PIL import BufrStubImagePlugin, Image, ImageFile
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
|
||||||
|
@ -50,30 +51,33 @@ def test_save(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_handler(tmp_path: Path) -> None:
|
def test_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler:
|
class TestHandler(ImageFile.StubHandler):
|
||||||
opened = False
|
opened = False
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
|
||||||
def open(self, im) -> None:
|
def open(self, im: ImageFile.StubImageFile) -> None:
|
||||||
self.opened = True
|
self.opened = True
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename) -> None:
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
handler = TestHandler()
|
handler = TestHandler()
|
||||||
BufrStubImagePlugin.register_handler(handler)
|
BufrStubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.bufr")
|
temp_file = str(tmp_path / "temp.bufr")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -53,6 +53,7 @@ def test_closed_file() -> None:
|
||||||
|
|
||||||
def test_seek_after_close() -> None:
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open("Tests/images/iss634.gif")
|
im = Image.open("Tests/images/iss634.gif")
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
tempfile = str(tmp_path / "temp.gif")
|
tempfile = str(tmp_path / "temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
b = BytesIO()
|
||||||
|
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
with Image.open(tempfile) as reloaded:
|
||||||
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
||||||
|
|
||||||
|
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
||||||
img = img.convert("L")
|
img = img.convert("L")
|
||||||
|
|
||||||
tempfile = str(tmp_path / "temp.gif")
|
tempfile = str(tmp_path / "temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
b = BytesIO()
|
||||||
|
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
with Image.open(tempfile) as reloaded:
|
||||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||||
|
|
||||||
|
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
||||||
assert rgb_img.getpixel((50, 50)) == circle
|
assert rgb_img.getpixel((50, 50)) == circle
|
||||||
|
|
||||||
# Check that frame transparency wasn't added unnecessarily
|
# Check that frame transparency wasn't added unnecessarily
|
||||||
assert img._frame_transparency is None
|
assert getattr(img, "_frame_transparency") is None
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import GribStubImagePlugin, Image
|
from PIL import GribStubImagePlugin, Image, ImageFile
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_handler(tmp_path: Path) -> None:
|
def test_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler:
|
class TestHandler(ImageFile.StubHandler):
|
||||||
opened = False
|
opened = False
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
GribStubImagePlugin.register_handler(handler)
|
GribStubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.grib")
|
temp_file = str(tmp_path / "temp.grib")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Hdf5StubImagePlugin, Image
|
from PIL import Hdf5StubImagePlugin, Image, ImageFile
|
||||||
|
|
||||||
TEST_FILE = "Tests/images/hdf5.h5"
|
TEST_FILE = "Tests/images/hdf5.h5"
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ def test_load() -> None:
|
||||||
def test_save() -> None:
|
def test_save() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
dummy_fp = None
|
dummy_fp = BytesIO()
|
||||||
dummy_filename = "dummy.filename"
|
dummy_filename = "dummy.filename"
|
||||||
|
|
||||||
# Act / Assert: stub cannot save without an implemented handler
|
# Act / Assert: stub cannot save without an implemented handler
|
||||||
|
@ -52,7 +53,7 @@ def test_save() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_handler(tmp_path: Path) -> None:
|
def test_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler:
|
class TestHandler(ImageFile.StubHandler):
|
||||||
opened = False
|
opened = False
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
@ -65,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
|
@ -72,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
Hdf5StubImagePlugin.register_handler(handler)
|
Hdf5StubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.h5")
|
temp_file = str(tmp_path / "temp.h5")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -171,7 +171,7 @@ class TestFileJpeg:
|
||||||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||||
)
|
)
|
||||||
def test_dpi(self, test_image_path: str) -> None:
|
def test_dpi(self, test_image_path: str) -> None:
|
||||||
def test(xdpi: int, ydpi: int | None = None):
|
def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
|
||||||
with Image.open(test_image_path) as im:
|
with Image.open(test_image_path) as im:
|
||||||
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
||||||
return im.info.get("dpi")
|
return im.info.get("dpi")
|
||||||
|
@ -443,7 +443,9 @@ class TestFileJpeg:
|
||||||
assert_image(im1, im2.mode, im2.size)
|
assert_image(im1, im2.mode, im2.size)
|
||||||
|
|
||||||
def test_subsampling(self) -> None:
|
def test_subsampling(self) -> None:
|
||||||
def getsampling(im: JpegImagePlugin.JpegImageFile):
|
def getsampling(
|
||||||
|
im: JpegImagePlugin.JpegImageFile,
|
||||||
|
) -> tuple[int, int, int, int, int, int]:
|
||||||
layer = im.layer
|
layer = im.layer
|
||||||
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]
|
||||||
|
|
||||||
|
@ -699,7 +701,7 @@ class TestFileJpeg:
|
||||||
def test_save_cjpeg(self, tmp_path: Path) -> None:
|
def test_save_cjpeg(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_FILE) as img:
|
with Image.open(TEST_FILE) as img:
|
||||||
tempfile = str(tmp_path / "temp.jpg")
|
tempfile = str(tmp_path / "temp.jpg")
|
||||||
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
|
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
|
||||||
# Default save quality is 75%, so a tiny bit of difference is alright
|
# Default save quality is 75%, so a tiny bit of difference is alright
|
||||||
assert_image_similar_tofile(img, tempfile, 17)
|
assert_image_similar_tofile(img, tempfile, 17)
|
||||||
|
|
||||||
|
@ -917,24 +919,25 @@ class TestFileJpeg:
|
||||||
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
|
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
|
||||||
assert im.info["icc_profile"] == b"profile"
|
assert im.info["icc_profile"] == b"profile"
|
||||||
|
|
||||||
def test_jpeg_magic_number(self) -> None:
|
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
size = 4097
|
size = 4097
|
||||||
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
|
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
|
||||||
buffer.max_pos = 0
|
max_pos = 0
|
||||||
orig_read = buffer.read
|
orig_read = buffer.read
|
||||||
|
|
||||||
def read(n=-1):
|
def read(n: int | None = -1) -> bytes:
|
||||||
|
nonlocal max_pos
|
||||||
res = orig_read(n)
|
res = orig_read(n)
|
||||||
buffer.max_pos = max(buffer.max_pos, buffer.tell())
|
max_pos = max(max_pos, buffer.tell())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
buffer.read = read
|
monkeypatch.setattr(buffer, "read", read)
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with Image.open(buffer):
|
with Image.open(buffer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Assert the entire file has not been read
|
# Assert the entire file has not been read
|
||||||
assert 0 < buffer.max_pos < size
|
assert 0 < max_pos < size
|
||||||
|
|
||||||
def test_getxmp(self) -> None:
|
def test_getxmp(self) -> None:
|
||||||
with Image.open("Tests/images/xmp_test.jpg") as im:
|
with Image.open("Tests/images/xmp_test.jpg") as im:
|
||||||
|
@ -945,6 +948,7 @@ class TestFileJpeg:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
@ -1029,8 +1033,10 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_repr_jpeg(self) -> None:
|
def test_repr_jpeg(self) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
b = im._repr_jpeg_()
|
||||||
|
assert b is not None
|
||||||
|
|
||||||
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
with Image.open(BytesIO(b)) as repr_jpeg:
|
||||||
assert repr_jpeg.format == "JPEG"
|
assert repr_jpeg.format == "JPEG"
|
||||||
assert_image_similar(im, repr_jpeg, 17)
|
assert_image_similar(im, repr_jpeg, 17)
|
||||||
|
|
||||||
|
|
|
@ -460,7 +460,7 @@ def test_plt_marker() -> None:
|
||||||
out.seek(length - 2, os.SEEK_CUR)
|
out.seek(length - 2, os.SEEK_CUR)
|
||||||
|
|
||||||
|
|
||||||
def test_9bit():
|
def test_9bit() -> None:
|
||||||
with Image.open("Tests/images/9bit.j2k") as im:
|
with Image.open("Tests/images/9bit.j2k") as im:
|
||||||
assert im.mode == "I;16"
|
assert im.mode == "I;16"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
|
@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_version(self) -> None:
|
def test_version(self) -> None:
|
||||||
version = features.version_codec("libtiff")
|
version = features.version_codec("libtiff")
|
||||||
assert version is not None
|
assert version is not None
|
||||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
assert re.search(r"\d+\.\d+\.\d+t?$", version)
|
||||||
|
|
||||||
def test_g4_tiff(self, tmp_path: Path) -> None:
|
def test_g4_tiff(self, tmp_path: Path) -> None:
|
||||||
"""Test the ordinary file path load path"""
|
"""Test the ordinary file path load path"""
|
||||||
|
|
|
@ -535,8 +535,10 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_repr_png(self) -> None:
|
def test_repr_png(self) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
b = im._repr_png_()
|
||||||
|
assert b is not None
|
||||||
|
|
||||||
with Image.open(BytesIO(im._repr_png_())) as repr_png:
|
with Image.open(BytesIO(b)) as repr_png:
|
||||||
assert repr_png.format == "PNG"
|
assert repr_png.format == "PNG"
|
||||||
assert_image_equal(im, repr_png)
|
assert_image_equal(im, repr_png)
|
||||||
|
|
||||||
|
@ -655,11 +657,12 @@ class TestFilePng:
|
||||||
png.call(cid, 0, 0)
|
png.call(cid, 0, 0)
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
def test_specify_bits(self, tmp_path: Path) -> None:
|
@pytest.mark.parametrize("save_all", (True, False))
|
||||||
|
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
|
|
||||||
out = str(tmp_path / "temp.png")
|
out = str(tmp_path / "temp.png")
|
||||||
im.save(out, bits=4)
|
im.save(out, bits=4, save_all=save_all)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert len(reloaded.png.im_palette[1]) == 48
|
assert len(reloaded.png.im_palette[1]) == 48
|
||||||
|
@ -683,6 +686,7 @@ class TestFilePng:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
@ -767,16 +771,12 @@ class TestFilePng:
|
||||||
def test_save_stdout(self, buffer: bool) -> None:
|
def test_save_stdout(self, buffer: bool) -> None:
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
|
||||||
|
|
||||||
class MyStdOut:
|
class MyStdOut:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
|
|
||||||
with Image.open(TEST_PNG_FILE) as im:
|
with Image.open(TEST_PNG_FILE) as im:
|
||||||
im.save(sys.stdout, "PNG")
|
im.save(sys.stdout, "PNG")
|
||||||
|
@ -784,7 +784,7 @@ class TestFilePng:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
with Image.open(mystdout) as reloaded:
|
with Image.open(mystdout) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||||
|
|
|
@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
|
||||||
def test_save_stdout(buffer: bool) -> None:
|
def test_save_stdout(buffer: bool) -> None:
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
|
||||||
|
|
||||||
class MyStdOut:
|
class MyStdOut:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
|
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.save(sys.stdout, "PPM")
|
im.save(sys.stdout, "PPM")
|
||||||
|
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
with Image.open(mystdout) as reloaded:
|
with Image.open(mystdout) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded, TEST_FILE)
|
assert_image_equal_tofile(reloaded, TEST_FILE)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
|
from PIL import Image, PsdImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||||
|
|
||||||
|
@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file,raises",
|
"test_file,raises",
|
||||||
[
|
[
|
||||||
(
|
|
||||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
|
||||||
UnidentifiedImageError,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
|
||||||
UnidentifiedImageError,
|
|
||||||
),
|
|
||||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file: str, raises) -> None:
|
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with pytest.raises(raises):
|
with pytest.raises(raises):
|
||||||
with Image.open(f):
|
with Image.open(f):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_file",
|
||||||
|
[
|
||||||
|
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||||
|
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_layer_crashes(test_file: str) -> None:
|
||||||
|
with open(test_file, "rb") as f:
|
||||||
|
with Image.open(f) as im:
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
im.layers
|
||||||
|
|
|
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
|
||||||
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert img_list is not None
|
||||||
assert len(img_list) == 1
|
assert len(img_list) == 1
|
||||||
assert isinstance(img_list[0], Image.Image)
|
assert isinstance(img_list[0], Image.Image)
|
||||||
assert img_list[0].size == (128, 128)
|
assert img_list[0].size == (128, 128)
|
||||||
|
|
|
@ -113,14 +113,14 @@ class TestFileTiff:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||||
|
|
||||||
def test_seek_too_large(self):
|
def test_seek_too_large(self) -> None:
|
||||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||||
Image.open("Tests/images/seek_too_large.tif")
|
Image.open("Tests/images/seek_too_large.tif")
|
||||||
|
|
||||||
def test_set_legacy_api(self) -> None:
|
def test_set_legacy_api(self) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
ifd.legacy_api = None
|
ifd.legacy_api = False
|
||||||
assert str(e.value) == "Not allowing setting of legacy api"
|
assert str(e.value) == "Not allowing setting of legacy api"
|
||||||
|
|
||||||
def test_xyres_tiff(self) -> None:
|
def test_xyres_tiff(self) -> None:
|
||||||
|
@ -621,6 +621,19 @@ class TestFileTiff:
|
||||||
|
|
||||||
assert_image_equal_tofile(im, tmpfile)
|
assert_image_equal_tofile(im, tmpfile)
|
||||||
|
|
||||||
|
def test_iptc(self, tmp_path: Path) -> None:
|
||||||
|
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
im = hopper()
|
||||||
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
ifd[33723] = 1
|
||||||
|
ifd.tagtype[33723] = 4
|
||||||
|
im.tag_v2 = ifd
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
with Image.open(outfile) as im:
|
||||||
|
assert 33723 not in im.tag_v2
|
||||||
|
|
||||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
@ -759,6 +772,7 @@ class TestFileTiff:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
||||||
|
|
||||||
from .helper import assert_deep_equal, hopper
|
from .helper import assert_deep_equal, hopper
|
||||||
|
|
||||||
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
TAG_IDS: dict[str, int] = {
|
||||||
|
info.name: info.value
|
||||||
|
for info in TiffTags.TAGS_V2.values()
|
||||||
|
if info.value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_rt_metadata(tmp_path: Path) -> None:
|
def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
|
@ -411,8 +415,8 @@ def test_empty_values() -> None:
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||||
info.load(data)
|
info.load(data)
|
||||||
# Should not raise ValueError.
|
# Should not raise ValueError.
|
||||||
info = dict(info)
|
info_dict = dict(info)
|
||||||
assert 33432 in info
|
assert 33432 in info_dict
|
||||||
|
|
||||||
|
|
||||||
def test_photoshop_info(tmp_path: Path) -> None:
|
def test_photoshop_info(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -70,7 +71,9 @@ class TestFileWebp:
|
||||||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||||
|
|
||||||
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
|
def _roundtrip(
|
||||||
|
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
|
||||||
|
) -> None:
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
|
||||||
hopper(mode).save(temp_file, **args)
|
hopper(mode).save(temp_file, **args)
|
||||||
|
@ -198,7 +201,9 @@ class TestFileWebp:
|
||||||
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
||||||
)
|
)
|
||||||
@skip_unless_feature("webp_anim")
|
@skip_unless_feature("webp_anim")
|
||||||
def test_invalid_background(self, background, tmp_path: Path) -> None:
|
def test_invalid_background(
|
||||||
|
self, background: int | tuple[int, ...], tmp_path: Path
|
||||||
|
) -> None:
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -69,7 +69,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
are visually similar to the originals.
|
are visually similar to the originals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(temp_file) -> None:
|
def check(temp_file: str) -> None:
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,7 @@ def test_getxmp() -> None:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
assert (
|
assert (
|
||||||
im.getxmp()["xmpmeta"]["xmptk"]
|
im.getxmp()["xmpmeta"]["xmptk"]
|
||||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, WmfImagePlugin
|
from PIL import Image, ImageFile, WmfImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_similar_tofile, hopper
|
from .helper import assert_image_similar_tofile, hopper
|
||||||
|
|
||||||
|
@ -34,10 +35,13 @@ def test_load() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_register_handler(tmp_path: Path) -> None:
|
def test_register_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler:
|
class TestHandler(ImageFile.StubHandler):
|
||||||
methodCalled = False
|
methodCalled = False
|
||||||
|
|
||||||
def save(self, im, fp, filename) -> None:
|
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||||
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.methodCalled = True
|
self.methodCalled = True
|
||||||
|
|
||||||
handler = TestHandler()
|
handler = TestHandler()
|
||||||
|
@ -70,7 +74,7 @@ def test_load_set_dpi() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
||||||
def test_save(ext, tmp_path: Path) -> None:
|
def test_save(ext: str, tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
tmpfile = str(tmp_path / ("temp" + ext))
|
tmpfile = str(tmp_path / ("temp" + ext))
|
||||||
|
|
|
@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
||||||
|
|
||||||
def test_leak(self) -> None:
|
def test_leak(self) -> None:
|
||||||
if features.check_module("freetype2"):
|
if features.check_module("freetype2"):
|
||||||
ImageFont.core = _util.DeferredError(ImportError)
|
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||||
try:
|
try:
|
||||||
default_font = ImageFont.load_default()
|
default_font = ImageFont.load_default()
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -8,7 +8,7 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import IO, Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ class TestImage:
|
||||||
|
|
||||||
def test_stringio(self) -> None:
|
def test_stringio(self) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
with Image.open(io.StringIO()):
|
with Image.open(io.StringIO()): # type: ignore[arg-type]
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_pathlib(self, tmp_path: Path) -> None:
|
def test_pathlib(self, tmp_path: Path) -> None:
|
||||||
|
@ -175,11 +175,19 @@ class TestImage:
|
||||||
def test_fp_name(self, tmp_path: Path) -> None:
|
def test_fp_name(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = str(tmp_path / "temp.jpg")
|
||||||
|
|
||||||
class FP:
|
class FP(io.BytesIO):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def write(self, b: bytes) -> None:
|
if sys.version_info >= (3, 12):
|
||||||
pass
|
from collections.abc import Buffer
|
||||||
|
|
||||||
|
def write(self, data: Buffer) -> int:
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def write(self, data: Any) -> int:
|
||||||
|
return len(data)
|
||||||
|
|
||||||
fp = FP()
|
fp = FP()
|
||||||
fp.name = temp_file
|
fp.name = temp_file
|
||||||
|
@ -393,13 +401,13 @@ class TestImage:
|
||||||
|
|
||||||
# errors
|
# errors
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, "invalid source")
|
source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), "invalid destination")
|
source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, 0)
|
source.alpha_composite(over, 0) # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), 0)
|
source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), (0, -1))
|
source.alpha_composite(over, (0, 0), (0, -1))
|
||||||
|
|
||||||
|
@ -909,6 +917,10 @@ 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_empty_xmp(self) -> None:
|
||||||
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
|
assert im.getxmp() == {}
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
|
|
|
@ -259,6 +259,7 @@ class TestCffi(AccessTest):
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
access = PyAccess.new(im, False)
|
access = PyAccess.new(im, False)
|
||||||
|
assert access is not None
|
||||||
|
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
for x in range(0, w, 10):
|
for x in range(0, w, 10):
|
||||||
|
@ -289,6 +290,7 @@ class TestCffi(AccessTest):
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
access = PyAccess.new(im, False)
|
access = PyAccess.new(im, False)
|
||||||
|
assert access is not None
|
||||||
|
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
for x in range(0, w, 10):
|
for x in range(0, w, 10):
|
||||||
|
@ -299,6 +301,8 @@ class TestCffi(AccessTest):
|
||||||
# Attempt to set the value on a read-only image
|
# Attempt to set the value on a read-only image
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
access = PyAccess.new(im, True)
|
access = PyAccess.new(im, True)
|
||||||
|
assert access is not None
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
access[(0, 0)] = color
|
access[(0, 0)] = color
|
||||||
|
|
||||||
|
@ -341,6 +345,8 @@ class TestCffi(AccessTest):
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
access = PyAccess.new(im, False)
|
access = PyAccess.new(im, False)
|
||||||
|
assert access is not None
|
||||||
|
|
||||||
access.putpixel((0, 0), color)
|
access.putpixel((0, 0), color)
|
||||||
|
|
||||||
if len(color) == 3:
|
if len(color) == 3:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||||
|
|
||||||
im = hopper().resize((128, 100))
|
im = hopper().resize((128, 100))
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
|
||||||
def test_toarray() -> None:
|
def test_toarray() -> None:
|
||||||
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
||||||
ai = numpy.array(im.convert(mode))
|
ai = numpy.array(im.convert(mode))
|
||||||
return ai.shape, ai.dtype.str, ai.nbytes
|
return ai.shape, ai.dtype.str, ai.nbytes
|
||||||
|
|
||||||
def test_with_dtype(dtype) -> None:
|
def test_with_dtype(dtype: npt.DTypeLike) -> None:
|
||||||
ai = numpy.array(im, dtype=dtype)
|
ai = numpy.array(im, dtype=dtype)
|
||||||
assert ai.dtype == dtype
|
assert ai.dtype == dtype
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@ def draft_roundtrip(
|
||||||
im = Image.new(in_mode, in_size)
|
im = Image.new(in_mode, in_size)
|
||||||
data = tostring(im, "JPEG")
|
data = tostring(im, "JPEG")
|
||||||
im = fromstring(data)
|
im = fromstring(data)
|
||||||
mode, box = im.draft(req_mode, req_size)
|
result = im.draft(req_mode, req_size)
|
||||||
|
assert result is not None
|
||||||
|
box = result[1]
|
||||||
scale, _ = im.decoderconfig
|
scale, _ = im.decoderconfig
|
||||||
assert box[:2] == (0, 0)
|
assert box[:2] == (0, 0)
|
||||||
assert (im.width - scale) < box[2] <= im.width
|
assert (im.width - scale) < box[2] <= im.width
|
||||||
|
|
|
@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
|
||||||
builtin_filter = ImageFilter.BuiltinFilter()
|
builtin_filter = ImageFilter.BuiltinFilter()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
builtin_filter.filter(hopper("P"))
|
builtin_filter.filter(hopper("P").im)
|
||||||
|
|
||||||
|
|
||||||
def test_kernel_not_enough_coefficients() -> None:
|
def test_kernel_not_enough_coefficients() -> None:
|
||||||
|
|
|
@ -68,7 +68,11 @@ def test_sanity() -> None:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_properties(
|
def test_properties(
|
||||||
mode, expected_base, expected_type, expected_bands, expected_band_names
|
mode: str,
|
||||||
|
expected_base: str,
|
||||||
|
expected_type: str,
|
||||||
|
expected_bands: int,
|
||||||
|
expected_band_names: tuple[str, ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
assert Image.getmodebase(mode) == expected_base
|
assert Image.getmodebase(mode) == expected_base
|
||||||
assert Image.getmodetype(mode) == expected_type
|
assert Image.getmodetype(mode) == expected_type
|
||||||
|
|
|
@ -338,3 +338,8 @@ class TestImagingPaste:
|
||||||
|
|
||||||
im.copy().paste(im2)
|
im.copy().paste(im2)
|
||||||
im.copy().paste(im2, (0, 0))
|
im.copy().paste(im2, (0, 0))
|
||||||
|
|
||||||
|
def test_incorrect_abbreviated_form(self) -> None:
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.paste(im, im, im)
|
||||||
|
|
|
@ -61,4 +61,4 @@ def test_f_lut() -> None:
|
||||||
def test_f_mode() -> None:
|
def test_f_mode() -> None:
|
||||||
im = hopper("F")
|
im = hopper("F")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.point(None)
|
im.point([])
|
||||||
|
|
|
@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
|
||||||
(
|
(
|
||||||
("RGBA", (1, 2, 3, 4)),
|
("RGBA", (1, 2, 3, 4)),
|
||||||
("RGBAX", (1, 2, 3, 4, 0)),
|
("RGBAX", (1, 2, 3, 4, 0)),
|
||||||
|
("ARGB", (4, 1, 2, 3)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
||||||
|
|
|
@ -98,7 +98,7 @@ def test_quantize_dither_diff() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
|
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
|
||||||
)
|
)
|
||||||
def test_quantize_kmeans(method) -> None:
|
def test_quantize_kmeans(method: Image.Quantize) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
no_kmeans = im.quantize(kmeans=0, method=method)
|
no_kmeans = im.quantize(kmeans=0, method=method)
|
||||||
kmeans = im.quantize(kmeans=1, method=method)
|
kmeans = im.quantize(kmeans=1, method=method)
|
||||||
|
|
|
@ -56,10 +56,12 @@ def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) ->
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
|
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
|
||||||
)
|
)
|
||||||
def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None:
|
def test_args_factor_error(
|
||||||
|
size: float | tuple[int, int], expected_error: type[Exception]
|
||||||
|
) -> None:
|
||||||
im = Image.new("L", (10, 10))
|
im = Image.new("L", (10, 10))
|
||||||
with pytest.raises(expected_error):
|
with pytest.raises(expected_error):
|
||||||
im.reduce(size)
|
im.reduce(size) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -86,10 +88,12 @@ def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) ->
|
||||||
((5, 0, 5, 10), ValueError),
|
((5, 0, 5, 10), ValueError),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None:
|
def test_args_box_error(
|
||||||
|
size: str | tuple[int, int, int, int], expected_error: type[Exception]
|
||||||
|
) -> None:
|
||||||
im = Image.new("L", (10, 10))
|
im = Image.new("L", (10, 10))
|
||||||
with pytest.raises(expected_error):
|
with pytest.raises(expected_error):
|
||||||
im.reduce(2, size).size
|
im.reduce(2, size).size # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
|
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
|
||||||
|
|
|
@ -445,7 +445,7 @@ class TestCoreResampleBox:
|
||||||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||||
|
|
||||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
||||||
im.resize((32, 32), resample, (im.width, im.height))
|
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't be negative"):
|
with pytest.raises(ValueError, match="can't be negative"):
|
||||||
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
||||||
|
|
|
@ -124,8 +124,8 @@ def test_fastpath_translate() -> None:
|
||||||
def test_center() -> None:
|
def test_center() -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
rotate(im, im.mode, 45, center=(0, 0))
|
rotate(im, im.mode, 45, center=(0, 0))
|
||||||
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0))
|
rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0))
|
||||||
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0))
|
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0))
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_no_fill() -> None:
|
def test_rotate_no_fill() -> None:
|
||||||
|
|
|
@ -16,7 +16,7 @@ from .helper import (
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
assert im.thumbnail((100, 100)) is None
|
im.thumbnail((100, 100))
|
||||||
|
|
||||||
assert im.size == (100, 100)
|
assert im.size == (100, 100)
|
||||||
|
|
||||||
|
@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None:
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
draft = im.draft
|
draft = im.draft
|
||||||
|
|
||||||
def im_draft(mode: str, size: tuple[int, int]):
|
def im_draft(
|
||||||
|
mode: str, size: tuple[int, int]
|
||||||
|
) -> tuple[str, tuple[int, int, float, float]] | None:
|
||||||
result = draft(mode, size)
|
result = draft(mode, size)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_flags() -> None:
|
def test_flags() -> None:
|
||||||
assert ImageCms.Flags.NONE == 0
|
assert ImageCms.Flags.NONE.value == 0
|
||||||
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
||||||
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
||||||
|
|
||||||
|
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
|
||||||
for delta in nine_grid_deltas:
|
for delta in nine_grid_deltas:
|
||||||
channel_data.paste(
|
channel_data.paste(
|
||||||
channel_pattern,
|
channel_pattern,
|
||||||
tuple(
|
(
|
||||||
paste_offset[c] + delta[c] * channel_pattern.size[c]
|
paste_offset[0] + delta[0] * channel_pattern.size[0],
|
||||||
for c in range(2)
|
paste_offset[1] + delta[1] * channel_pattern.size[1],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
chans.append(channel_data)
|
chans.append(channel_data)
|
||||||
|
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
|
||||||
# convert with and without AUX data, test colors are equal
|
# convert with and without AUX data, test colors are equal
|
||||||
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
||||||
source_profile = ImageCms.createProfile(src_colorSpace)
|
source_profile = ImageCms.createProfile(src_colorSpace)
|
||||||
destination_profile = ImageCms.createProfile(dst_format[1])
|
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
|
||||||
|
destination_profile = ImageCms.createProfile(dst_colorSpace)
|
||||||
source_image = src_format[3]
|
source_image = src_format[3]
|
||||||
test_transform = ImageCms.buildTransform(
|
test_transform = ImageCms.buildTransform(
|
||||||
source_profile,
|
source_profile,
|
||||||
|
|
|
@ -448,6 +448,7 @@ def test_shape1() -> None:
|
||||||
x3, y3 = 95, 5
|
x3, y3 = 95, 5
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -469,6 +470,7 @@ def test_shape2() -> None:
|
||||||
x3, y3 = 5, 95
|
x3, y3 = 5, 95
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -487,6 +489,7 @@ def test_transform() -> None:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.line(0, 0)
|
s.line(0, 0)
|
||||||
s.transform((0, 0, 0, 0, 0, 0))
|
s.transform((0, 0, 0, 0, 0, 0))
|
||||||
|
@ -913,7 +916,12 @@ def test_rounded_rectangle_translucent(
|
||||||
def test_floodfill(bbox: Coords) -> None:
|
def test_floodfill(bbox: Coords) -> None:
|
||||||
red = ImageColor.getrgb("red")
|
red = ImageColor.getrgb("red")
|
||||||
|
|
||||||
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
|
mode_values: list[tuple[str, int | tuple[int, ...]]] = [
|
||||||
|
("L", 1),
|
||||||
|
("RGBA", (255, 0, 0, 0)),
|
||||||
|
("RGB", red),
|
||||||
|
]
|
||||||
|
for mode, value in mode_values:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new(mode, (W, H))
|
im = Image.new(mode, (W, H))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
@ -1429,6 +1437,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
||||||
x2, y2 = 95, 50
|
x2, y2 = 95, 50
|
||||||
x3, y3 = 95, 5
|
x3, y3 = 95, 5
|
||||||
|
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -1467,7 +1476,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
||||||
(4, "square", {}),
|
(4, "square", {}),
|
||||||
(8, "regular_octagon", {}),
|
(8, "regular_octagon", {}),
|
||||||
(4, "square_rotate_45", {"rotation": 45}),
|
(4, "square_rotate_45", {"rotation": 45}),
|
||||||
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
|
(3, "triangle_width", {"outline": "yellow", "width": 5}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_draw_regular_polygon(
|
def test_draw_regular_polygon(
|
||||||
|
@ -1477,7 +1486,10 @@ def test_draw_regular_polygon(
|
||||||
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
bounding_circle = ((W // 2, H // 2), 25)
|
bounding_circle = ((W // 2, H // 2), 25)
|
||||||
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
|
rotation = int(args.get("rotation", 0))
|
||||||
|
outline = args.get("outline")
|
||||||
|
width = int(args.get("width", 1))
|
||||||
|
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
|
||||||
assert_image_equal_tofile(im, filename)
|
assert_image_equal_tofile(im, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1562,10 +1574,14 @@ def test_compute_regular_polygon_vertices(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compute_regular_polygon_vertices_input_error_handling(
|
def test_compute_regular_polygon_vertices_input_error_handling(
|
||||||
n_sides, bounding_circle, rotation, expected_error, error_message
|
n_sides: int,
|
||||||
|
bounding_circle: int | tuple[int | tuple[int] | str, ...],
|
||||||
|
rotation: int | str,
|
||||||
|
expected_error: type[Exception],
|
||||||
|
error_message: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
with pytest.raises(expected_error) as e:
|
with pytest.raises(expected_error) as e:
|
||||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
||||||
assert str(e.value) == error_message
|
assert str(e.value) == error_message
|
||||||
|
|
||||||
|
|
||||||
|
@ -1624,3 +1640,8 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
|
||||||
draw.rectangle(xy)
|
draw.rectangle(xy)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
draw.rounded_rectangle(xy)
|
draw.rounded_rectangle(xy)
|
||||||
|
|
||||||
|
|
||||||
|
def test_getdraw() -> None:
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
ImageDraw.getdraw(None, [])
|
||||||
|
|
|
@ -51,9 +51,18 @@ def test_sanity() -> None:
|
||||||
pen = ImageDraw2.Pen("blue", width=7)
|
pen = ImageDraw2.Pen("blue", width=7)
|
||||||
draw.line(list(range(10)), pen)
|
draw.line(list(range(10)), pen)
|
||||||
|
|
||||||
draw, handler = ImageDraw.getdraw(im)
|
draw2, handler = ImageDraw.getdraw(im)
|
||||||
|
assert draw2 is not None
|
||||||
pen = ImageDraw2.Pen("blue", width=7)
|
pen = ImageDraw2.Pen("blue", width=7)
|
||||||
draw.line(list(range(10)), pen)
|
draw2.line(list(range(10)), pen)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mode() -> None:
|
||||||
|
draw = ImageDraw2.Draw("L", (1, 1))
|
||||||
|
assert draw.image.mode == "L"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageDraw2.Draw("L")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
|
|
|
@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
super().__init__(mode, *args)
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||||
# eof
|
# eof
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
||||||
|
|
||||||
super().__init__(mode, *args)
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def encode(self, buffer):
|
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||||
return 1, 1, b""
|
return 1, 1, b""
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
|
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||||
)
|
)
|
||||||
assert MockPyEncoder.last.cleanup_called
|
last: MockPyEncoder | None = MockPyEncoder.last
|
||||||
|
assert last
|
||||||
|
assert last.cleanup_called
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
|
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
|
||||||
def test_encode(self) -> None:
|
def test_encode(self) -> None:
|
||||||
encoder = ImageFile.PyEncoder(None)
|
encoder = ImageFile.PyEncoder(None)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
encoder.encode(None)
|
encoder.encode(0)
|
||||||
|
|
||||||
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
||||||
assert bytes_consumed == 0
|
assert bytes_consumed == 0
|
||||||
|
|
|
@ -209,7 +209,7 @@ def test_getlength(
|
||||||
assert length == length_raqm
|
assert length == length_raqm
|
||||||
|
|
||||||
|
|
||||||
def test_float_size() -> None:
|
def test_float_size(layout_engine: ImageFont.Layout) -> None:
|
||||||
lengths = []
|
lengths = []
|
||||||
for size in (48, 48.5, 49):
|
for size in (48, 48.5, 49):
|
||||||
f = ImageFont.truetype(
|
f = ImageFont.truetype(
|
||||||
|
@ -224,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
line_spacing = font.getbbox("A")[3] + 4
|
line_spacing = font.getbbox("A")[3] + 4
|
||||||
lines = TEST_TEXT.split("\n")
|
lines = TEST_TEXT.split("\n")
|
||||||
y = 0
|
y: float = 0
|
||||||
for line in lines:
|
for line in lines:
|
||||||
draw.text((0, y), line, font=font)
|
draw.text((0, y), line, font=font)
|
||||||
y += line_spacing
|
y += line_spacing
|
||||||
|
@ -494,8 +494,8 @@ def test_default_font() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
|
||||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
|
||||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||||
|
|
||||||
|
|
||||||
|
@ -548,7 +548,7 @@ def test_find_font(
|
||||||
|
|
||||||
def loadable_font(
|
def loadable_font(
|
||||||
filepath: str, size: int, index: int, encoding: str, *args: Any
|
filepath: str, size: int, index: int, encoding: str, *args: Any
|
||||||
):
|
) -> ImageFont.FreeTypeFont:
|
||||||
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
||||||
if filepath == path_to_fake:
|
if filepath == path_to_fake:
|
||||||
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
||||||
|
@ -564,6 +564,7 @@ def test_find_font(
|
||||||
# catching syntax like errors
|
# catching syntax like errors
|
||||||
monkeypatch.setattr(sys, "platform", platform)
|
monkeypatch.setattr(sys, "platform", platform)
|
||||||
if platform == "linux":
|
if platform == "linux":
|
||||||
|
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
||||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
||||||
|
|
||||||
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
||||||
|
@ -1096,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
imagefont.getmask("A" * 1_000_001)
|
imagefont.getmask("A" * 1_000_001)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
|
||||||
|
assert font.getlength(b"test") == font.getlength("test")
|
||||||
|
|
||||||
|
assert font.getbbox(b"test") == font.getbbox("test")
|
||||||
|
|
||||||
|
assert_image_equal(
|
||||||
|
Image.Image()._new(font.getmask(b"test")),
|
||||||
|
Image.Image()._new(font.getmask("test")),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_image_equal(
|
||||||
|
Image.Image()._new(font.getmask2(b"test")[0]),
|
||||||
|
Image.Image()._new(font.getmask2("test")[0]),
|
||||||
|
)
|
||||||
|
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file",
|
||||||
[
|
[
|
||||||
|
|
|
@ -14,7 +14,7 @@ original_core = ImageFont.core
|
||||||
|
|
||||||
def setup_module() -> None:
|
def setup_module() -> None:
|
||||||
if features.check_module("freetype2"):
|
if features.check_module("freetype2"):
|
||||||
ImageFont.core = _util.DeferredError(ImportError)
|
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||||
|
|
||||||
|
|
||||||
def teardown_module() -> None:
|
def teardown_module() -> None:
|
||||||
|
@ -76,3 +76,8 @@ def test_oom() -> None:
|
||||||
font = ImageFont.ImageFont()
|
font = ImageFont.ImageFont()
|
||||||
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
|
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
|
||||||
font.getmask("A" * 1_000_000)
|
font.getmask("A" * 1_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freetypefont_without_freetype() -> None:
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
||||||
|
|
|
@ -89,6 +89,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, list)
|
||||||
assert len(im) == 1
|
assert len(im) == 1
|
||||||
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
||||||
|
|
||||||
|
@ -105,6 +106,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, Image.Image)
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
@ -120,6 +122,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
with open(image_path, "rb") as fp:
|
with open(image_path, "rb") as fp:
|
||||||
subprocess.call(["wl-copy"], stdin=fp)
|
subprocess.call(["wl-copy"], stdin=fp)
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, Image.Image)
|
||||||
assert_image_equal_tofile(im, image_path)
|
assert_image_equal_tofile(im, image_path)
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
|
@ -454,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
|
||||||
# Test the cutoff argument of autocontrast
|
# Test the cutoff argument of autocontrast
|
||||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||||
|
|
||||||
def autocontrast(cutoff: int | tuple[int, int]):
|
def autocontrast(cutoff: int | tuple[int, int]) -> list[int]:
|
||||||
return ImageOps.autocontrast(img, cutoff).histogram()
|
return ImageOps.autocontrast(img, cutoff).histogram()
|
||||||
|
|
||||||
assert autocontrast(10) == autocontrast((10, 10))
|
assert autocontrast(10) == autocontrast((10, 10))
|
||||||
|
|
|
@ -58,7 +58,6 @@ def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
||||||
blur = ImageFilter.GaussianBlur
|
blur = ImageFilter.GaussianBlur
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.convert("1").filter(blur)
|
im.convert("1").filter(blur)
|
||||||
blur(im.convert("L"))
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.convert("I").filter(blur)
|
im.convert("I").filter(blur)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -102,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
||||||
assert i.im.getpixel((x, y))[c] >= 250
|
assert i.im.getpixel((x, y))[c] >= 250
|
||||||
# Fuzzy match.
|
# Fuzzy match.
|
||||||
|
|
||||||
def gp(x, y):
|
def gp(x: int, y: int) -> tuple[int, ...]:
|
||||||
return i.im.getpixel((x, y))
|
return i.im.getpixel((x, y))
|
||||||
|
|
||||||
assert 236 <= gp(7, 4)[0] <= 239
|
assert 236 <= gp(7, 4)[0] <= 239
|
||||||
|
|
|
@ -45,7 +45,7 @@ def test_getcolor() -> None:
|
||||||
|
|
||||||
# Test unknown color specifier
|
# Test unknown color specifier
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
palette.getcolor("unknown")
|
palette.getcolor("unknown") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
def test_getcolor_rgba_color_rgb_palette() -> None:
|
def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||||
|
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
|
||||||
|
|
||||||
palette.save(f)
|
palette.save(f)
|
||||||
|
|
||||||
p = ImagePalette.load(f)
|
lut = ImagePalette.load(f)
|
||||||
|
|
||||||
# load returns raw palette information
|
# load returns raw palette information
|
||||||
assert len(p[0]) == 768
|
assert len(lut[0]) == 768
|
||||||
assert p[1] == "RGB"
|
assert lut[1] == "RGB"
|
||||||
|
|
||||||
p = ImagePalette.raw(p[1], p[0])
|
p = ImagePalette.raw(lut[1], lut[0])
|
||||||
assert isinstance(p, ImagePalette.ImagePalette)
|
assert isinstance(p, ImagePalette.ImagePalette)
|
||||||
assert p.palette == palette.tobytes()
|
assert p.palette == palette.tobytes()
|
||||||
|
|
||||||
|
|
|
@ -41,13 +41,8 @@ def test_rgb() -> None:
|
||||||
checkrgb(0, 0, 255)
|
checkrgb(0, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_image() -> None:
|
@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
|
||||||
modes = ["1", "RGB", "RGBA", "L", "P"]
|
def test_image(mode: str) -> None:
|
||||||
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
|
|
||||||
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
|
|
||||||
modes.append("I;16")
|
|
||||||
|
|
||||||
for mode in modes:
|
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||||
if mode not in ("RGB", "RGBA"):
|
if mode not in ("RGB", "RGBA"):
|
||||||
|
|
|
@ -45,21 +45,22 @@ if is_win32():
|
||||||
memcpy = ctypes.cdll.msvcrt.memcpy
|
memcpy = ctypes.cdll.msvcrt.memcpy
|
||||||
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
||||||
|
|
||||||
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
|
windll = getattr(ctypes, "windll")
|
||||||
|
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
|
||||||
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
||||||
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
||||||
|
|
||||||
DeleteDC = ctypes.windll.gdi32.DeleteDC
|
DeleteDC = windll.gdi32.DeleteDC
|
||||||
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
||||||
|
|
||||||
SelectObject = ctypes.windll.gdi32.SelectObject
|
SelectObject = windll.gdi32.SelectObject
|
||||||
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
||||||
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
||||||
|
|
||||||
DeleteObject = ctypes.windll.gdi32.DeleteObject
|
DeleteObject = windll.gdi32.DeleteObject
|
||||||
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
||||||
|
|
||||||
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
|
CreateDIBSection = windll.gdi32.CreateDIBSection
|
||||||
CreateDIBSection.argtypes = [
|
CreateDIBSection.argtypes = [
|
||||||
ctypes.wintypes.HDC,
|
ctypes.wintypes.HDC,
|
||||||
ctypes.c_void_p,
|
ctypes.c_void_p,
|
||||||
|
@ -70,7 +71,7 @@ if is_win32():
|
||||||
]
|
]
|
||||||
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
|
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
|
||||||
|
|
||||||
def serialize_dib(bi, pixels) -> bytearray:
|
def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
|
||||||
bf = BITMAPFILEHEADER()
|
bf = BITMAPFILEHEADER()
|
||||||
bf.bfType = 0x4D42
|
bf.bfType = 0x4D42
|
||||||
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
|
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
|
||||||
|
|
|
@ -11,7 +11,7 @@ import pytest
|
||||||
"args, report",
|
"args, report",
|
||||||
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
|
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
|
||||||
)
|
)
|
||||||
def test_main(args, report) -> None:
|
def test_main(args: list[str], report: bool) -> None:
|
||||||
args = [sys.executable, "-m"] + args
|
args = [sys.executable, "-m"] + args
|
||||||
out = subprocess.check_output(args).decode("utf-8")
|
out = subprocess.check_output(args).decode("utf-8")
|
||||||
lines = out.splitlines()
|
lines = out.splitlines()
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, _typing
|
||||||
|
|
||||||
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
|
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import numpy
|
||||||
|
import numpy.typing as npt
|
||||||
|
else:
|
||||||
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||||
|
|
||||||
TEST_IMAGE_SIZE = (10, 10)
|
TEST_IMAGE_SIZE = (10, 10)
|
||||||
|
|
||||||
|
|
||||||
def test_numpy_to_image() -> None:
|
def test_numpy_to_image() -> None:
|
||||||
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
|
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||||
if bands == 1:
|
if bands == 1:
|
||||||
if boolean:
|
if boolean:
|
||||||
data = [0, 255] * 50
|
data = [0, 255] * 50
|
||||||
|
@ -99,14 +104,14 @@ def test_1d_array() -> None:
|
||||||
assert_image(Image.fromarray(a), "L", (1, 5))
|
assert_image(Image.fromarray(a), "L", (1, 5))
|
||||||
|
|
||||||
|
|
||||||
def _test_img_equals_nparray(img: Image.Image, np) -> None:
|
def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
|
||||||
assert len(np.shape) >= 2
|
assert len(np_img.shape) >= 2
|
||||||
np_size = np.shape[1], np.shape[0]
|
np_size = np_img.shape[1], np_img.shape[0]
|
||||||
assert img.size == np_size
|
assert img.size == np_size
|
||||||
px = img.load()
|
px = img.load()
|
||||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||||
assert_deep_equal(px[x, y], np[y, x])
|
assert_deep_equal(px[x, y], np_img[y, x])
|
||||||
|
|
||||||
|
|
||||||
def test_16bit() -> None:
|
def test_16bit() -> None:
|
||||||
|
@ -157,7 +162,7 @@ def test_save_tiff_uint16() -> None:
|
||||||
("HSV", numpy.uint8),
|
("HSV", numpy.uint8),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_to_array(mode: str, dtype) -> None:
|
def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
|
||||||
img = hopper(mode)
|
img = hopper(mode)
|
||||||
|
|
||||||
# Resize to non-square
|
# Resize to non-square
|
||||||
|
@ -207,7 +212,7 @@ def test_putdata() -> None:
|
||||||
numpy.float64,
|
numpy.float64,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_roundtrip_eye(dtype) -> None:
|
def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
|
||||||
arr = numpy.eye(10, dtype=dtype)
|
arr = numpy.eye(10, dtype=dtype)
|
||||||
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
||||||
|
|
||||||
|
|
|
@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
|
||||||
|
|
||||||
class MyStdOut:
|
class MyStdOut:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
|
|
||||||
ps = PSDraw.PSDraw()
|
ps = PSDraw.PSDraw()
|
||||||
_create_document(ps)
|
_create_document(ps)
|
||||||
|
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
assert mystdout.getvalue() != b""
|
assert mystdout.getvalue() != b""
|
||||||
|
|
|
@ -46,7 +46,7 @@ def roundtrip(expected: Image.Image) -> None:
|
||||||
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
|
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
|
||||||
def test_sanity(tmp_path: Path) -> None:
|
def test_sanity(tmp_path: Path) -> None:
|
||||||
# Segfault test
|
# Segfault test
|
||||||
app = QApplication([])
|
app: QApplication | None = QApplication([])
|
||||||
ex = Example()
|
ex = Example()
|
||||||
assert app # Silence warning
|
assert app # Silence warning
|
||||||
assert ex # Silence warning
|
assert ex # Silence warning
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import IO, Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -22,11 +23,11 @@ class TestShellInjection:
|
||||||
self,
|
self,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
src_img: Image.Image,
|
src_img: Image.Image,
|
||||||
save_func: Callable[[Image.Image, int, str], None],
|
save_func: Callable[[Image.Image, IO[bytes], str | bytes], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
for filename in test_filenames:
|
for filename in test_filenames:
|
||||||
dest_file = str(tmp_path / filename)
|
dest_file = str(tmp_path / filename)
|
||||||
save_func(src_img, 0, dest_file)
|
save_func(src_img, BytesIO(), dest_file)
|
||||||
# If file can't be opened, shell injection probably occurred
|
# If file can't be opened, shell injection probably occurred
|
||||||
with Image.open(dest_file) as im:
|
with Image.open(dest_file) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# install libimagequant
|
# install libimagequant
|
||||||
|
|
||||||
archive_name=libimagequant
|
archive_name=libimagequant
|
||||||
archive_version=4.3.0
|
archive_version=4.3.1
|
||||||
|
|
||||||
archive=$archive_name-$archive_version
|
archive=$archive_name-$archive_version
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install webp
|
# install webp
|
||||||
|
|
||||||
archive=libwebp-1.3.2
|
archive=libwebp-1.4.0
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ PAPER =
|
||||||
BUILDDIR = _build
|
BUILDDIR = _build
|
||||||
|
|
||||||
# Internal variables.
|
# Internal variables.
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
PAPEROPT_a4 = --define latex_paper_size=a4
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
PAPEROPT_letter = --define latex_paper_size=letter
|
||||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
# the i18n builder cannot share the environment and doctrees with the others
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
@ -51,42 +51,42 @@ install-sphinx:
|
||||||
.PHONY: html
|
.PHONY: html
|
||||||
html:
|
html:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
$(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
.PHONY: dirhtml
|
.PHONY: dirhtml
|
||||||
dirhtml:
|
dirhtml:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
$(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
.PHONY: singlehtml
|
.PHONY: singlehtml
|
||||||
singlehtml:
|
singlehtml:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
$(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
.PHONY: pickle
|
.PHONY: pickle
|
||||||
pickle:
|
pickle:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
$(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished; now you can process the pickle files."
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
.PHONY: json
|
.PHONY: json
|
||||||
json:
|
json:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
$(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished; now you can process the JSON files."
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
.PHONY: htmlhelp
|
.PHONY: htmlhelp
|
||||||
htmlhelp:
|
htmlhelp:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
$(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
@ -94,7 +94,7 @@ htmlhelp:
|
||||||
.PHONY: qthelp
|
.PHONY: qthelp
|
||||||
qthelp:
|
qthelp:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
$(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@ -105,7 +105,7 @@ qthelp:
|
||||||
.PHONY: devhelp
|
.PHONY: devhelp
|
||||||
devhelp:
|
devhelp:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
$(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished."
|
@echo "Build finished."
|
||||||
@echo "To view the help file:"
|
@echo "To view the help file:"
|
||||||
|
@ -116,14 +116,14 @@ devhelp:
|
||||||
.PHONY: epub
|
.PHONY: epub
|
||||||
epub:
|
epub:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
$(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
.PHONY: latex
|
.PHONY: latex
|
||||||
latex:
|
latex:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
@ -132,7 +132,7 @@ latex:
|
||||||
.PHONY: latexpdf
|
.PHONY: latexpdf
|
||||||
latexpdf:
|
latexpdf:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
$(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
@echo "Running LaTeX files through pdflatex..."
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
@ -140,21 +140,21 @@ latexpdf:
|
||||||
.PHONY: text
|
.PHONY: text
|
||||||
text:
|
text:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
$(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
.PHONY: man
|
.PHONY: man
|
||||||
man:
|
man:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
$(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
.PHONY: texinfo
|
.PHONY: texinfo
|
||||||
texinfo:
|
texinfo:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
@ -163,7 +163,7 @@ texinfo:
|
||||||
.PHONY: info
|
.PHONY: info
|
||||||
info:
|
info:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
$(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
@echo "Running Texinfo files through makeinfo..."
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
make -C $(BUILDDIR)/texinfo info
|
make -C $(BUILDDIR)/texinfo info
|
||||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
@ -171,21 +171,21 @@ info:
|
||||||
.PHONY: gettext
|
.PHONY: gettext
|
||||||
gettext:
|
gettext:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
$(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
.PHONY: changes
|
.PHONY: changes
|
||||||
changes:
|
changes:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
$(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
@echo
|
@echo
|
||||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
.PHONY: linkcheck
|
.PHONY: linkcheck
|
||||||
linkcheck:
|
linkcheck:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
|
$(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
|
||||||
@echo
|
@echo
|
||||||
@echo "Link check complete; look for any errors in the above output " \
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
@ -193,7 +193,7 @@ linkcheck:
|
||||||
.PHONY: doctest
|
.PHONY: doctest
|
||||||
doctest:
|
doctest:
|
||||||
$(MAKE) install-sphinx
|
$(MAKE) install-sphinx
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
$(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
"results in $(BUILDDIR)/doctest/output.txt."
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,13 @@ Support for LibTIFF earlier than 4
|
||||||
Support for LibTIFF earlier than version 4 has been deprecated.
|
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||||
Upgrade to a newer version of LibTIFF instead.
|
Upgrade to a newer version of LibTIFF instead.
|
||||||
|
|
||||||
|
ImageDraw.getdraw hints parameter
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 10.4.0
|
||||||
|
|
||||||
|
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -279,7 +286,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
||||||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||||
offset.
|
offset.
|
||||||
|
|
||||||
.. image:: ./example/size_vs_bbox.png
|
.. image:: ./example/size_vs_bbox.webp
|
||||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 9.5 KiB |
|
@ -26,5 +26,5 @@ if __name__ == "__main__":
|
||||||
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
||||||
if y != 0:
|
if y != 0:
|
||||||
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
||||||
im.save("docs/example/anchors.png")
|
im.save("docs/example/anchors.webp")
|
||||||
im.show()
|
im.show()
|
||||||
|
|
BIN
docs/example/anchors.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/image_thumbnail.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
docs/example/imageops_cover.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/example/imageops_fit.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_pad.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
docs/example/size_vs_bbox.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
|
@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
||||||
.. py:currentmodule:: PIL.Image
|
.. py:currentmodule:: PIL.Image
|
||||||
|
|
||||||
.. data:: Resampling.NEAREST
|
.. data:: Resampling.NEAREST
|
||||||
|
:noindex:
|
||||||
|
|
||||||
Pick one nearest pixel from the input image. Ignore all other input pixels.
|
Pick one nearest pixel from the input image. Ignore all other input pixels.
|
||||||
|
|
||||||
.. data:: Resampling.BOX
|
.. data:: Resampling.BOX
|
||||||
|
:noindex:
|
||||||
|
|
||||||
Each pixel of source image contributes to one pixel of the
|
Each pixel of source image contributes to one pixel of the
|
||||||
destination image with identical weights.
|
destination image with identical weights.
|
||||||
|
@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
||||||
.. versionadded:: 3.4.0
|
.. versionadded:: 3.4.0
|
||||||
|
|
||||||
.. data:: Resampling.BILINEAR
|
.. data:: Resampling.BILINEAR
|
||||||
|
:noindex:
|
||||||
|
|
||||||
For resize calculate the output pixel value using linear interpolation
|
For resize calculate the output pixel value using linear interpolation
|
||||||
on all pixels that may contribute to the output value.
|
on all pixels that may contribute to the output value.
|
||||||
|
@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
||||||
in the input image is used.
|
in the input image is used.
|
||||||
|
|
||||||
.. data:: Resampling.HAMMING
|
.. data:: Resampling.HAMMING
|
||||||
|
:noindex:
|
||||||
|
|
||||||
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
|
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
|
||||||
dislocations on local level like with :data:`Resampling.BOX`.
|
dislocations on local level like with :data:`Resampling.BOX`.
|
||||||
|
@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
||||||
.. versionadded:: 3.4.0
|
.. versionadded:: 3.4.0
|
||||||
|
|
||||||
.. data:: Resampling.BICUBIC
|
.. data:: Resampling.BICUBIC
|
||||||
|
:noindex:
|
||||||
|
|
||||||
For resize calculate the output pixel value using cubic interpolation
|
For resize calculate the output pixel value using cubic interpolation
|
||||||
on all pixels that may contribute to the output value.
|
on all pixels that may contribute to the output value.
|
||||||
|
@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
||||||
in the input image is used.
|
in the input image is used.
|
||||||
|
|
||||||
.. data:: Resampling.LANCZOS
|
.. data:: Resampling.LANCZOS
|
||||||
|
:noindex:
|
||||||
|
|
||||||
Calculate the output pixel value using a high-quality Lanczos filter (a
|
Calculate the output pixel value using a high-quality Lanczos filter (a
|
||||||
truncated sinc) on all pixels that may contribute to the output value.
|
truncated sinc) on all pixels that may contribute to the output value.
|
||||||
|
|
|
@ -132,7 +132,7 @@ of the two lines.
|
||||||
|
|
||||||
.. comment: Image generated with ../example/anchors.py
|
.. comment: Image generated with ../example/anchors.py
|
||||||
|
|
||||||
.. image:: ../example/anchors.png
|
.. image:: ../example/anchors.webp
|
||||||
:alt: Text anchor examples
|
:alt: Text anchor examples
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
|
|
@ -278,26 +278,26 @@ choose to resize relative to a given size.
|
||||||
|
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
size = (100, 150)
|
size = (100, 150)
|
||||||
with Image.open("Tests/images/hopper.png") as im:
|
with Image.open("Tests/images/hopper.webp") as im:
|
||||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||||
|
|
||||||
# thumbnail() can also be used,
|
# thumbnail() can also be used,
|
||||||
# but will modify the image object in place
|
# but will modify the image object in place
|
||||||
im.thumbnail(size)
|
im.thumbnail(size)
|
||||||
im.save("imageops_thumbnail.png")
|
im.save("image_thumbnail.webp")
|
||||||
|
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|
|
||||||
.. _color-transforms:
|
.. _color-transforms:
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libimagequant** provides improved color quantization
|
* **libimagequant** provides improved color quantization
|
||||||
|
|
||||||
* Pillow has been tested with libimagequant **2.6-4.3**
|
* Pillow has been tested with libimagequant **2.6-4.3.1**
|
||||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||||
the Pillow license, therefore we will not be distributing binaries
|
the Pillow license, therefore we will not be distributing binaries
|
||||||
with libimagequant support enabled.
|
with libimagequant support enabled.
|
||||||
|
|
|
@ -78,8 +78,6 @@ Constructing images
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. autofunction:: new
|
.. autofunction:: new
|
||||||
.. autoclass:: SupportsArrayInterface
|
|
||||||
:show-inheritance:
|
|
||||||
.. autofunction:: fromarray
|
.. autofunction:: fromarray
|
||||||
.. autofunction:: frombytes
|
.. autofunction:: frombytes
|
||||||
.. autofunction:: frombuffer
|
.. autofunction:: frombuffer
|
||||||
|
@ -197,6 +195,7 @@ This helps to get the bounding box coordinates of the input image::
|
||||||
.. automethod:: PIL.Image.Image.getpalette
|
.. automethod:: PIL.Image.Image.getpalette
|
||||||
.. automethod:: PIL.Image.Image.getpixel
|
.. automethod:: PIL.Image.Image.getpixel
|
||||||
.. automethod:: PIL.Image.Image.getprojection
|
.. automethod:: PIL.Image.Image.getprojection
|
||||||
|
.. automethod:: PIL.Image.Image.getxmp
|
||||||
.. automethod:: PIL.Image.Image.histogram
|
.. automethod:: PIL.Image.Image.histogram
|
||||||
.. automethod:: PIL.Image.Image.paste
|
.. automethod:: PIL.Image.Image.paste
|
||||||
.. automethod:: PIL.Image.Image.point
|
.. automethod:: PIL.Image.Image.point
|
||||||
|
@ -365,6 +364,14 @@ Classes
|
||||||
.. autoclass:: PIL.Image.ImagePointHandler
|
.. autoclass:: PIL.Image.ImagePointHandler
|
||||||
.. autoclass:: PIL.Image.ImageTransformHandler
|
.. autoclass:: PIL.Image.ImageTransformHandler
|
||||||
|
|
||||||
|
Protocols
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. autoclass:: SupportsArrayInterface
|
||||||
|
:show-inheritance:
|
||||||
|
.. autoclass:: SupportsGetData
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Constants
|
Constants
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -418,7 +425,6 @@ See :ref:`concept-filters` for details.
|
||||||
.. autoclass:: Resampling
|
.. autoclass:: Resampling
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:noindex:
|
|
||||||
|
|
||||||
Dither modes
|
Dither modes
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
|
@ -691,23 +691,7 @@ Methods
|
||||||
:param hints: An optional list of hints.
|
:param hints: An optional list of hints.
|
||||||
:returns: A (drawing context, drawing resource factory) tuple.
|
:returns: A (drawing context, drawing resource factory) tuple.
|
||||||
|
|
||||||
.. py:method:: floodfill(image, xy, value, border=None, thresh=0)
|
.. autofunction:: PIL.ImageDraw.floodfill
|
||||||
|
|
||||||
.. warning:: This method is experimental.
|
|
||||||
|
|
||||||
Fills a bounded region with a given color.
|
|
||||||
|
|
||||||
:param image: Target image.
|
|
||||||
:param xy: Seed position (a 2-item coordinate tuple).
|
|
||||||
:param value: Fill color.
|
|
||||||
:param border: Optional border value. If given, the region consists of
|
|
||||||
pixels with a color different from the border color. If not given,
|
|
||||||
the region consists of pixels having the same color as the seed
|
|
||||||
pixel.
|
|
||||||
:param thresh: Optional threshold value which specifies a maximum
|
|
||||||
tolerable difference of a pixel value from the 'background' in
|
|
||||||
order for it to be replaced. Useful for filling regions of non-
|
|
||||||
homogeneous, but similar, colors.
|
|
||||||
|
|
||||||
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
|
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
|
||||||
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
|
||||||
|
|
|
@ -57,6 +57,10 @@ Classes
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: PIL.ImageFile.StubHandler()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. autoclass:: PIL.ImageFile.StubImageFile()
|
.. autoclass:: PIL.ImageFile.StubImageFile()
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
|
@ -36,26 +36,26 @@ Resize relative to a given size
|
||||||
|
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
size = (100, 150)
|
size = (100, 150)
|
||||||
with Image.open("Tests/images/hopper.png") as im:
|
with Image.open("Tests/images/hopper.webp") as im:
|
||||||
ImageOps.contain(im, size).save("imageops_contain.png")
|
ImageOps.contain(im, size).save("imageops_contain.webp")
|
||||||
ImageOps.cover(im, size).save("imageops_cover.png")
|
ImageOps.cover(im, size).save("imageops_cover.webp")
|
||||||
ImageOps.fit(im, size).save("imageops_fit.png")
|
ImageOps.fit(im, size).save("imageops_fit.webp")
|
||||||
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
ImageOps.pad(im, size, color="#f00").save("imageops_pad.webp")
|
||||||
|
|
||||||
# thumbnail() can also be used,
|
# thumbnail() can also be used,
|
||||||
# but will modify the image object in place
|
# but will modify the image object in place
|
||||||
im.thumbnail(size)
|
im.thumbnail(size)
|
||||||
im.save("imageops_thumbnail.png")
|
im.save("image_thumbnail.webp")
|
||||||
|
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||||
+================+===========================================+============================================+==========================================+========================================+========================================+
|
+================+============================================+=============================================+===========================================+=========================================+=========================================+
|
||||||
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
|Resulting image | .. image:: ../example/image_thumbnail.webp | .. image:: ../example/imageops_contain.webp | .. image:: ../example/imageops_cover.webp | .. image:: ../example/imageops_fit.webp | .. image:: ../example/imageops_pad.webp |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
|
||||||
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
+----------------+--------------------------------------------+---------------------------------------------+-------------------------------------------+-----------------------------------------+-----------------------------------------+
|
||||||
|
|
||||||
.. autofunction:: contain
|
.. autofunction:: contain
|
||||||
.. autofunction:: cover
|
.. autofunction:: cover
|
||||||
|
|
|
@ -44,42 +44,23 @@ Access using negative indexes is also possible. ::
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
.. class:: PixelAccess
|
.. class:: PixelAccess
|
||||||
|
:canonical: PIL.Image.core.PixelAccess
|
||||||
|
|
||||||
.. method:: __setitem__(self, xy, color):
|
.. method:: __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]
|
||||||
|
|
||||||
Modifies the pixel at x,y. The color is given as a single
|
|
||||||
numerical value for single band images, and a tuple for
|
|
||||||
multi-band images
|
|
||||||
|
|
||||||
:param xy: The pixel coordinate, given as (x, y).
|
|
||||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
|
||||||
|
|
||||||
.. method:: __getitem__(self, xy):
|
|
||||||
|
|
||||||
Returns the pixel at x,y. The pixel is returned as a single
|
Returns the pixel at x,y. The pixel is returned as a single
|
||||||
value for single band images or a tuple for multiple band
|
value for single band images or a tuple for multi-band images.
|
||||||
images
|
|
||||||
|
|
||||||
:param xy: The pixel coordinate, given as (x, y).
|
:param xy: The pixel coordinate, given as (x, y).
|
||||||
:returns: a pixel value for single band images, a tuple of
|
:returns: a pixel value for single band images, a tuple of
|
||||||
pixel values for multiband images.
|
pixel values for multiband images.
|
||||||
|
|
||||||
.. method:: putpixel(self, xy, color):
|
.. method:: __setitem__(self, xy: tuple[int, int], color: float | tuple[int, ...]) -> None
|
||||||
|
|
||||||
Modifies the pixel at x,y. The color is given as a single
|
Modifies the pixel at x,y. The color is given as a single
|
||||||
numerical value for single band images, and a tuple for
|
numerical value for single band images, and a tuple for
|
||||||
multi-band images. In addition to this, RGB and RGBA tuples
|
multi-band images.
|
||||||
are accepted for P and PA images.
|
|
||||||
|
|
||||||
:param xy: The pixel coordinate, given as (x, y).
|
:param xy: The pixel coordinate, given as (x, y).
|
||||||
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
|
:param color: The pixel value according to its mode,
|
||||||
|
e.g. tuple (r, g, b) for RGB mode.
|
||||||
.. method:: getpixel(self, xy):
|
|
||||||
|
|
||||||
Returns the pixel at x,y. The pixel is returned as a single
|
|
||||||
value for single band images or a tuple for multiple band
|
|
||||||
images
|
|
||||||
|
|
||||||
:param xy: The pixel coordinate, given as (x, y).
|
|
||||||
:returns: a pixel value for single band images, a tuple of
|
|
||||||
pixel values for multiband images.
|
|
||||||
|
|
|
@ -44,3 +44,4 @@ Access using negative indexes is also possible. ::
|
||||||
|
|
||||||
.. autoclass:: PIL.PyAccess.PyAccess()
|
.. autoclass:: PIL.PyAccess.PyAccess()
|
||||||
:members:
|
:members:
|
||||||
|
:special-members: __getitem__, __setitem__
|
||||||
|
|
|
@ -33,6 +33,10 @@ Internal Modules
|
||||||
Provides a convenient way to import type hints that are not available
|
Provides a convenient way to import type hints that are not available
|
||||||
on some Python versions.
|
on some Python versions.
|
||||||
|
|
||||||
|
.. py:class:: NumpyArray
|
||||||
|
|
||||||
|
Typing alias.
|
||||||
|
|
||||||
.. py:class:: StrOrBytesPath
|
.. py:class:: StrOrBytesPath
|
||||||
|
|
||||||
Typing alias.
|
Typing alias.
|
||||||
|
|
|
@ -34,6 +34,11 @@ Support for LibTIFF earlier than 4
|
||||||
Support for LibTIFF earlier than version 4 has been deprecated.
|
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||||
Upgrade to a newer version of LibTIFF instead.
|
Upgrade to a newer version of LibTIFF instead.
|
||||||
|
|
||||||
|
ImageDraw.getdraw hints parameter
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
|
||||||
|
|
||||||
API Changes
|
API Changes
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,9 @@ is not secure.
|
||||||
|
|
||||||
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
|
- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
|
||||||
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
|
orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
|
||||||
- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
|
- ``getxmp()`` was added to :py:class:`~PIL.JpegImagePlugin.JpegImageFile` in Pillow
|
||||||
will now use ``defusedxml`` instead. If the dependency is not present, an empty
|
8.2.0. It will now use ``defusedxml`` instead. If the dependency is not present, an
|
||||||
dictionary will be returned and a warning raised.
|
empty dictionary will be returned and a warning raised.
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
============
|
============
|
||||||
|
|
|
@ -98,7 +98,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
||||||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||||
offset.
|
offset.
|
||||||
|
|
||||||
.. image:: ../example/size_vs_bbox.png
|
.. image:: ../example/size_vs_bbox.webp
|
||||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
|
4
setup.py
|
@ -37,7 +37,9 @@ IMAGEQUANT_ROOT = None
|
||||||
JPEG2K_ROOT = None
|
JPEG2K_ROOT = None
|
||||||
JPEG_ROOT = None
|
JPEG_ROOT = None
|
||||||
LCMS_ROOT = None
|
LCMS_ROOT = None
|
||||||
|
RAQM_ROOT = None
|
||||||
TIFF_ROOT = None
|
TIFF_ROOT = None
|
||||||
|
WEBP_ROOT = None
|
||||||
ZLIB_ROOT = None
|
ZLIB_ROOT = None
|
||||||
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
||||||
|
|
||||||
|
@ -459,6 +461,8 @@ class pil_build_ext(build_ext):
|
||||||
"FREETYPE_ROOT": "freetype2",
|
"FREETYPE_ROOT": "freetype2",
|
||||||
"HARFBUZZ_ROOT": "harfbuzz",
|
"HARFBUZZ_ROOT": "harfbuzz",
|
||||||
"FRIBIDI_ROOT": "fribidi",
|
"FRIBIDI_ROOT": "fribidi",
|
||||||
|
"RAQM_ROOT": "raqm",
|
||||||
|
"WEBP_ROOT": "libwebp",
|
||||||
"LCMS_ROOT": "lcms2",
|
"LCMS_ROOT": "lcms2",
|
||||||
"IMAGEQUANT_ROOT": "libimagequant",
|
"IMAGEQUANT_ROOT": "libimagequant",
|
||||||
}.items():
|
}.items():
|
||||||
|
|
|
@ -103,7 +103,7 @@ def bdf_char(
|
||||||
class BdfFontFile(FontFile.FontFile):
|
class BdfFontFile(FontFile.FontFile):
|
||||||
"""Font file plugin for the X11 BDF format."""
|
"""Font file plugin for the X11 BDF format."""
|
||||||
|
|
||||||
def __init__(self, fp: BinaryIO):
|
def __init__(self, fp: BinaryIO) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
s = fp.readline()
|
s = fp.readline()
|
||||||
|
|
|
@ -31,10 +31,12 @@ BLP files come in many different flavours:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
@ -55,11 +57,13 @@ class AlphaEncoding(IntEnum):
|
||||||
DXT5 = 7
|
DXT5 = 7
|
||||||
|
|
||||||
|
|
||||||
def unpack_565(i):
|
def unpack_565(i: int) -> tuple[int, int, int]:
|
||||||
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
|
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
|
||||||
|
|
||||||
|
|
||||||
def decode_dxt1(data, alpha=False):
|
def decode_dxt1(
|
||||||
|
data: bytes, alpha: bool = False
|
||||||
|
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
"""
|
"""
|
||||||
input: one "row" of data (i.e. will produce 4*width pixels)
|
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||||
"""
|
"""
|
||||||
|
@ -67,9 +71,9 @@ def decode_dxt1(data, alpha=False):
|
||||||
blocks = len(data) // 8 # number of blocks in row
|
blocks = len(data) // 8 # number of blocks in row
|
||||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
for block in range(blocks):
|
for block_index in range(blocks):
|
||||||
# Decode next 8-byte block.
|
# Decode next 8-byte block.
|
||||||
idx = block * 8
|
idx = block_index * 8
|
||||||
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
|
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
|
||||||
|
|
||||||
r0, g0, b0 = unpack_565(color0)
|
r0, g0, b0 = unpack_565(color0)
|
||||||
|
@ -114,7 +118,7 @@ def decode_dxt1(data, alpha=False):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def decode_dxt3(data):
|
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
"""
|
"""
|
||||||
input: one "row" of data (i.e. will produce 4*width pixels)
|
input: one "row" of data (i.e. will produce 4*width pixels)
|
||||||
"""
|
"""
|
||||||
|
@ -122,8 +126,8 @@ def decode_dxt3(data):
|
||||||
blocks = len(data) // 16 # number of blocks in row
|
blocks = len(data) // 16 # number of blocks in row
|
||||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
for block in range(blocks):
|
for block_index in range(blocks):
|
||||||
idx = block * 16
|
idx = block_index * 16
|
||||||
block = data[idx : idx + 16]
|
block = data[idx : idx + 16]
|
||||||
# Decode next 16-byte block.
|
# Decode next 16-byte block.
|
||||||
bits = struct.unpack_from("<8B", block)
|
bits = struct.unpack_from("<8B", block)
|
||||||
|
@ -167,7 +171,7 @@ def decode_dxt3(data):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def decode_dxt5(data):
|
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
|
||||||
"""
|
"""
|
||||||
input: one "row" of data (i.e. will produce 4 * width pixels)
|
input: one "row" of data (i.e. will produce 4 * width pixels)
|
||||||
"""
|
"""
|
||||||
|
@ -175,8 +179,8 @@ def decode_dxt5(data):
|
||||||
blocks = len(data) // 16 # number of blocks in row
|
blocks = len(data) // 16 # number of blocks in row
|
||||||
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
ret = (bytearray(), bytearray(), bytearray(), bytearray())
|
||||||
|
|
||||||
for block in range(blocks):
|
for block_index in range(blocks):
|
||||||
idx = block * 16
|
idx = block_index * 16
|
||||||
block = data[idx : idx + 16]
|
block = data[idx : idx + 16]
|
||||||
# Decode next 16-byte block.
|
# Decode next 16-byte block.
|
||||||
a0, a1 = struct.unpack_from("<BB", block)
|
a0, a1 = struct.unpack_from("<BB", block)
|
||||||
|
@ -275,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
|
||||||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
_pulls_fd = True
|
_pulls_fd = True
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||||
try:
|
try:
|
||||||
self._read_blp_header()
|
self._read_blp_header()
|
||||||
self._load()
|
self._load()
|
||||||
|
@ -284,7 +288,12 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
raise OSError(msg) from e
|
raise OSError(msg) from e
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
def _read_blp_header(self):
|
@abc.abstractmethod
|
||||||
|
def _load(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _read_blp_header(self) -> None:
|
||||||
|
assert self.fd is not None
|
||||||
self.fd.seek(4)
|
self.fd.seek(4)
|
||||||
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
|
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
|
||||||
|
|
||||||
|
@ -303,10 +312,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||||
|
|
||||||
def _safe_read(self, length):
|
def _safe_read(self, length: int) -> bytes:
|
||||||
return ImageFile._safe_read(self.fd, length)
|
return ImageFile._safe_read(self.fd, length)
|
||||||
|
|
||||||
def _read_palette(self):
|
def _read_palette(self) -> list[tuple[int, int, int, int]]:
|
||||||
ret = []
|
ret = []
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
try:
|
try:
|
||||||
|
@ -316,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
ret.append((b, g, r, a))
|
ret.append((b, g, r, a))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _read_bgra(self, palette):
|
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
||||||
while True:
|
while True:
|
||||||
|
@ -325,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
except struct.error:
|
except struct.error:
|
||||||
break
|
break
|
||||||
b, g, r, a = palette[offset]
|
b, g, r, a = palette[offset]
|
||||||
d = (r, g, b)
|
d: tuple[int, ...] = (r, g, b)
|
||||||
if self._blp_alpha_depth:
|
if self._blp_alpha_depth:
|
||||||
d += (a,)
|
d += (a,)
|
||||||
data.extend(d)
|
data.extend(d)
|
||||||
|
@ -349,29 +358,30 @@ class BLP1Decoder(_BLPBaseDecoder):
|
||||||
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
|
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
|
||||||
raise BLPFormatError(msg)
|
raise BLPFormatError(msg)
|
||||||
|
|
||||||
def _decode_jpeg_stream(self):
|
def _decode_jpeg_stream(self) -> None:
|
||||||
from .JpegImagePlugin import JpegImageFile
|
from .JpegImagePlugin import JpegImageFile
|
||||||
|
|
||||||
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
||||||
jpeg_header = self._safe_read(jpeg_header_size)
|
jpeg_header = self._safe_read(jpeg_header_size)
|
||||||
|
assert self.fd is not None
|
||||||
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
|
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
|
||||||
data = self._safe_read(self._blp_lengths[0])
|
data = self._safe_read(self._blp_lengths[0])
|
||||||
data = jpeg_header + data
|
data = jpeg_header + data
|
||||||
data = BytesIO(data)
|
image = JpegImageFile(BytesIO(data))
|
||||||
image = JpegImageFile(data)
|
|
||||||
Image._decompression_bomb_check(image.size)
|
Image._decompression_bomb_check(image.size)
|
||||||
if image.mode == "CMYK":
|
if image.mode == "CMYK":
|
||||||
decoder_name, extents, offset, args = image.tile[0]
|
decoder_name, extents, offset, args = image.tile[0]
|
||||||
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
|
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
|
||||||
r, g, b = image.convert("RGB").split()
|
r, g, b = image.convert("RGB").split()
|
||||||
image = Image.merge("RGB", (b, g, r))
|
reversed_image = Image.merge("RGB", (b, g, r))
|
||||||
self.set_as_raw(image.tobytes())
|
self.set_as_raw(reversed_image.tobytes())
|
||||||
|
|
||||||
|
|
||||||
class BLP2Decoder(_BLPBaseDecoder):
|
class BLP2Decoder(_BLPBaseDecoder):
|
||||||
def _load(self):
|
def _load(self) -> None:
|
||||||
palette = self._read_palette()
|
palette = self._read_palette()
|
||||||
|
|
||||||
|
assert self.fd is not None
|
||||||
self.fd.seek(self._blp_offsets[0])
|
self.fd.seek(self._blp_offsets[0])
|
||||||
|
|
||||||
if self._blp_compression == 1:
|
if self._blp_compression == 1:
|
||||||
|
@ -420,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
||||||
|
|
||||||
def _write_palette(self) -> bytes:
|
def _write_palette(self) -> bytes:
|
||||||
data = b""
|
data = b""
|
||||||
|
assert self.im is not None
|
||||||
palette = self.im.getpalette("RGBA", "RGBA")
|
palette = self.im.getpalette("RGBA", "RGBA")
|
||||||
for i in range(len(palette) // 4):
|
for i in range(len(palette) // 4):
|
||||||
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||||
|
@ -428,12 +439,13 @@ class BLPEncoder(ImageFile.PyEncoder):
|
||||||
data += b"\x00" * 4
|
data += b"\x00" * 4
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def encode(self, bufsize):
|
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||||
palette_data = self._write_palette()
|
palette_data = self._write_palette()
|
||||||
|
|
||||||
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
offset = 20 + 16 * 4 * 2 + len(palette_data)
|
||||||
data = struct.pack("<16I", offset, *((0,) * 15))
|
data = struct.pack("<16I", offset, *((0,) * 15))
|
||||||
|
|
||||||
|
assert self.im is not None
|
||||||
w, h = self.im.size
|
w, h = self.im.size
|
||||||
data += struct.pack("<16I", w * h, *((0,) * 15))
|
data += struct.pack("<16I", w * h, *((0,) * 15))
|
||||||
|
|
||||||
|
@ -446,7 +458,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
||||||
return len(data), 0, data
|
return len(data), 0, data
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
if im.mode != "P":
|
if im.mode != "P":
|
||||||
msg = "Unsupported BLP image mode"
|
msg = "Unsupported BLP image mode"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
from . import Image, ImageFile, ImagePalette
|
from . import Image, ImageFile, ImagePalette
|
||||||
from ._binary import i16le as i16
|
from ._binary import i16le as i16
|
||||||
|
@ -52,7 +53,7 @@ def _accept(prefix: bytes) -> bool:
|
||||||
return prefix[:2] == b"BM"
|
return prefix[:2] == b"BM"
|
||||||
|
|
||||||
|
|
||||||
def _dib_accept(prefix):
|
def _dib_accept(prefix: bytes) -> bool:
|
||||||
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
||||||
|
|
||||||
|
|
||||||
|
@ -300,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
class BmpRleDecoder(ImageFile.PyDecoder):
|
class BmpRleDecoder(ImageFile.PyDecoder):
|
||||||
_pulls_fd = True
|
_pulls_fd = True
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||||
|
assert self.fd is not None
|
||||||
rle4 = self.args[1]
|
rle4 = self.args[1]
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
x = 0
|
x = 0
|
||||||
|
@ -394,11 +396,13 @@ SAVE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _dib_save(im, fp, filename):
|
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
_save(im, fp, filename, False)
|
_save(im, fp, filename, False)
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename, bitmap_header=True):
|
def _save(
|
||||||
|
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
rawmode, bits, colors = SAVE[im.mode]
|
rawmode, bits, colors = SAVE[im.mode]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
|
|
|
@ -10,12 +10,14 @@
|
||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
|
|
||||||
_handler = None
|
_handler = None
|
||||||
|
|
||||||
|
|
||||||
def register_handler(handler):
|
def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||||
"""
|
"""
|
||||||
Install application-specific BUFR image handler.
|
Install application-specific BUFR image handler.
|
||||||
|
|
||||||
|
@ -54,11 +56,11 @@ class BufrStubImageFile(ImageFile.StubImageFile):
|
||||||
if loader:
|
if loader:
|
||||||
loader.open(self)
|
loader.open(self)
|
||||||
|
|
||||||
def _load(self):
|
def _load(self) -> ImageFile.StubHandler | None:
|
||||||
return _handler
|
return _handler
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
if _handler is None or not hasattr(_handler, "save"):
|
if _handler is None or not hasattr(_handler, "save"):
|
||||||
msg = "BUFR save handler not installed"
|
msg = "BUFR save handler not installed"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|