Skip to content

Commit

Permalink
Merge branch 'main' into multiband
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Sep 9, 2024
2 parents d5844a9 + c9dc34a commit bd2543e
Show file tree
Hide file tree
Showing 76 changed files with 764 additions and 354 deletions.
5 changes: 1 addition & 4 deletions .ci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then
fi

# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.9
]]; then
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi
Expand Down
1 change: 1 addition & 0 deletions .ci/requirements-mypy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools
2 changes: 1 addition & 1 deletion .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]

timeout-minutes: 30

Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ jobs:
]
python-version: [
"pypy3.10",
"pypy3.9",
"3.13",
"3.12",
"3.11",
Expand Down Expand Up @@ -77,7 +76,7 @@ jobs:
"pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
uses: deadsnakes/action@v3.2.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- pp39
- pp310
- cp3{9,10,11}
- cp3{12,13}
Expand All @@ -57,7 +56,6 @@ jobs:
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux }

steps:
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
rev: v0.6.3
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
Expand Down Expand Up @@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/

- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.1
rev: 0.29.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
Expand Down
36 changes: 36 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ Changelog (Pillow)
11.0.0 (unreleased)
-------------------

- Deprecate support for FreeType 2.9.0 #8356
[hugovk, radarhere]

- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]

- Handle duplicate EXIF header #8350
[zakajd, radarhere]

- Return early from BoxBlur if either width or height is zero #8347
[radarhere]

- Check text is either string or bytes #8308
[radarhere]

- Added writing XMP bytes to JPEG #8286
[radarhere]

- Support JPEG2000 RGBA palettes #8256
[radarhere]

- Expand C image to match GIF frame image size #8237
[radarhere]

- Allow saving I;16 images as PPM #8231
[radarhere]

- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]

- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]

- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]

- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]

Expand Down
Binary file added Tests/images/test_extents_transparency.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion Tests/oss-fuzz/fuzz_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@


import atheris
from atheris.import_hook import instrument_imports

with atheris.instrument_imports():
with instrument_imports():
import sys

import fuzzers
Expand Down
3 changes: 2 additions & 1 deletion Tests/oss-fuzz/fuzz_pillow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@


import atheris
from atheris.import_hook import instrument_imports

with atheris.instrument_imports():
with instrument_imports():
import sys

import fuzzers
Expand Down
2 changes: 1 addition & 1 deletion Tests/oss-fuzz/python.supp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
<py3_8_encode_current_locale>
<py3_10_encode_current_locale>
Memcheck:Cond
...
fun:encode_current_locale
Expand Down
5 changes: 5 additions & 0 deletions Tests/test_box_blur.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr"))


@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size


def test_radius_0() -> None:
assert_blur(
sample,
Expand Down
39 changes: 31 additions & 8 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,16 +1378,39 @@ def test_lzw_bits() -> None:
im.load()


def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100)
@pytest.mark.parametrize(
"test_file, loading_strategy",
(
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)

# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)

im.seek(1)
assert im.size == (150, 150)
im.seek(1)
assert im.size == (150, 150)

im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST


def test_missing_background() -> None:
Expand Down
28 changes: 27 additions & 1 deletion Tests/test_file_ico.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest

from PIL import IcoImagePlugin, Image, ImageDraw
from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile

from .helper import assert_image_equal, assert_image_equal_tofile, hopper

Expand Down Expand Up @@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:

with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")


def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()

ImageFile.LOAD_TRUNCATED_IMAGES = True
data = data[:-3]

try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"

# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")

data = output.getvalue()[:-1]

with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
19 changes: 18 additions & 1 deletion Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,12 +991,29 @@ def test_getxmp_padded(self) -> None:
else:
assert im.getxmp() == {"xmpmeta": None}

def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"

im.info["xmp"] = b"1" * 65504
im.save(f)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504

with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)

@pytest.mark.timeout(timeout=1)
def test_eof(self) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0

Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
Expand Down
16 changes: 16 additions & 0 deletions Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False


@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"


def test_header_errors() -> None:
for path in (
"Tests/images/invalid_header_length.jp2",
Expand Down Expand Up @@ -391,6 +400,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0

with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0


def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
Expand Down
2 changes: 2 additions & 0 deletions Tests/test_file_ppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)

im.convert("I;16").save(filename, "PPM")
assert_image_equal_tofile(im, filename)


Expand Down
16 changes: 16 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,22 @@ def test_empty_exif(self) -> None:
exif.load(b"Exif\x00\x00")
assert not dict(exif)

def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]

exif = im.getexif()
assert exif[274] == 1

def test_empty_get_ifd(self) -> None:
exif = Image.Exif()
ifd = exif.get_ifd(0x8769)
assert ifd == {}

ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}

@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test(mode: str) -> tuple[tuple[int, ...], str, int]:

def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype
assert ai.dtype.type is dtype

# assert test("1") == ((100, 128), '|b1', 1600))
assert test("L") == ((100, 128), "|u1", 12800)
Expand Down
Loading

0 comments on commit bd2543e

Please sign in to comment.