Merge branch 'main' into bugreport

This commit is contained in:
Andrew Murray 2024-03-02 17:04:41 +11:00 committed by GitHub
commit 01fdf2ff51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 239 additions and 127 deletions

View File

@ -6,6 +6,7 @@ init:
# Uncomment previous line to get RDP access during the build. # Uncomment previous line to get RDP access during the build.
environment: environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe EXECUTABLE: python.exe
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES

View File

@ -7,10 +7,12 @@ on:
paths: paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"
- "src/PIL/**"
pull_request: pull_request:
paths: paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"
- "src/PIL/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -64,10 +67,10 @@ jobs:
mingw-w64-x86_64-python3-cffi \ mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python3-setuptools \
mingw-w64-x86_64-python-pyqt6 mingw-w64-x86_64-python-pyqt6
python3 -m ensurepip
python3 -m pip install pyroma pytest pytest-cov pytest-timeout python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -66,8 +69,16 @@ jobs:
- name: Print build system information - name: Print build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma - name: Install Python dependencies
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma run: >
python3 -m pip install
coverage>=7.4.2
defusedxml
olefile
pyroma
pytest
pytest-cov
pytest-timeout
- name: Install dependencies - name: Install dependencies
id: install id: install

View File

@ -27,6 +27,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:

View File

@ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.0 HARFBUZZ_VERSION=8.3.0
LIBPNG_VERSION=1.6.40 LIBPNG_VERSION=1.6.40
JPEGTURBO_VERSION=3.0.1 JPEGTURBO_VERSION=3.0.1
OPENJPEG_VERSION=2.5.0 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
LCMS2_VERSION=2.16 LCMS2_VERSION=2.16
@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg { function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz) local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
(cd $out_dir \ (cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install) && make install)
@ -93,6 +93,9 @@ function build {
done done
fi fi
build_openjpeg build_openjpeg
if [ -f /usr/local/lib64/libopenjp2.so ]; then
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
fi
ORIGINAL_CFLAGS=$CFLAGS ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG" CFLAGS="$CFLAGS -O3 -DNDEBUG"

View File

@ -5,6 +5,18 @@ Changelog (Pillow)
10.3.0 (unreleased) 10.3.0 (unreleased)
------------------- -------------------
- Handle truncated chunks at the end of PNG images #7709
[lajiyuan, radarhere]
- Match mask size to pasted image size in GifImagePlugin #7779
[radarhere]
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
[evanmiller, radarhere]
- Fixed reading FLI/FLC images with a prefix chunk #7804
[twolife]
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 - Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
[nik012003, radarhere] [nik012003, radarhere]

View File

@ -82,9 +82,6 @@ As of 2019, Pillow development is
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img <a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
alt="Join the chat at https://gitter.im/python-pillow/Pillow" alt="Join the chat at https://gitter.im/python-pillow/Pillow"
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a> src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
<a href="https://twitter.com/PythonPillow"><img
alt="Follow on https://twitter.com/PythonPillow"
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
<a href="https://fosstodon.org/@pillow"><img <a href="https://fosstodon.org/@pillow"><img
alt="Follow on https://fosstodon.org/@pillow" alt="Follow on https://fosstodon.org/@pillow"
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg" src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"

View File

@ -86,7 +86,7 @@ Released as needed privately to individual vendors for critical security-related
## Publicize Release ## Publicize Release
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
## Documentation ## Documentation

BIN
Tests/images/2422.flc Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -20,7 +20,7 @@ from PIL import _deprecate
), ),
], ],
) )
def test_version(version, expected) -> None: def test_version(version: int | None, expected: str) -> None:
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing") _deprecate.deprecate("Old thing", version, "new thing")
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
), ),
], ],
) )
def test_old_version(deprecated, plural, expected) -> None: def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r"" expected = r""
with pytest.raises(RuntimeError, match=expected): with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural) _deprecate.deprecate(deprecated, 1, plural=plural)
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
"Upgrade to new thing.", "Upgrade to new thing.",
], ],
) )
def test_action(action) -> None: def test_action(action: str) -> None:
expected = ( expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\." r"Upgrade to new thing\."

View File

@ -21,9 +21,16 @@ def test_isatty() -> None:
assert container.isatty() is False assert container.isatty() is False
def test_seek_mode_0() -> None: @pytest.mark.parametrize(
"mode, expected_value",
(
(0, 33),
(1, 66),
(2, 100),
),
)
def test_seek_mode(mode: int, expected_value: int) -> None:
# Arrange # Arrange
mode = 0
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -32,35 +39,7 @@ def test_seek_mode_0() -> None:
container.seek(33, mode) container.seek(33, mode)
# Assert # Assert
assert container.tell() == 33 assert container.tell() == expected_value
def test_seek_mode_1() -> None:
# Arrange
mode = 1
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(33, mode)
container.seek(33, mode)
# Assert
assert container.tell() == 66
def test_seek_mode_2() -> None:
# Arrange
mode = 2
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(33, mode)
container.seek(33, mode)
# Assert
assert container.tell() == 100
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))

View File

@ -4,7 +4,7 @@ import warnings
import pytest import pytest
from PIL import FliImagePlugin, Image from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# save as...-> hopper.fli, default options. # save as...-> hopper.fli, default options.
static_test_file = "Tests/images/hopper.fli" static_test_file = "Tests/images/hopper.fli"
# From https://samples.libav.org/fli-flc/ # From https://samples.ffmpeg.org/fli-flc/
animated_test_file = "Tests/images/a.fli" animated_test_file = "Tests/images/a.fli"
# From https://samples.ffmpeg.org/fli-flc/
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None: def test_sanity() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
@ -32,6 +35,24 @@ def test_sanity() -> None:
assert im.is_animated assert im.is_animated
def test_prefix_chunk() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open() -> None:

View File

@ -1113,6 +1113,21 @@ def test_append_images(tmp_path: Path) -> None:
assert reread.n_frames == 10 assert reread.n_frames == 10
def test_append_different_size_image(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00")
im.save(out, save_all=True, append_images=[bigger_im])
with Image.open(out) as reread:
assert reread.size == (100, 100)
reread.seek(1)
assert reread.size == (100, 100)
def test_transparent_optimize(tmp_path: Path) -> None: def test_transparent_optimize(tmp_path: Path) -> None:
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency. # transparency.

View File

@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode) -> None: def test_save_to_bytes_bmp(mode: str) -> None:
output = io.BytesIO() output = io.BytesIO()
im = hopper(mode) im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])

View File

@ -98,7 +98,7 @@ def test_i() -> None:
assert ret == 97 assert ret == 97
def test_dump(monkeypatch) -> None: def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
c = b"abc" c = b"abc"
# Temporarily redirect stdout # Temporarily redirect stdout

View File

@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
assert isinstance(im, MspImagePlugin.MspImageFile) assert isinstance(im, MspImagePlugin.MspImageFile)
def _assert_file_image_equal(source_path, target_path) -> None: def _assert_file_image_equal(source_path: str, target_path: str) -> None:
with Image.open(source_path) as im: with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path) assert_image_equal_tofile(im, target_path)

View File

@ -6,6 +6,7 @@ import warnings
import zlib import zlib
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from types import ModuleType
from typing import Any from typing import Any
import pytest import pytest
@ -23,6 +24,7 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
ElementTree: ModuleType | None
try: try:
from defusedxml import ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
@ -781,6 +783,18 @@ class TestFilePng:
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)
def test_truncated_end_chunk(self) -> None:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
with pytest.raises(OSError):
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")

View File

@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None:
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
], ],
) )
def test_crashes(test_file, raises) -> None: def test_crashes(test_file: str, raises) -> 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):

View File

@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES) @pytest.mark.parametrize("mode", _MODES)
def test_sanity(mode, tmp_path: Path) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
def roundtrip(original_im) -> None: def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle) original_im.save(out, rle=rle)

View File

@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: def test_writing_other_types_to_ascii(
value: bytes | int, expected: str, tmp_path: Path
) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271] tag = TiffTags.TAGS_V2[271]
@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
@pytest.mark.parametrize("value", (1, IFDRational(1))) @pytest.mark.parametrize("value", (1, IFDRational(1)))
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
im = hopper() im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()

View File

@ -685,15 +685,18 @@ class TestImage:
_make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette())
def test_p_from_rgb_rgba(self) -> None: @pytest.mark.parametrize(
for mode, color in [ "mode, color",
(
("RGB", "#DDEEFF"), ("RGB", "#DDEEFF"),
("RGB", (221, 238, 255)), ("RGB", (221, 238, 255)),
("RGBA", (221, 238, 255, 255)), ("RGBA", (221, 238, 255, 255)),
]: ),
im = Image.new("P", (100, 100), color) )
expected = Image.new(mode, (100, 100), color) def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
assert_image_equal(im.convert(mode), expected) im = Image.new("P", (100, 100), color)
expected = Image.new(mode, (100, 100), color)
assert_image_equal(im.convert(mode), expected)
def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: def test_no_resource_warning_on_save(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/835 # https://github.com/python-pillow/Pillow/issues/835

View File

@ -6,6 +6,7 @@ import re
import shutil import shutil
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None:
@pytest.mark.parametrize("flag", ("my string", -1)) @pytest.mark.parametrize("flag", ("my string", -1))
def test_invalid_flag(flag) -> None: def test_invalid_flag(flag: str | int) -> None:
with hopper() as im: with hopper() as im:
with pytest.raises( with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and " ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@ -335,19 +336,21 @@ def test_extended_information() -> None:
o = ImageCms.getOpenProfile(SRGB) o = ImageCms.getOpenProfile(SRGB)
p = o.profile p = o.profile
def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: def assert_truncated_tuple_equal(
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
) -> None:
# Helper function to reduce precision of tuples of floats # Helper function to reduce precision of tuples of floats
# recursively and then check equality. # recursively and then check equality.
power = 10**digits power = 10**digits
def truncate_tuple(tuple_or_float): def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
return tuple( return tuple(
( (
truncate_tuple(val) truncate_tuple(val)
if isinstance(val, tuple) if isinstance(val, tuple)
else int(val * power) / power else int(val * power) / power
) )
for val in tuple_or_float for val in tuple_value
) )
assert truncate_tuple(tup1) == truncate_tuple(tup2) assert truncate_tuple(tup1) == truncate_tuple(tup2)
@ -504,8 +507,10 @@ def test_profile_typesafety() -> None:
ImageCms.ImageCmsProfile(1).tobytes() ImageCms.ImageCmsProfile(1).tobytes()
def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: def assert_aux_channel_preserved(
def create_test_image(): mode: str, transform_in_place: bool, preserved_channel: str
) -> None:
def create_test_image() -> Image.Image:
# set up test image with something interesting in the tested aux channel. # set up test image with something interesting in the tested aux channel.
# fmt: off # fmt: off
nine_grid_deltas = [ nine_grid_deltas = [
@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode) -> None: def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB") converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128) assert converted_im.getpixel((0, 0)) == (0, 128, 128)

View File

@ -7,7 +7,7 @@ import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import BinaryIO from typing import Any, BinaryIO
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -44,7 +44,7 @@ def test_sanity() -> None:
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
], ],
) )
def layout_engine(request): def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param return request.param
@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
) )
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def test_find_font(monkeypatch, platform, font_directory) -> None: def test_find_font(
monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
) -> None:
def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
# Make a copy of FreeTypeFont so we can patch the original # Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
def loadable_font(filepath, size, index, encoding, *args, **kwargs): def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
):
if filepath == path_to_fake: if filepath == path_to_fake:
return ImageFont._FreeTypeFont( return ImageFont._FreeTypeFont(
FONT_PATH, size, index, encoding, *args, **kwargs FONT_PATH, size, index, encoding, *args
) )
return ImageFont._FreeTypeFont( return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
filepath, size, index, encoding, *args, **kwargs
)
m.setattr(ImageFont, "FreeTypeFont", loadable_font) m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname) font = ImageFont.truetype(fontname)
@ -563,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
if platform == "linux": if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path): def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
if path == font_directory: if path == font_directory:
return [ return [
( (
@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None:
font.getmask("Test Text") font.getmask("Test Text")
def test_raqm_missing_warning(monkeypatch) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record: with pytest.warns(UserWarning) as record:
font = ImageFont.truetype( font = ImageFont.truetype(

View File

@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None: def test_grabclipboard_file(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate() p.communicate()
@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_png(self) -> None: def test_grabclipboard_png(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write( p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
$ms = new-object System.IO.MemoryStream(, $bytes) $ms = new-object System.IO.MemoryStream(, $bytes)
@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only", reason="Linux with wl-clipboard only",
) )
@pytest.mark.parametrize("ext", ("gif", "png", "ico")) @pytest.mark.parametrize("ext", ("gif", "png", "ico"))
def test_grabclipboard_wl_clipboard(self, ext) -> None: def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
image_path = "Tests/images/hopper." + ext image_path = "Tests/images/hopper." + ext
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)
@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only", reason="Linux with wl-clipboard only",
) )
@pytest.mark.parametrize("arg", ("text", "--clear")) @pytest.mark.parametrize("arg", ("text", "--clear"))
def test_grabclipboard_wl_clipboard_errors(self, arg): def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
subprocess.call(["wl-copy", arg]) subprocess.call(["wl-copy", arg])
assert ImageGrab.grabclipboard() is None assert ImageGrab.grabclipboard() is None

View File

@ -58,7 +58,9 @@ def test_path() -> None:
ImagePath.Path((0, 1)), ImagePath.Path((0, 1)),
), ),
) )
def test_path_constructors(coords) -> None: def test_path_constructors(
coords: Sequence[float] | array.array[float] | ImagePath.Path,
) -> None:
# Arrange / Act # Arrange / Act
p = ImagePath.Path(coords) p = ImagePath.Path(coords)
@ -206,9 +208,9 @@ class Evil:
def __init__(self) -> None: def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000) self.corrupt = Image.core.path(0x4000000000000000)
def __getitem__(self, i): def __getitem__(self, i: int) -> bytes:
x = self.corrupt[i] x = self.corrupt[i]
return struct.pack("dd", x[0], x[1]) return struct.pack("dd", x[0], x[1])
def __setitem__(self, i, x) -> None: def __setitem__(self, i: int, x: bytes) -> None:
self.corrupt[i] = struct.unpack("dd", x) self.corrupt[i] = struct.unpack("dd", x)

View File

@ -28,7 +28,7 @@ def test_rgb() -> None:
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
def checkrgb(r, g, b) -> None: def checkrgb(r: int, g: int, b: int) -> None:
val = ImageQt.rgb(r, g, b) val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha val = val % 2**24 # drop the alpha
assert val >> 16 == r assert val >> 16 == r

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest import pytest
from PIL import Image, ImageShow from PIL import Image, ImageShow
@ -24,9 +26,9 @@ def test_register() -> None:
"order", "order",
[-1, 0], [-1, 0],
) )
def test_viewer_show(order) -> None: def test_viewer_show(order: int) -> None:
class TestViewer(ImageShow.Viewer): class TestViewer(ImageShow.Viewer):
def show_image(self, image, **options) -> bool: def show_image(self, image: Image.Image, **options: Any) -> bool:
self.methodCalled = True self.methodCalled = True
return True return True
@ -48,7 +50,7 @@ def test_viewer_show(order) -> None:
reason="Only run on CIs; hangs on Windows CIs", reason="Only run on CIs; hangs on Windows CIs",
) )
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
def test_show(mode) -> None: def test_show(mode: str) -> None:
im = hopper(mode) im = hopper(mode)
assert ImageShow.show(im) assert ImageShow.show(im)
@ -73,7 +75,7 @@ def test_viewer() -> None:
@pytest.mark.parametrize("viewer", ImageShow._viewers) @pytest.mark.parametrize("viewer", ImageShow._viewers)
def test_viewers(viewer) -> None: def test_viewers(viewer: ImageShow.Viewer) -> None:
try: try:
viewer.get_command("test.jpg") viewer.get_command("test.jpg")
except NotImplementedError: except NotImplementedError:

View File

@ -70,7 +70,7 @@ if is_win32():
] ]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels): def serialize_dib(bi, pixels) -> 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

View File

@ -14,7 +14,7 @@ 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): def to_image(dtype, 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,7 +99,7 @@ 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, np) -> None: def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2 assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0] np_size = np.shape[1], np.shape[0]
assert img.size == np_size assert img.size == np_size
@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8), ("HSV", numpy.uint8),
), ),
) )
def test_to_array(mode, dtype) -> None: def test_to_array(mode: str, dtype) -> None:
img = hopper(mode) img = hopper(mode)
# Resize to non-square # Resize to non-square

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest import pytest
from PIL import ImageQt from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -37,7 +37,7 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy()) lbl.setPixmap(pixmap1.copy())
def roundtrip(expected) -> None: def roundtrip(expected: Image.Image) -> None:
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb # Qt saves all pixmaps as rgb
assert_image_similar(result, expected.convert("RGB"), 1) assert_image_similar(result, expected.convert("RGB"), 1)

View File

@ -17,7 +17,7 @@ if ImageQt.qt_is_installed:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
def test_sanity(mode, tmp_path: Path) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode) src = hopper(mode)
data = ImageQt.toqimage(src) data = ImageQt.toqimage(src)

View File

@ -10,7 +10,7 @@ from PIL import _util
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
) )
def test_is_path(test_path) -> None: def test_is_path(test_path: str | Path | PurePath) -> None:
# Act # Act
it_is = _util.is_path(test_path) it_is = _util.is_path(test_path)

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install openjpeg # install openjpeg
archive=openjpeg-2.5.0 archive=openjpeg-2.5.2
./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

View File

@ -504,3 +504,27 @@ PIL.OleFileIO
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from (2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``). PyPI (eg. ``python3 -m pip install olefile``).
import _imaging
~~~~~~~~~~~~~~~
.. versionremoved:: 2.1.0
Pillow >= 2.1.0 no longer supports ``import _imaging``.
Please use ``from PIL.Image import core as _imaging`` instead.
Pillow and PIL
~~~~~~~~~~~~~~
.. versionremoved:: 1.0.0
Pillow and PIL cannot co-exist in the same environment.
Before installing Pillow, please uninstall PIL.
import Image
~~~~~~~~~~~~
.. versionremoved:: 1.0.0
Pillow >= 1.0 no longer supports ``import Image``.
Please use ``from PIL import Image`` instead.

View File

@ -73,10 +73,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
:alt: Join the chat at https://gitter.im/python-pillow/Pillow :alt: Join the chat at https://gitter.im/python-pillow/Pillow
.. image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
:target: https://twitter.com/PythonPillow
:alt: Follow on https://twitter.com/PythonPillow
.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg .. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg
:target: https://fosstodon.org/@pillow :target: https://fosstodon.org/@pillow
:alt: Follow on https://fosstodon.org/@pillow :alt: Follow on https://fosstodon.org/@pillow

View File

@ -9,15 +9,6 @@ Installation
}); });
</script> </script>
Warnings
--------
.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL.
.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead.
.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead.
Python Support Python Support
-------------- --------------
@ -186,7 +177,7 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality. * **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
**2.4.0** and **2.5.0**. **2.4.0**, **2.5.0** and **2.5.2**.
* Pillow does **not** support the earlier **1.5** series which ships * Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie. with Debian Jessie.

View File

@ -14,6 +14,8 @@ only work on L and RGB images.
.. autofunction:: colorize .. autofunction:: colorize
.. autofunction:: crop .. autofunction:: crop
.. autofunction:: scale .. autofunction:: scale
.. autoclass:: SupportsGetMesh
:show-inheritance:
.. autofunction:: deform .. autofunction:: deform
.. autofunction:: equalize .. autofunction:: equalize
.. autofunction:: expand .. autofunction:: expand

View File

@ -79,3 +79,9 @@ Portable FloatMap (PFM) images
Support has been added for reading and writing grayscale (Pf format) Support has been added for reading and writing grayscale (Pf format)
Portable FloatMap (PFM) files containing ``F`` data. Portable FloatMap (PFM) files containing ``F`` data.
Release GIL when fetching WebP frames
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Python's Global Interpreter Lock is now released when fetching WebP frames from
the libwebp decoder.

View File

@ -33,6 +33,7 @@ classifiers = [
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
"Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Multimedia :: Graphics :: Viewers", "Topic :: Multimedia :: Graphics :: Viewers",
"Typing :: Typed",
] ]
dynamic = [ dynamic = [
"version", "version",
@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org"
Mastodon = "https://fosstodon.org/@pillow" Mastodon = "https://fosstodon.org/@pillow"
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
Source = "https://github.com/python-pillow/Pillow" Source = "https://github.com/python-pillow/Pillow"
Twitter = "https://twitter.com/PythonPillow"
[tool.setuptools] [tool.setuptools]
packages = ["PIL"] packages = ["PIL"]

View File

@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF100: if i16(s, 4) == 0xF100:
# prefix chunk; ignore it # prefix chunk; ignore it
self.__offset = self.__offset + i32(s) self.__offset = self.__offset + i32(s)
self.fp.seek(self.__offset)
s = self.fp.read(16) s = self.fp.read(16)
if i16(s, 4) == 0xF1FA: if i16(s, 4) == 0xF1FA:

View File

@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette):
if "transparency" in encoderinfo: if "transparency" in encoderinfo:
# When the delta is zero, fill the image with transparency # When the delta is zero, fill the image with transparency
diff_frame = im_frame.copy() diff_frame = im_frame.copy()
fill = Image.new( fill = Image.new("P", delta.size, encoderinfo["transparency"])
"P", diff_frame.size, encoderinfo["transparency"]
)
if delta.mode == "RGBA": if delta.mode == "RGBA":
r, g, b, a = delta.split() r, g, b, a = delta.split()
mask = ImageMath.eval( mask = ImageMath.eval(

View File

@ -411,7 +411,15 @@ def scale(
return image.resize(size, resample) return image.resize(size, resample)
class _SupportsGetMesh(Protocol): class SupportsGetMesh(Protocol):
"""
An object that supports the ``getmesh`` method, taking an image as an
argument, and returning a list of tuples. Each tuple contains two tuples,
the source box as a tuple of 4 integers, and a tuple of 8 integers for the
final quadrilateral, in order of top left, bottom left, bottom right, top
right.
"""
def getmesh( def getmesh(
self, image: Image.Image self, image: Image.Image
) -> list[ ) -> list[
@ -421,7 +429,7 @@ class _SupportsGetMesh(Protocol):
def deform( def deform(
image: Image.Image, image: Image.Image,
deformer: _SupportsGetMesh, deformer: SupportsGetMesh,
resample: int = Image.Resampling.BILINEAR, resample: int = Image.Resampling.BILINEAR,
) -> Image.Image: ) -> Image.Image:
""" """

View File

@ -981,7 +981,13 @@ class PngImageFile(ImageFile.ImageFile):
except EOFError: except EOFError:
if cid == b"fdAT": if cid == b"fdAT":
length -= 4 length -= 4
ImageFile._safe_read(self.fp, length) try:
ImageFile._safe_read(self.fp, length)
except OSError as e:
if ImageFile.LOAD_TRUNCATED_IMAGES:
break
else:
raise e
except AttributeError: except AttributeError:
logger.debug("%r %s %s (unknown)", cid, pos, length) logger.debug("%r %s %s (unknown)", cid, pos, length)
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)

0
src/PIL/py.typed Normal file
View File

View File

@ -448,11 +448,16 @@ PyObject *
_anim_decoder_get_next(PyObject *self) { _anim_decoder_get_next(PyObject *self) {
uint8_t *buf; uint8_t *buf;
int timestamp; int timestamp;
int ok;
PyObject *bytes; PyObject *bytes;
PyObject *ret; PyObject *ret;
ImagingSectionCookie cookie;
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
if (!WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp)) { ImagingSectionEnter(&cookie);
ok = WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp);
ImagingSectionLeave(&cookie);
if (!ok) {
PyErr_SetString(PyExc_OSError, "failed to read next frame"); PyErr_SetString(PyExc_OSError, "failed to read next frame");
return NULL; return NULL;
} }

View File

@ -308,21 +308,16 @@ DEPS = {
"libs": [r"Lib\MS\*.lib"], "libs": [r"Lib\MS\*.lib"],
}, },
"openjpeg": { "openjpeg": {
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.2.tar.gz",
"filename": "openjpeg-2.5.0.tar.gz", "filename": "openjpeg-2.5.2.tar.gz",
"dir": "openjpeg-2.5.0", "dir": "openjpeg-2.5.2",
"license": "LICENSE", "license": "LICENSE",
"patch": {
r"src\lib\openjp2\ht_dec.c": {
"#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
}
},
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
), ),
cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.2"),
cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.2"),
], ],
"libs": [r"bin\*.lib"], "libs": [r"bin\*.lib"],
}, },