Skip to content

Commit

Permalink
Merge branch 'main' into geometry
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk authored Sep 28, 2024
2 parents e57da68 + ed143f5 commit e9f831d
Show file tree
Hide file tree
Showing 19 changed files with 188 additions and 147 deletions.
5 changes: 2 additions & 3 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
- path %PYTHON%;%PATH%
- .ci\test.cmd

after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
Expand Down
3 changes: 3 additions & 0 deletions .ci/test.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
2 changes: 1 addition & 1 deletion .ci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ set -e

python3 -c "from PIL import Image"

python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE
3 changes: 1 addition & 2 deletions .github/workflows/test-mingw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ jobs:
- name: Test Pillow
run: |
python3 selftest.py --installed
python3 -c "from PIL import Image"
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
.ci/test.sh
- name: Upload coverage
uses: codecov/codecov-action@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH%
python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd

- name: Prepare to upload errors
Expand Down
38 changes: 11 additions & 27 deletions .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main

# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
if [[ "$MB_ML_VER" != 2014 ]]; then
HARFBUZZ_VERSION=9.0.0
else
HARFBUZZ_VERSION=8.5.0
fi
HARFBUZZ_VERSION=10.0.1
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2
Expand Down Expand Up @@ -65,21 +61,15 @@ function build_brotli {
}

function build_harfbuzz {
if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
else
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \
&& meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \
&& meson install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
fi
python3 -m pip install meson ninja

local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \
&& meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \
&& meson install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
fi
}

Expand Down Expand Up @@ -155,13 +145,7 @@ if [[ -n "$IS_MACOS" ]]; then
brew remove --ignore-dependencies webp
fi

brew install meson pkg-config
elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then
if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then
yum install -y meson
fi
else
apk add meson
brew install pkg-config
fi

wrap_wheel_builder build
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,18 @@ jobs:
- name: "macOS 10.10 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310* cp3{9,10,11}*"
build: "cp3{9,10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-latest
cibw_arch: arm64
Expand Down
12 changes: 12 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ Changelog (Pillow)
11.0.0 (unreleased)
-------------------

- Improved copying imagequant libraries #8420
[radarhere]

- Use Capsule for WebP saving #8386
[homm, radarhere]

- Fixed writing multiple StripOffsets to TIFF #8317
[Yay295, radarhere]

- Fix dereference before checking for NULL in ImagingTransformAffine #8398
[PavlNekrasov]

- Use transposed size after opening for TIFF images #8390
[radarhere, homm]

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
python3 -m ruff check --fix .

.PHONY: mypy
mypy:
Expand Down
3 changes: 2 additions & 1 deletion Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def test_bigtiff(self, tmp_path: Path) -> None:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")

with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# multistrip support not yet implemented
# The data type of this file's StripOffsets tag is LONG8,
# which is not yet supported for offset data when saving multiple frames.
del im.tag_v2[273]

outfile = str(tmp_path / "temp.tif")
Expand Down
23 changes: 23 additions & 0 deletions Tests/test_file_tiff_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,29 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG


def test_save_multiple_stripoffsets() -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456)
assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG

# all values are in little-endian
assert ifd.tobytes() == (
# number of tags == 1
b"\x01\x00"
# tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes)
# TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18
# where STRIPOFFSETS is 273, LONG is 4
# and 18 is the offset of the tag data
b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00"
# end of entries
b"\x00\x00\x00\x00"
# 26 is the total number of bytes output,
# the offset for any auxiliary strip data that will then be appended
# (123 + 26, 456 + 26) == (149, 482)
b"\x95\x00\x00\x00\xe2\x01\x00\x00"
)


def test_no_duplicate_50741_tag() -> None:
assert TAG_IDS["MakerNoteSafety"] == 50741
assert TAG_IDS["BestQualityScale"] == 50780
Expand Down
32 changes: 17 additions & 15 deletions Tests/test_file_webp.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_read_rgb(self) -> 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 = tmp_path / "temp.webp"

hopper(mode).save(temp_file, **args)
with Image.open(temp_file) as image:
Expand Down Expand Up @@ -116,7 +116,7 @@ def test_write_method(self, tmp_path: Path) -> None:
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()

def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
Expand All @@ -127,6 +127,11 @@ def test_save_all(self, tmp_path: Path) -> None:
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)

def test_unsupported_image_mode(self) -> None:
im = Image.new("1", (1, 1))
with pytest.raises(ValueError):
_webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "")

def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
self._roundtrip(
Expand All @@ -151,18 +156,16 @@ def test_write_unsupported_mode_P(self, tmp_path: Path) -> None:

@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
im.save(temp_file, method=0)
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"

@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
im.save(tmp_path / "temp.webp")
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
Expand All @@ -187,9 +190,8 @@ def test_WebPAnimDecoder_with_invalid_args(self) -> None:
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with warnings.catch_warnings():
image.save(temp_file)
image.save(tmp_path / "temp.webp")

def test_file_pointer_could_be_reused(self) -> None:
file_path = "Tests/images/hopper.webp"
Expand All @@ -204,27 +206,27 @@ def test_file_pointer_could_be_reused(self) -> None:
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)

def test_background_from_gif(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"

# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

# Save P mode GIF with background
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))

# Save as WEBP
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

# Save as GIF
out_gif = str(tmp_path / "temp.gif")
out_gif = tmp_path / "temp.gif"
with Image.open(out_webp) as im:
im.save(out_gif)

Expand All @@ -234,18 +236,18 @@ def test_background_from_gif(self, tmp_path: Path) -> None:
assert difference < 5

def test_duration(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"

with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000

out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

with Image.open(out_webp) as reloaded:
reloaded.load()
assert reloaded.info["duration"] == 1000

def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
Expand Down
4 changes: 2 additions & 2 deletions depends/install_imagequant.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ else
cargo cinstall --prefix=/usr --destdir=.

# Copy into place
sudo cp usr/lib/libimagequant.so* /usr/lib/
sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \;
sudo cp usr/include/libimagequant.h /usr/include/

if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-$archive_name
mkdir ~/cache-$archive_name
cp usr/lib/libimagequant.so* ~/cache-$archive_name/
find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \;
cp usr/include/libimagequant.h ~/cache-$archive_name/
fi

Expand Down
2 changes: 1 addition & 1 deletion depends/install_raqm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# install raqm


archive=libraqm-0.10.1
archive=libraqm-0.10.2

./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,13 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"

[tool.black]
exclude = "wheels/multibuild"

[tool.ruff]
fix = true
exclude = [ "wheels/multibuild" ]

fix = true
lint.select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors
Expand Down
8 changes: 5 additions & 3 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,9 +991,11 @@ def tobytes(self, offset: int = 0) -> bytes:
if stripoffsets is not None:
tag, typ, count, value, data = entries[stripoffsets]
if data:
msg = "multistrip support not yet implemented"
raise NotImplementedError(msg)
value = self._pack("L", self._unpack("L", value)[0] + offset)
size, handler = self._load_dispatch[typ]
values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values)
else:
value = self._pack("L", self._unpack("L", value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data

# pass 2: write entries to file
Expand Down
Loading

0 comments on commit e9f831d

Please sign in to comment.