Merge branch 'main' into wrap

This commit is contained in:
Andrew Murray 2026-03-28 22:44:21 +11:00
commit a69b4ec228
90 changed files with 1117 additions and 648 deletions

View File

@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -24,6 +24,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
Fuzzing:
runs-on: ubuntu-latest

View File

@ -48,6 +48,13 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
uses: actions/cache@v5
id: cache-libimagequant
@ -55,12 +62,21 @@ jobs:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: "3.x"
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Build
run: |

View File

@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
update_release_draft:
permissions:

View File

@ -12,6 +12,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
stale:
if: github.repository_owner == 'python-pillow'

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
@ -83,7 +86,7 @@ jobs:
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Docker build
run: |

View File

@ -28,6 +28,7 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
build:

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:

View File

@ -24,6 +24,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:

View File

@ -28,6 +28,7 @@ concurrency:
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
build:
@ -98,8 +99,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.6.0 --no-progress
echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.7.0 --no-progress
echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -61,7 +61,7 @@ jobs:
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# Intel
- { os: "macos-15-intel", python-version: "3.10" }
- { os: "macos-26-intel", python-version: "3.10" }
exclude:
- { os: "macos-latest", python-version: "3.10" }
@ -91,6 +91,14 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
@ -99,13 +107,23 @@ jobs:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS')

View File

@ -90,13 +90,9 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds.
if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3
else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.3.2
LIBPNG_VERSION=1.6.54
FREETYPE_VERSION=2.14.3
HARFBUZZ_VERSION=13.2.1
LIBPNG_VERSION=1.6.56
JPEGTURBO_VERSION=3.1.3
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.2
@ -108,7 +104,7 @@ LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.2.0
LIBAVIF_VERSION=1.3.0
LIBAVIF_VERSION=1.4.1
function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi
@ -310,10 +306,6 @@ function build {
if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build
if [[ -z "$IOS_SDK" ]]; then
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
fi
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else
build_freetype

View File

@ -53,19 +53,19 @@ jobs:
include:
- name: "macOS 10.10 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "{cp314,pp3}*"
macosx_deployment_target: "10.15"
@ -250,7 +250,7 @@ jobs:
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-*
path: dist
@ -269,7 +269,7 @@ jobs:
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-!(sdist)*
path: dist
@ -291,7 +291,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-*
path: dist

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
rev: v0.15.4
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.9.3
rev: 1.9.4
hooks:
- id: bandit
args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v21.1.8
rev: v22.1.0
hooks:
- id: clang-format
types: [c]
@ -38,6 +38,7 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict
@ -51,7 +52,7 @@ repos:
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.1
rev: 0.37.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -68,12 +69,12 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.12.1
rev: v2.16.2
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12]

View File

@ -1,10 +1,10 @@
STARTFONT
FONT ÿ
SIZE 10
FONTBOUNDINGBOX
CHARS
FONTBOUNDINGBOX 1 1 0 0
CHARS 1
STARTCHAR
ENCODING
ENCODING 65
BBX 2 5
ENDCHAR
ENDFONT

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -56,7 +56,7 @@ def test_questionable() -> None:
im.load()
if os.path.basename(f) not in supported:
print(f"Please add {f} to the partially supported bmp specs.")
except Exception: # as msg:
except Exception: # noqa: PERF203
if os.path.basename(f) in supported:
raise
@ -106,7 +106,7 @@ def test_good() -> None:
assert_image_similar(im_converted, compare_converted, 5)
except Exception as msg:
except Exception as msg: # noqa: PERF203
# there are three here that are unsupported:
unsupported = (
os.path.join(base, "g", "rgb32bf.bmp"),

View File

@ -145,14 +145,14 @@ class TestFileAvif:
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.88
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 8.62)
assert_image_similar(reloaded, im, 9.28)
def test_AvifEncoder_with_invalid_args(self) -> None:
"""
@ -461,12 +461,9 @@ class TestFileAvif:
@pytest.mark.parametrize(
"advanced",
[
{
"aq-mode": "1",
"enable-chroma-deltaq": "1",
},
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
{"tune": "psnr"},
(("tune", "psnr"),),
[("tune", "psnr")],
],
)
def test_encoder_advanced_codec_options(

View File

@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None:
im.load()
def test_rle_delta() -> None:
with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png")
def test_unsupported_bmp_bitfields_layout() -> None:
fp = io.BytesIO(
o32(40) # header size

View File

@ -179,9 +179,7 @@ def test_iter(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = []
for line in container:
data.append(line)
data = list(container)
# Assert
if bytesmode:

View File

@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
assert reloaded.getpixel((0, 0)) == 255
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("RGB", size)
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(b, "GIF")
@pytest.mark.parametrize(
"path, mode",
(

View File

@ -85,7 +85,7 @@ class TestFileJpeg:
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = tmp_path / "temp.jpg"
im = Image.new("RGB", size)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(f)
def test_app(self) -> None:

View File

@ -148,6 +148,22 @@ def test_prog_res_rt(card: ImageFile.ImageFile) -> None:
assert_image_equal(im, card)
def test_unknown_progression(tmp_path: Path) -> None:
outfile = tmp_path / "temp.jp2"
im = Image.new("1", (1, 1))
with pytest.raises(ValueError, match="unknown progression"):
im.save(outfile, progression="invalid")
def test_unknown_cinema_mode(tmp_path: Path) -> None:
outfile = tmp_path / "temp.jp2"
im = Image.new("1", (1, 1))
with pytest.raises(ValueError, match="unknown cinema mode"):
im.save(outfile, cinema_mode="invalid")
@pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(
card: ImageFile.ImageFile, num_resolutions: int
@ -440,11 +456,19 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
for enumcs in (0, 15, 17):
with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp:
data = bytearray(fp.read())
data[114:115] = bytes([enumcs])
with Image.open(BytesIO(data)) as im:
assert im.mode == "L"
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "CMYK"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0

View File

@ -224,10 +224,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/hopper_g4.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
for tag in im.tag_v2:
try:
del core_items[tag]
except KeyError:
pass
core_items.pop(tag, None)
del core_items[320] # colormap is special, tested below
# Type codes:
@ -1244,7 +1241,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0))
out = tmp_path / "temp.tif"
with pytest.raises(SystemError):
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path: Path) -> None:

View File

@ -6,7 +6,14 @@ from typing import Any
import pytest
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from PIL import (
Image,
ImageFile,
JpegImagePlugin,
MpoImagePlugin,
TiffImagePlugin,
_binary,
)
from .helper import (
assert_image_equal,
@ -145,6 +152,32 @@ def test_parallax() -> None:
assert exif.get_ifd(0x927C)[0xB211] == -3.125
def test_truncated_makernote() -> None:
def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None:
fp = BytesIO()
ifd.save(fp)
e = Image.Exif()
e.load(fp.getvalue())
assert e.get_ifd(37500) == {}
# Nintendo
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[271] = "Nintendo"
ifd[34665] = {37500: b" "}
check(ifd)
# Fujifilm
for data in (
b"FUJIFILM",
b"FUJIFILM" + _binary.o32le(50),
b"FUJIFILM" + _binary.o32le(0),
):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[34665] = {37500: data}
check(ifd)
def test_reload_exif_after_seek() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()

View File

@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = io.BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError):
im.save(b, "PCX")
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3

View File

@ -707,6 +707,16 @@ class TestFilePng:
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 3
def test_plte_cmyk(self, tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
im.putpalette((0, 100, 150, 200), "CMYK")
out = tmp_path / "temp.png"
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.convert("CMYK").getpixel((0, 0)) == (200, 222, 232, 0)
def test_getxmp(self) -> None:
with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None:

View File

@ -68,6 +68,14 @@ def test_save(tmp_path: Path) -> None:
assert im2.format == "SPIDER"
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError, match="cannot write empty image"):
im.save(b, "SPIDER")
def test_tempfile() -> None:
# Arrange
im = hopper()

View File

@ -1,11 +1,12 @@
from __future__ import annotations
import os
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, UnidentifiedImageError
from PIL import Image, UnidentifiedImageError, _binary
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -13,8 +14,6 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
_ORIGINS = ("tl", "bl")
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@ -29,7 +28,7 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
("200x32", "RGBA"),
),
)
@pytest.mark.parametrize("origin", _ORIGINS)
@pytest.mark.parametrize("origin", _ORIGIN_TO_ORIENTATION)
@pytest.mark.parametrize("rle", (True, False))
def test_sanity(
size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path
@ -94,6 +93,25 @@ def test_rgba_16() -> None:
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_v2_no_alpha() -> None:
test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga"
with open(test_file, "rb") as fp:
data = fp.read()
data += (
b"\x00" * 495
+ _binary.o32le(len(data))
+ _binary.o32le(0)
+ b"TRUEVISION-XFILE.\x00"
)
with Image.open(BytesIO(data)) as im:
with Image.open(test_file) as im2:
r, g, b = im2.split()[:3]
a = Image.new("L", im2.size, 255)
expected = Image.merge("RGBA", (r, g, b, a))
assert_image_equal(im, expected)
def test_id_field() -> None:
# tga file with id field
test_file = "Tests/images/tga_id_field.tga"

View File

@ -16,6 +16,7 @@ from PIL import (
TiffImagePlugin,
TiffTags,
UnidentifiedImageError,
_binary,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -941,6 +942,15 @@ class TestFileTiff:
4001,
]
def test_truncated_photoshop_blocks(self) -> None:
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[34377] = b"8BIM"
assert im.get_photoshop_blocks() == {}
im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5
assert im.get_photoshop_blocks() == {}
def test_tiff_chunks(self, tmp_path: Path) -> None:
tmpfile = tmp_path / "temp.tif"

View File

@ -1,11 +1,7 @@
from __future__ import annotations
import pytest
from PIL import Image, ImageDraw, ImageFont
from .helper import skip_unless_feature
class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
@ -18,8 +14,6 @@ class TestFontCrash:
draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
draw.text((10, 10), "Test Text", font=font, fill="#000")
@skip_unless_feature("freetype2")
def test_segfault(self) -> None:
with pytest.raises(OSError):
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)

View File

@ -75,6 +75,16 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None:
assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png")
def test_to_imagefont() -> None:
with open(fontname, "rb") as test_file:
pcffont = PcfFontFile.PcfFontFile(test_file)
imagefont = pcffont.to_imagefont()
im = Image.new("L", (130, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=imagefont)
assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png")
def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import pytest
@ -7,6 +8,15 @@ import pytest
from PIL import FontFile, Image
def test_puti16() -> None:
fp = BytesIO()
FontFile.puti16(fp, (0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
assert fp.getvalue() == (
b"\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04"
b"\x00\x05\x00\x06\x00\x07\x00\x08\x00\t"
)
def test_compile() -> None:
font = FontFile.FontFile()
font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0)))
@ -24,5 +34,11 @@ def test_save(tmp_path: Path) -> None:
tempname = str(tmp_path / "temp.pil")
font = FontFile.FontFile()
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="No bitmap created"):
font.save(tempname)
def test_to_imagefont() -> None:
font = FontFile.FontFile()
with pytest.raises(ValueError, match="No bitmap created"):
font.to_imagefont()

View File

@ -91,6 +91,21 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
assert im.palette.colors == {(1, 2, 3, 4): 0}
@pytest.mark.parametrize(
"mode, palette",
(
("CMYK", (1, 2, 3, 4)),
("CMYKX", (1, 2, 3, 4, 0)),
),
)
def test_cmyk_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [250, 249, 248]
assert im.palette is not None
assert im.palette.colors == {(1, 2, 3, 4): 0}
def test_empty_palette() -> None:
im = Image.new("P", (1, 1))
assert im.getpalette() == []

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from io import BytesIO
import io
from pathlib import Path
import pytest
@ -23,6 +23,13 @@ def test_reload() -> None:
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
def test_save_fp() -> None:
palette = ImagePalette.ImagePalette()
with io.StringIO() as fp:
palette.save(fp)
assert not fp.closed
def test_getcolor() -> None:
palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0
@ -204,7 +211,7 @@ def test_2bit_palette(tmp_path: Path) -> None:
def test_getpalette() -> None:
b = BytesIO(b"0 1\n1 2 3 4")
b = io.BytesIO(b"0 1\n1 2 3 4")
p = PaletteFile.PaletteFile(b)
palette, rawmode = p.getpalette()
@ -216,6 +223,6 @@ def test_invalid_palette() -> None:
with pytest.raises(OSError):
ImagePalette.load("Tests/images/hopper.jpg")
b = BytesIO(b"1" * 101)
b = io.BytesIO(b"1" * 101)
with pytest.raises(SyntaxError, match="bad palette file"):
PaletteFile.PaletteFile(b)

View File

@ -5,7 +5,10 @@ archive=$1
url=$2
if [ ! -f $archive.tar.gz ]; then
wget --no-verbose -O $archive.tar.gz $url
wget -O $archive.tar.gz $url \
--no-verbose \
--retry-connrefused \
--retry-on-http-error=429,503,504
fi
rmdir $archive

View File

@ -1,68 +1,86 @@
#!/usr/bin/env bash
set -eo pipefail
version=1.3.0
version=1.4.1
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then
pushd libavif-$version
LIBDIR=/usr/lib/x86_64-linux-gnu
# Apply patch for SVT-AV1 4.0 compatibility
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
patch -p1 < ../libavif-svt4.patch
# Copy cached files into place
sudo cp ~/cache-libavif/lib/* $LIBDIR/
sudo cp -r ~/cache-libavif/include/avif /usr/include/
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
pushd libavif-$version
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
sudo make install
if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then
# Copy to cache
LIBDIR=/usr/lib/x86_64-linux-gnu
rm -rf ~/cache-libavif
mkdir -p ~/cache-libavif/lib
mkdir -p ~/cache-libavif/include
cp $LIBDIR/libavif.so* ~/cache-libavif/lib/
cp -r /usr/include/avif ~/cache-libavif/include/
fi
popd
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
make install
popd

View File

@ -3,10 +3,30 @@
archive=libwebp-1.6.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then
pushd $archive
# Copy cached files into place
sudo cp ~/cache-libwebp/lib/* /usr/lib/
sudo cp -r ~/cache-libwebp/include/webp /usr/include/
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
else
popd
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-libwebp
mkdir -p ~/cache-libwebp/lib
mkdir -p ~/cache-libwebp/include
cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/
cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/
cp -r /usr/include/webp ~/cache-libwebp/include/
fi
popd
fi

View File

@ -1,14 +0,0 @@
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -162,7 +162,11 @@ static avifResult svtCodecEncodeImage(avifEncoder * encoder,
#else
svt_config->logical_processors = encoder->maxThreads;
#endif
+#if SVT_AV1_CHECK_VERSION(4, 0, 0)
+ svt_config->aq_mode = 2;
+#else
svt_config->enable_adaptive_quantization = 2;
+#endif
// disable 2-pass
#if SVT_AV1_CHECK_VERSION(0, 9, 0)
svt_config->rc_stats_buffer = (SvtAv1FixedBuf) { NULL, 0 };

View File

@ -7,6 +7,7 @@ itself.
Here is a list of PyPI projects that offer additional plugins:
* :pypi:`amigainfo`: Adds support for Amiga Workbench .info icon files.
* :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs.
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.

View File

@ -39,15 +39,15 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 15 Sequoia | 3.10 | x86-64 |
| +----------------------------+---------------------+
| | 3.11, 3.12, 3.13, 3.14, | arm64 |
| | PyPy3 | |
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 |
| | 3.15, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| macOS 26 Tahoe | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| | 3.14, 3.15, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
@ -55,7 +55,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.10 | x86 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | |
| | 3.15, PyPy3 | |
| +----------------------------+---------------------+
| | 3.13 (MinGW) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -8,10 +8,14 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance
this class store bitmap fonts, and are used with the
:py:meth:`PIL.ImageDraw.ImageDraw.text` method.
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
`pilfont.py <https://github.com/python-pillow/pillow-scripts/blob/main/Scripts/pilfont.py>`_
from :pypi:`pillow-scripts` to convert BDF and
PCF font descriptors (X window font formats) to this format.
Pillow uses its own font file format to store bitmap fonts, limited to 256 characters. You
can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font
descriptors (X Window font formats) to this format::
from PIL import PcfFontFile
with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp:
font = PcfFontFile.PcfFontFile(fp)
imagefont = font.to_imagefont()
Starting with version 1.1.4, PIL can be configured to support TrueType and
OpenType fonts (as well as other font formats supported by the FreeType

View File

@ -14,33 +14,34 @@ TODO
TODO
Backwards incompatible changes
==============================
TODO
^^^^
TODO
Deprecations
============
TODO
^^^^
TODO
API changes
===========
TODO
^^^^
Error when encoding an empty image
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Attempting to encode an image with zero width or height would previously raise
a :py:exc:`SystemError`. That has now been changed to a :py:exc:`ValueError`.
This does not add any new errors. SGI, ICNS and ICO formats are still able to
save (0, 0) images.
API additions
=============
FontFile.to_imagefont()
^^^^^^^^^^^^^^^^^^^^^^^
:py:class:`~PIL.FontFile.FontFile` instances can now be directly converted to
:py:class:`~PIL.ImageFont.ImageFont` instances::
>>> from PIL import PcfFontFile
>>> with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp:
... pcffont = PcfFontFile.PcfFontFile(fp)
... pcffont.to_imagefont()
...
<PIL.ImageFont.ImageFont object at 0x10457bb80>
ImageText.Text.wrap
^^^^^^^^^^^^^^^^^^^
@ -69,7 +70,8 @@ or scaling, optionally with a font size limit::
Other changes
=============
TODO
^^^^
Support reading JPEG2000 images with CMYK palettes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
JPEG2000 images with CMYK palettes can now be read. This is the first integration of
CMYK palettes into Pillow.

View File

@ -62,7 +62,6 @@ optional-dependencies.test-arrow = [
"nanoarrow",
"pyarrow",
]
optional-dependencies.tests = [
"check-manifest",
"coverage>=7.4.2",
@ -77,16 +76,15 @@ optional-dependencies.tests = [
"pytest-xdist",
"trove-classifiers>=2024.10.12",
]
optional-dependencies.xmp = [
"defusedxml",
]
urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
urls.Changelog = "https://github.com/python-pillow/Pillow/releases"
urls.Documentation = "https://pillow.readthedocs.io"
urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
urls.Homepage = "https://python-pillow.github.io"
urls.Mastodon = "https://fosstodon.org/@pillow"
urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
urls.Source = "https://github.com/python-pillow/Pillow"
[tool.setuptools]
@ -95,70 +93,50 @@ packages = [
]
include-package-data = true
package-dir = { "" = "src" }
[tool.setuptools.dynamic]
version = { attr = "PIL.__version__" }
dynamic.version = { attr = "PIL.__version__" }
[tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
test-requires = [
"numpy",
]
xbuild-tools = [ ]
[tool.cibuildwheel.ios]
xbuild-tools = []
# Disable platform guessing on iOS, and disable raqm (since there won't be a
# vendor version, and we can't distribute it due to licensing)
config-settings = "raqm=disable imagequant=disable platform-guessing=disable"
ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable"
# iOS needs to be given a specific pytest invocation and list of test sources.
test-sources = [
ios.test-sources = [
"checks",
"Tests",
"selftest.py",
]
test-command = [
ios.test-command = [
"python -m selftest",
"python -m pytest -vv -x -W always checks/check_wheel.py Tests",
]
# There's no numpy wheel for iOS (yet...)
test-requires = [ ]
[tool.cibuildwheel.macos]
ios.test-requires = []
# Disable platform guessing on macOS to avoid picking up Homebrew etc.
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
[tool.cibuildwheel.macos.environment]
macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
# Isolate macOS build environment from Homebrew etc.
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
[[tool.cibuildwheel.overrides]]
# iOS environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphoneos"
environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH"
[[tool.cibuildwheel.overrides]]
# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphonesimulator"
environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH"
[[tool.cibuildwheel.overrides]]
select = "*-win32"
test-requires = [ ]
macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
overrides = [
# iOS environment is isolated by cibuildwheel, but needs the dependencies
{ select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" },
# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies
{ select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" },
{ select = "*-win32", test-requires = [] },
]
[tool.black]
exclude = "wheels/multibuild"
[tool.ruff]
exclude = [ "wheels/multibuild" ]
fix = true
lint.select = [
"C4", # flake8-comprehensions
@ -168,6 +146,7 @@ lint.select = [
"I", # isort
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PERF", # perflint
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PT", # flake8-pytest-style
@ -207,8 +186,8 @@ lint.isort.required-imports = [
[tool.pyproject-fmt]
max_supported_python = "3.14"
[tool.pytest.ini_options]
addopts = "-ra --color=auto"
[tool.pytest]
addopts = [ "-ra", "--color=auto" ]
testpaths = [
"Tests",
]

View File

@ -302,7 +302,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None:
subprocess.check_output(command_cflags).decode("utf8").strip(),
)[::2][1:]
return libs, cflags
except Exception:
except Exception: # noqa: PERF203
pass
return None
@ -1078,10 +1078,10 @@ libraries: list[tuple[str, _BuildInfo]] = [
]
files: list[str | os.PathLike[str]] = ["src/_imaging.c"]
for src_file in _IMAGING:
files.append("src/" + src_file + ".c")
for src_file in _LIB_IMAGING:
files.append(os.path.join("src/libImaging", src_file + ".c"))
files.extend("src/" + src_file + ".c" for src_file in _IMAGING)
files.extend(
os.path.join("src/libImaging", src_file + ".c") for src_file in _LIB_IMAGING
)
ext_modules = [
Extension("PIL._imaging", files),
Extension("PIL._imagingft", ["src/_imagingft.c"]),

View File

@ -369,7 +369,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
right, up = bytes_read
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:

View File

@ -289,6 +289,7 @@ class Base(IntEnum):
OpcodeList2 = 0xC741
OpcodeList3 = 0xC74E
NoiseProfile = 0xC761
FrameRate = 0xC764
"""Maps EXIF tags to tag names."""

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import os
from typing import BinaryIO
from . import Image, _binary
from . import Image, ImageFont, _binary
WIDTH = 800
@ -110,6 +110,22 @@ class FontFile:
self.bitmap.paste(im.crop(src), s)
self.metrics[i] = d, dst, s
def _encode_metrics(self) -> bytes:
values: list[int] = []
for i in range(256):
m = self.metrics[i]
if m:
values.extend(m[0] + m[1] + m[2])
else:
values.extend((0,) * 10)
data = bytearray()
for v in values:
if v < 0:
v += 65536
data += _binary.o16be(v)
return bytes(data)
def save(self, filename: str) -> None:
"""Save font"""
@ -126,9 +142,18 @@ class FontFile:
fp.write(b"PILfont\n")
fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
fp.write(b"DATA\n")
for id in range(256):
m = self.metrics[id]
if not m:
puti16(fp, (0,) * 10)
else:
puti16(fp, m[0] + m[1] + m[2])
fp.write(self._encode_metrics())
def to_imagefont(self) -> ImageFont.ImageFont:
"""Convert to ImageFont"""
self.compile()
# font data
if not self.bitmap:
msg = "No bitmap created"
raise ValueError(msg)
imagefont = ImageFont.ImageFont()
imagefont._load(self.bitmap, self._encode_metrics())
return imagefont

View File

@ -164,13 +164,13 @@ class GifImageFile(ImageFile.ImageFile):
self._seek(0)
last_frame = self.__frame
for f in range(self.__frame + 1, frame + 1):
try:
try:
for f in range(self.__frame + 1, frame + 1):
self._seek(f)
except EOFError as e:
self.seek(last_frame)
msg = "no more images in GIF file"
raise EOFError(msg) from e
except EOFError as e:
self.seek(last_frame)
msg = "no more images in GIF file"
raise EOFError(msg) from e
def _seek(self, frame: int, update_image: bool = True) -> None:
if isinstance(self._fp, DeferredError):
@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
if im.mode in ("P", "L") and info and info.get("optimize"):
if (
im.mode in ("P", "L")
and info
and info.get("optimize")
and im.width != 0
and im.height != 0
):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette

View File

@ -80,8 +80,7 @@ def read_32(
if byte_int & 0x80:
blocksize = byte_int - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
data.extend([byte] * blocksize)
else:
blocksize = byte_int + 1
data.append(fobj.read(blocksize))

View File

@ -488,7 +488,7 @@ def init() -> bool:
try:
logger.debug("Importing %s", plugin)
__import__(f"{__spec__.parent}.{plugin}", globals(), locals(), [])
except ImportError as e:
except ImportError as e: # noqa: PERF203
logger.debug("Image: failed to import %s: %s", plugin, e)
if OPEN or SAVE:
@ -885,7 +885,7 @@ class Image:
# unpack data
e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im)
e.setimage(self.im, (0, 0) + self.size)
from . import ImageFile
@ -956,7 +956,7 @@ class Image:
# unpack data
d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im)
d.setimage(self.im, (0, 0) + self.size)
s = d.decode(data)
if s[0] >= 0:
@ -2145,8 +2145,8 @@ class Image:
Alternatively, an 8-bit string may be used instead of an integer sequence.
:param data: A palette sequence (either a list or a string).
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode
that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L").
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", "CMYK", or a
mode that can be transformed to one of those modes (e.g. "R", "RGBA;L").
"""
from . import ImagePalette
@ -2165,7 +2165,12 @@ class Image:
palette = ImagePalette.raw(rawmode, data)
self._mode = "PA" if "A" in self.mode else "P"
self.palette = palette
self.palette.mode = "RGBA" if "A" in rawmode else "RGB"
if rawmode.startswith("CMYK"):
self.palette.mode = "CMYK"
elif "A" in rawmode:
self.palette.mode = "RGBA"
else:
self.palette.mode = "RGB"
self.load() # install new palette
def putpixel(
@ -4229,80 +4234,83 @@ class Exif(_ExifBase):
if tag == ExifTags.IFD.MakerNote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data.startswith(b"FUJIFILM"):
ifd_offset = i32le(tag_data, 8)
ifd_data = tag_data[ifd_offset:]
try:
if tag_data.startswith(b"FUJIFILM"):
ifd_offset = i32le(tag_data, 8)
ifd_data = tag_data[ifd_offset:]
makernote = {}
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
try:
(
unit_size,
handler,
) = ImageFileDirectory_v2._load_dispatch[typ]
except KeyError:
continue
size = count * unit_size
if size > 4:
(offset,) = struct.unpack("<L", data)
data = ifd_data[offset - 12 : offset + size - 12]
else:
data = data[:size]
if len(data) != size:
warnings.warn(
"Possibly corrupt EXIF MakerNote data. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
makernote = {}
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
continue
try:
(
unit_size,
handler,
) = ImageFileDirectory_v2._load_dispatch[typ]
except KeyError:
continue
size = count * unit_size
if size > 4:
(offset,) = struct.unpack("<L", data)
data = ifd_data[offset - 12 : offset + size - 12]
else:
data = data[:size]
if not data:
continue
if len(data) != size:
warnings.warn(
"Possibly corrupt EXIF MakerNote data. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
)
continue
makernote[ifd_tag] = handler(
ImageFileDirectory_v2(), data, False
)
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
if ifd_tag == 0x1101:
# CameraInfo
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
if not data:
continue
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
makernote[ifd_tag] = handler(
ImageFileDirectory_v2(), data, False
)
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
if ifd_tag == 0x1101:
# CameraInfo
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
self.fp.read(4)
# Seconds since 2000
camerainfo["TimeStamp"] = i32le(self.fp.read(12))
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4)
camerainfo["InternalSerialNumber"] = self.fp.read(4)
self.fp.read(4)
# Seconds since 2000
camerainfo["TimeStamp"] = i32le(self.fp.read(12))
self.fp.read(12)
parallax = self.fp.read(4)
handler = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)[0]
self.fp.read(4)
camerainfo["InternalSerialNumber"] = self.fp.read(4)
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
self.fp.read(12)
parallax = self.fp.read(4)
handler = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)[0]
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
except struct.error:
pass
else:
# Interop
ifd = self._get_ifd_dict(tag_data, tag)

View File

@ -215,8 +215,10 @@ class ImageFile(Image.Image):
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifds = [
(exif._get_ifd_dict(subifd_offset), subifd_offset)
for subifd_offset in subifd_offsets
]
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None
@ -579,10 +581,7 @@ class Parser:
pass # not enough data
else:
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
if flag or len(im.tile) != 1:
# custom load code, or multiple tiles
self.decode = None
else:
if not flag and len(im.tile) == 1:
# initialize decoder
im.load_prepare()
d, e, o, a = im.tile[0]

View File

@ -149,6 +149,9 @@ class ImageFont:
# read PILfont metrics
data = file.read(256 * 20)
self._load(image, data)
def _load(self, image: Image.Image, data: bytes) -> None:
image.load()
self.font = Image.core.font(image.im, data)
@ -927,7 +930,7 @@ def load_path(filename: str | bytes) -> ImageFont:
for directory in sys.path:
try:
return load(os.path.join(directory, filename))
except OSError:
except OSError: # noqa: PERF203
pass
msg = f'cannot find font file "{filename}" in sys.path'
if os.path.exists(filename):

View File

@ -191,19 +191,22 @@ class ImagePalette:
if self.rawmode:
msg = "palette contains raw palette data"
raise ValueError(msg)
open_fp = False
if isinstance(fp, str):
fp = open(fp, "w")
fp.write("# Palette\n")
fp.write(f"# Mode: {self.mode}\n")
for i in range(256):
fp.write(f"{i}")
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
try:
fp.write(f" {self.palette[j]}")
except IndexError:
fp.write(" 0")
fp.write("\n")
fp.close()
open_fp = True
try:
fp.write("# Palette\n")
fp.write(f"# Mode: {self.mode}\n")
palette_len = len(self.palette)
for i in range(256):
fp.write(f"{i}")
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
fp.write(f" {self.palette[j] if j < palette_len else 0}")
fp.write("\n")
finally:
if open_fp:
fp.close()
# --------------------------------------------------------------------

View File

@ -185,13 +185,9 @@ def getiptcinfo(
data = None
info: dict[tuple[int, int], bytes | list[bytes]] = {}
if isinstance(im, IptcImageFile):
# return info dictionary right away
for k, v in im.info.items():
if isinstance(k, tuple):
info[k] = v
return info
return {k: v for k, v in im.info.items() if isinstance(k, tuple)}
elif isinstance(im, JpegImagePlugin.JpegImageFile):
# extract the IPTC/NAA resource
@ -227,7 +223,4 @@ def getiptcinfo(
except (IndexError, KeyError):
pass # expected failure
for k, v in iptc_im.info.items():
if isinstance(k, tuple):
info[k] = v
return info
return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)}

View File

@ -176,6 +176,7 @@ def _parse_jp2_header(
nc = None
dpi = None # 2-tuple of DPI info, or None
palette = None
colr = None
while header.has_next_box():
tbox = header.next_box_type()
@ -196,11 +197,18 @@ def _parse_jp2_header(
mode = "RGB"
elif nc == 4:
mode = "RGBA"
elif tbox == b"colr" and nc == 4:
elif tbox == b"colr":
meth, _, _, enumcs = header.read_fields(">BBBI")
if meth == 1 and enumcs == 12:
mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"):
if meth == 1:
if enumcs in (0, 15):
colr = "1"
elif enumcs == 12:
colr = "CMYK"
if nc == 4:
mode = "CMYK"
elif enumcs == 17:
colr = "L"
elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"):
ne, npc = header.read_fields(">HB")
assert isinstance(ne, int)
assert isinstance(npc, int)
@ -210,7 +218,11 @@ def _parse_jp2_header(
if bitdepth > max_bitdepth:
max_bitdepth = bitdepth
if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB")
if npc == 4:
palette_mode = "CMYK" if colr == "CMYK" else "RGBA"
else:
palette_mode = "RGB"
palette = ImagePalette.ImagePalette(palette_mode)
for i in range(ne):
color: list[int] = []
for value in header.read_fields(">" + ("B" * npc)):

View File

@ -127,8 +127,8 @@ def APP(self: JpegImageFile, marker: int) -> None:
# parse the image resource block
offset = 14
photoshop = self.info.setdefault("photoshop", {})
while s[offset : offset + 4] == b"8BIM":
try:
try:
while s[offset : offset + 4] == b"8BIM":
offset += 4
# resource code
code = i16(s, offset)
@ -153,8 +153,8 @@ def APP(self: JpegImageFile, marker: int) -> None:
photoshop[code] = data
offset += size
offset += offset & 1 # align
except struct.error:
break # insufficient data
except struct.error:
pass # insufficient data
elif marker == 0xFFEE and s.startswith(b"Adobe"):
self.info["adobe"] = i16(s, 5)
@ -661,10 +661,6 @@ def get_sampling(im: Image.Image) -> int:
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0:
msg = "cannot write empty image as JPEG"
raise ValueError(msg)
try:
rawmode = RAWMODE[im.mode]
except KeyError as e:
@ -742,17 +738,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if not (0 < len(qtables) < 5):
msg = "None or too many quantization tables"
raise ValueError(msg)
for idx, table in enumerate(qtables):
try:
try:
for idx, table in enumerate(qtables):
if len(table) != 64:
msg = "Invalid quantization table"
raise TypeError(msg)
table_array = array.array("H", table)
except TypeError as e:
msg = "Invalid quantization table"
raise ValueError(msg) from e
else:
qtables[idx] = list(table_array)
qtables[idx] = list(array.array("H", table))
except TypeError as e:
msg = "Invalid quantization table"
raise ValueError(msg) from e
return qtables
if qtables == "keep":

View File

@ -59,11 +59,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ b"MPF\0"
+ b" " * ifd_length
)
exif = im_frame.encoderinfo.get("exif")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
im_frame.encoderinfo["exif"] = exif
if exif:
if exif := im_frame.encoderinfo.get("exif"):
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
im_frame.encoderinfo["exif"] = exif
mpf_offset += 4 + len(exif)
JpegImagePlugin._save(im_frame, fp, filename)

View File

@ -251,7 +251,7 @@ class PcfFontFile(FontFile.FontFile):
]
if encoding_offset != 0xFFFF:
encoding[i] = encoding_offset
except UnicodeDecodeError:
except UnicodeDecodeError: # noqa: PERF203
# character is not supported in selected encoding
pass

View File

@ -146,6 +146,10 @@ SAVE = {
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0:
msg = "Cannot write empty image as PCX"
raise ValueError(msg)
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:

View File

@ -866,13 +866,13 @@ class PngImageFile(ImageFile.ImageFile):
self._seek(0, True)
last_frame = self.__frame
for f in range(self.__frame + 1, frame + 1):
try:
try:
for f in range(self.__frame + 1, frame + 1):
self._seek(f)
except EOFError as e:
self.seek(last_frame)
msg = "no more images in APNG file"
raise EOFError(msg) from e
except EOFError as e:
self.seek(last_frame)
msg = "no more images in APNG file"
raise EOFError(msg) from e
def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
@ -1353,6 +1353,9 @@ def _save(
mode = im.mode
outmode = mode
palette = []
if im.palette:
palette = im.getpalette() or []
if mode == "P":
#
# attempt to minimize storage requirements for palette images
@ -1362,7 +1365,7 @@ def _save(
else:
# check palette contents
if im.palette:
colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
colors = max(min(len(palette) // 3, 256), 1)
else:
colors = 256
@ -1403,8 +1406,7 @@ def _save(
chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
if icc:
if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")):
# ICC profile
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
@ -1419,8 +1421,7 @@ def _save(
# Disallow sRGB chunks when an iCCP-chunk has been emitted.
chunks.remove(b"sRGB")
info = im.encoderinfo.get("pnginfo")
if info:
if info := im.encoderinfo.get("pnginfo"):
chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
for info_chunk in info.chunks:
cid, data = info_chunk[:2]
@ -1437,7 +1438,7 @@ def _save(
if im.mode == "P":
palette_byte_number = colors * 3
palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
palette_bytes = bytes(palette[:palette_byte_number])
while len(palette_bytes) < palette_byte_number:
palette_bytes += b"\0"
chunk(fp, b"PLTE", palette_bytes)
@ -1472,8 +1473,7 @@ def _save(
alpha_bytes = colors
chunk(fp, b"tRNS", alpha[:alpha_bytes])
dpi = im.encoderinfo.get("dpi")
if dpi:
if dpi := im.encoderinfo.get("dpi"):
chunk(
fp,
b"pHYs",
@ -1490,8 +1490,7 @@ def _save(
chunks.remove(cid)
chunk(fp, cid, data)
exif = im.encoderinfo.get("exif")
if exif:
if exif := im.encoderinfo.get("exif"):
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
if exif.startswith(b"Exif\x00\x00"):

View File

@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header
lenbyt = max(1, nsam) * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt)
if 1024 % lenbyt != 0:
labrec += 1

View File

@ -17,11 +17,13 @@
#
from __future__ import annotations
import os
import warnings
from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
@ -157,6 +159,20 @@ class TgaImageFile(ImageFile.ImageFile):
pass # cannot decode
def load_end(self) -> None:
if self.mode == "RGBA":
assert self.fp is not None
self.fp.seek(-26, os.SEEK_END)
footer = self.fp.read(26)
if footer.endswith(b"TRUEVISION-XFILE.\x00"):
# version 2
extension_offset = i32(footer)
if extension_offset:
self.fp.seek(extension_offset + 494)
attributes_type = self.fp.read(1)
if attributes_type == b"\x00":
# No alpha
self.im.fillband(3, 255)
if self._flip_horizontally:
self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)

View File

@ -1287,10 +1287,13 @@ class TiffImageFile(ImageFile.ImageFile):
blocks = {}
val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val.startswith(b"8BIM"):
while val.startswith(b"8BIM") and len(val) >= 12:
id = i16(val[4:6])
n = math.ceil((val[6] + 1) / 2) * 2
size = i32(val[6 + n : 10 + n])
try:
size = i32(val[6 + n : 10 + n])
except struct.error:
break
data = val[10 + n : 10 + n + size]
blocks[id] = {"data": data}

View File

@ -124,8 +124,10 @@ PyImagingPhotoPut(
if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) {
block.pixelSize = 1;
block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0;
} else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) {
} else if (
im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa
) {
block.pixelSize = 4;
block.offset[0] = 0;
block.offset[1] = 1;

View File

@ -425,7 +425,7 @@ end:
return (PyObject *)self;
}
PyObject *
void
_encoder_dealloc(AvifEncoderObject *self) {
if (self->encoder) {
avifEncoderDestroy(self->encoder);
@ -433,7 +433,7 @@ _encoder_dealloc(AvifEncoderObject *self) {
if (self->image) {
avifImageDestroy(self->image);
}
Py_RETURN_NONE;
Py_TYPE(self)->tp_free(self);
}
PyObject *
@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
frame = image;
} else {
frame = avifImageCreateEmpty();
if (image == NULL) {
if (frame == NULL) {
PyErr_SetString(PyExc_ValueError, "Image creation failed");
return NULL;
}
@ -687,13 +687,13 @@ AvifDecoderNew(PyObject *self_, PyObject *args) {
return (PyObject *)self;
}
PyObject *
void
_decoder_dealloc(AvifDecoderObject *self) {
if (self->decoder) {
avifDecoderDestroy(self->decoder);
}
PyBuffer_Release(&self->buffer);
Py_RETURN_NONE;
Py_TYPE(self)->tp_free(self);
}
PyObject *
@ -708,15 +708,27 @@ _decoder_get_info(AvifDecoderObject *self) {
if (image->xmp.size) {
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
if (!xmp) {
return NULL;
}
}
if (image->exif.size) {
exif =
PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size);
if (!exif) {
Py_XDECREF(xmp);
return NULL;
}
}
if (image->icc.size) {
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
if (!icc) {
Py_XDECREF(xmp);
Py_XDECREF(exif);
return NULL;
}
}
ret = Py_BuildValue(
@ -799,6 +811,7 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
avifRGBImageFreePixels(&rgb);
return NULL;
}
@ -806,6 +819,9 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);
avifRGBImageFreePixels(&rgb);
if (!bytes) {
return NULL;
}
ret = Py_BuildValue(
"SKKK",

View File

@ -254,6 +254,9 @@ void
ReleaseArrowSchemaPyCapsule(PyObject *capsule) {
struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema");
if (!schema) {
return;
}
if (schema->release != NULL) {
schema->release(schema);
}
@ -276,6 +279,9 @@ void
ReleaseArrowArrayPyCapsule(PyObject *capsule) {
struct ArrowArray *array =
(struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array");
if (!array) {
return;
}
if (array->release != NULL) {
array->release(array);
}
@ -1216,7 +1222,9 @@ _getxy(PyObject *xy, int *x, int *y) {
PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL);
if (int_value != NULL && PyLong_Check(int_value)) {
*x = PyLong_AS_LONG(int_value);
Py_DECREF(int_value);
} else {
Py_XDECREF(int_value);
goto badval;
}
}
@ -1230,7 +1238,9 @@ _getxy(PyObject *xy, int *x, int *y) {
PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL);
if (int_value != NULL && PyLong_Check(int_value)) {
*y = PyLong_AS_LONG(int_value);
Py_DECREF(int_value);
} else {
Py_XDECREF(int_value);
goto badval;
}
}
@ -2469,6 +2479,9 @@ _split(ImagingObject *self) {
}
list = PyTuple_New(self->image->bands);
if (!list) {
return NULL;
}
for (i = 0; i < self->image->bands; i++) {
imaging_object = PyImagingNew(bands[i]);
if (!imaging_object) {
@ -3765,6 +3778,9 @@ _ptr_destructor(PyObject *capsule) {
static PyObject *
_getattr_ptr(ImagingObject *self, void *closure) {
PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor);
if (!capsule) {
return NULL;
}
Py_INCREF(self);
PyCapsule_SetContext(capsule, self);
return capsule;
@ -4320,8 +4336,9 @@ setup_module(PyObject *m) {
#else
have_libjpegturbo = Py_False;
#endif
Py_INCREF(have_libjpegturbo);
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
if (PyModule_AddObjectRef(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo) < 0) {
return -1;
}
PyObject *have_mozjpeg;
#ifdef JPEG_C_PARAM_SUPPORTED
@ -4329,8 +4346,9 @@ setup_module(PyObject *m) {
#else
have_mozjpeg = Py_False;
#endif
Py_INCREF(have_mozjpeg);
PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg);
if (PyModule_AddObjectRef(m, "HAVE_MOZJPEG", have_mozjpeg) < 0) {
return -1;
}
PyObject *have_libimagequant;
#ifdef HAVE_LIBIMAGEQUANT
@ -4344,8 +4362,9 @@ setup_module(PyObject *m) {
#else
have_libimagequant = Py_False;
#endif
Py_INCREF(have_libimagequant);
PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant);
if (PyModule_AddObjectRef(m, "HAVE_LIBIMAGEQUANT", have_libimagequant) < 0) {
return -1;
}
#ifdef HAVE_LIBZ
/* zip encoding strategies */
@ -4373,8 +4392,9 @@ setup_module(PyObject *m) {
#else
have_zlibng = Py_False;
#endif
Py_INCREF(have_zlibng);
PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng);
if (PyModule_AddObjectRef(m, "HAVE_ZLIBNG", have_zlibng) < 0) {
return -1;
}
#ifdef HAVE_LIBTIFF
{
@ -4391,8 +4411,9 @@ setup_module(PyObject *m) {
#else
have_xcb = Py_False;
#endif
Py_INCREF(have_xcb);
PyModule_AddObject(m, "HAVE_XCB", have_xcb);
if (PyModule_AddObjectRef(m, "HAVE_XCB", have_xcb) < 0) {
return -1;
}
PyObject *pillow_version = PyUnicode_FromString(version);
PyDict_SetItemString(

View File

@ -558,7 +558,13 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!imOut) {
return NULL;
}
return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform));
}
@ -1447,14 +1453,14 @@ setup_module(PyObject *m) {
int vn;
/* Ready object types */
PyType_Ready(&CmsProfile_Type);
PyType_Ready(&CmsTransform_Type);
if (PyType_Ready(&CmsProfile_Type) < 0 || PyType_Ready(&CmsTransform_Type) < 0) {
return -1;
}
Py_INCREF(&CmsProfile_Type);
PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type);
Py_INCREF(&CmsTransform_Type);
PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type);
if (PyModule_AddObjectRef(m, "CmsProfile", (PyObject *)&CmsProfile_Type) < 0 ||
PyModule_AddObjectRef(m, "CmsTransform", (PyObject *)&CmsTransform_Type) < 0) {
return -1;
}
d = PyModule_GetDict(m);

View File

@ -941,8 +941,18 @@ font_render(FontObject *self, PyObject *args) {
return NULL;
}
PyObject *imagePtr = PyObject_GetAttrString(image, "ptr");
if (!imagePtr) {
PyMem_Del(glyph_info);
Py_DECREF(image);
return NULL;
}
im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC);
Py_XDECREF(imagePtr);
Py_DECREF(imagePtr);
if (!im) {
PyMem_Del(glyph_info);
Py_DECREF(image);
return NULL;
}
x_offset = round(x_offset - stroke_width);
y_offset = round(y_offset - stroke_width);
@ -1042,130 +1052,99 @@ font_render(FontObject *self, PyObject *args) {
yy = -(py + glyph_slot->bitmap_top);
}
// Null buffer, is dereferenced in FT_Bitmap_Convert
if (!bitmap.buffer && bitmap.rows) {
PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph");
goto glyph_error;
}
/* convert non-8bpp bitmaps */
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
convert_scale = 255;
break;
case FT_PIXEL_MODE_GRAY2:
convert_scale = 255 / 3;
break;
case FT_PIXEL_MODE_GRAY4:
convert_scale = 255 / 15;
break;
default:
convert_scale = 1;
}
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
case FT_PIXEL_MODE_GRAY2:
case FT_PIXEL_MODE_GRAY4:
if (!bitmap_converted_ready) {
FT_Bitmap_Init(&bitmap_converted);
bitmap_converted_ready = 1;
}
error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1);
if (error) {
geterror(error);
goto glyph_error;
}
bitmap = bitmap_converted;
/* bitmap is now FT_PIXEL_MODE_GRAY, fall through */
case FT_PIXEL_MODE_GRAY:
break;
case FT_PIXEL_MODE_BGRA:
if (color) {
if (bitmap.buffer) {
/* convert non-8bpp bitmaps */
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
convert_scale = 255;
break;
}
/* we didn't ask for color, fall through to default */
default:
PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
/* clip glyph bitmap width to target image bounds */
x0 = 0;
x1 = bitmap.width;
if (xx < 0) {
x0 = -xx;
}
if (xx + x1 > im->xsize) {
x1 = im->xsize - xx;
}
source = (unsigned char *)bitmap.buffer;
for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) {
/* clip glyph bitmap height to target image bounds */
if (yy >= 0 && yy < im->ysize) {
/* blend this glyph into the buffer */
int k;
unsigned char *target;
unsigned int tmp;
if (color) {
/* target[RGB] returns the color, target[A] returns the mask */
/* target bands get split again in ImageDraw.text */
target = (unsigned char *)im->image[yy] + xx * 4;
} else {
target = im->image8[yy] + xx;
}
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
/* paste color glyph */
for (k = x0; k < x1; k++) {
unsigned int src_alpha = source[k * 4 + 3];
/* paste only if source has data */
if (src_alpha > 0) {
/* unpremultiply BGRa */
int src_red =
CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
int src_green =
CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
int src_blue =
CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);
/* blend required if target has data */
if (target[k * 4 + 3] > 0) {
/* blend RGBA colors */
target[k * 4 + 0] =
BLEND(src_alpha, target[k * 4 + 0], src_red, tmp);
target[k * 4 + 1] =
BLEND(src_alpha, target[k * 4 + 1], src_green, tmp);
target[k * 4 + 2] =
BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp);
target[k * 4 + 3] = CLIP8(
src_alpha +
MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp)
);
} else {
/* paste unpremultiplied RGBA values */
target[k * 4 + 0] = src_red;
target[k * 4 + 1] = src_green;
target[k * 4 + 2] = src_blue;
target[k * 4 + 3] = src_alpha;
}
}
case FT_PIXEL_MODE_GRAY2:
convert_scale = 255 / 3;
break;
case FT_PIXEL_MODE_GRAY4:
convert_scale = 255 / 15;
break;
default:
convert_scale = 1;
}
switch (bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
case FT_PIXEL_MODE_GRAY2:
case FT_PIXEL_MODE_GRAY4:
if (!bitmap_converted_ready) {
FT_Bitmap_Init(&bitmap_converted);
bitmap_converted_ready = 1;
}
} else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1);
if (error) {
geterror(error);
goto glyph_error;
}
bitmap = bitmap_converted;
/* bitmap is now FT_PIXEL_MODE_GRAY, fall through */
case FT_PIXEL_MODE_GRAY:
break;
case FT_PIXEL_MODE_BGRA:
if (color) {
unsigned char *ink = (unsigned char *)&foreground_ink;
break;
}
/* we didn't ask for color, fall through to default */
default:
PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
/* clip glyph bitmap width to target image bounds */
x0 = 0;
x1 = bitmap.width;
if (xx < 0) {
x0 = -xx;
}
if (xx + x1 > im->xsize) {
x1 = im->xsize - xx;
}
source = (unsigned char *)bitmap.buffer;
for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) {
/* clip glyph bitmap height to target image bounds */
if (yy >= 0 && yy < im->ysize) {
/* blend this glyph into the buffer */
int k;
unsigned char *target;
unsigned int tmp;
if (color) {
/* target[RGB] returns the color, target[A] returns the mask */
/* target bands get split again in ImageDraw.text */
target = (unsigned char *)im->image[yy] + xx * 4;
} else {
target = im->image8[yy] + xx;
}
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
/* paste color glyph */
for (k = x0; k < x1; k++) {
unsigned int src_alpha = source[k] * convert_scale;
unsigned int src_alpha = source[k * 4 + 3];
/* paste only if source has data */
if (src_alpha > 0) {
/* unpremultiply BGRa */
int src_red =
CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
int src_green =
CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
int src_blue =
CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);
/* blend required if target has data */
if (target[k * 4 + 3] > 0) {
/* blend RGBA colors */
target[k * 4 + 0] = BLEND(
src_alpha, target[k * 4 + 0], ink[0], tmp
src_alpha, target[k * 4 + 0], src_red, tmp
);
target[k * 4 + 1] = BLEND(
src_alpha, target[k * 4 + 1], ink[1], tmp
src_alpha, target[k * 4 + 1], src_green, tmp
);
target[k * 4 + 2] = BLEND(
src_alpha, target[k * 4 + 2], ink[2], tmp
src_alpha, target[k * 4 + 2], src_blue, tmp
);
target[k * 4 + 3] = CLIP8(
src_alpha +
@ -1174,35 +1153,68 @@ font_render(FontObject *self, PyObject *args) {
)
);
} else {
target[k * 4 + 0] = ink[0];
target[k * 4 + 1] = ink[1];
target[k * 4 + 2] = ink[2];
/* paste unpremultiplied RGBA values */
target[k * 4 + 0] = src_red;
target[k * 4 + 1] = src_green;
target[k * 4 + 2] = src_blue;
target[k * 4 + 3] = src_alpha;
}
}
}
} else {
for (k = x0; k < x1; k++) {
unsigned int src_alpha = source[k] * convert_scale;
if (src_alpha > 0) {
target[k] =
target[k] > 0
? CLIP8(
src_alpha +
MULDIV255(
target[k], (255 - src_alpha), tmp
} else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
if (color) {
unsigned char *ink = (unsigned char *)&foreground_ink;
for (k = x0; k < x1; k++) {
unsigned int src_alpha = source[k] * convert_scale;
if (src_alpha > 0) {
if (target[k * 4 + 3] > 0) {
target[k * 4 + 0] = BLEND(
src_alpha, target[k * 4 + 0], ink[0], tmp
);
target[k * 4 + 1] = BLEND(
src_alpha, target[k * 4 + 1], ink[1], tmp
);
target[k * 4 + 2] = BLEND(
src_alpha, target[k * 4 + 2], ink[2], tmp
);
target[k * 4 + 3] = CLIP8(
src_alpha + MULDIV255(
target[k * 4 + 3],
(255 - src_alpha),
tmp
)
);
} else {
target[k * 4 + 0] = ink[0];
target[k * 4 + 1] = ink[1];
target[k * 4 + 2] = ink[2];
target[k * 4 + 3] = src_alpha;
}
}
}
} else {
for (k = x0; k < x1; k++) {
unsigned int src_alpha = source[k] * convert_scale;
if (src_alpha > 0) {
target[k] =
target[k] > 0
? CLIP8(
src_alpha +
MULDIV255(
target[k], (255 - src_alpha), tmp
)
)
)
: src_alpha;
: src_alpha;
}
}
}
} else {
PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
} else {
PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
source += bitmap.pitch;
}
source += bitmap.pitch;
}
x += glyph_info[i].x_advance;
y += glyph_info[i].y_advance;
@ -1543,7 +1555,9 @@ setup_module(PyObject *m) {
d = PyModule_GetDict(m);
/* Ready object type */
PyType_Ready(&Font_Type);
if (PyType_Ready(&Font_Type) < 0) {
return -1;
}
if (FT_Init_FreeType(&library)) {
return 0; /* leave it uninitialized */

View File

@ -187,8 +187,17 @@ _unop(PyObject *self, PyObject *args) {
}
unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC);
if (!unop) {
return NULL;
}
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!out) {
return NULL;
}
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!im1) {
return NULL;
}
unop(out, im1);
@ -219,9 +228,21 @@ _binop(PyObject *self, PyObject *args) {
}
binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC);
if (!binop) {
return NULL;
}
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!out) {
return NULL;
}
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!im1) {
return NULL;
}
im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC);
if (!im2) {
return NULL;
}
binop(out, im1, im2);

View File

@ -53,7 +53,13 @@ apply(PyObject *self, PyObject *args) {
}
imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!imgin) {
return NULL;
}
imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!imgout) {
return NULL;
}
width = imgin->xsize;
height = imgin->ysize;
@ -143,6 +149,9 @@ match(PyObject *self, PyObject *args) {
}
imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!imgin) {
return NULL;
}
if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) {
PyErr_SetString(PyExc_RuntimeError, "Unsupported image type");
@ -185,6 +194,10 @@ match(PyObject *self, PyObject *args) {
(b6 << 6) | (b7 << 7) | (b8 << 8));
if (lut[lut_idx]) {
PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx);
if (!coordObj) {
Py_DECREF(ret);
return NULL;
}
PyList_Append(ret, coordObj);
Py_XDECREF(coordObj);
}
@ -216,6 +229,9 @@ get_on_pixels(PyObject *self, PyObject *args) {
}
img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!img) {
return NULL;
}
rows = img->image8;
width = img->xsize;
height = img->ysize;
@ -230,6 +246,10 @@ get_on_pixels(PyObject *self, PyObject *args) {
for (col_idx = 0; col_idx < width; col_idx++) {
if (row[col_idx]) {
PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx);
if (!coordObj) {
Py_DECREF(ret);
return NULL;
}
PyList_Append(ret, coordObj);
Py_XDECREF(coordObj);
}

View File

@ -219,6 +219,7 @@ _anim_encoder_dealloc(PyObject *self) {
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
Py_TYPE(self)->tp_free(self);
}
PyObject *
@ -261,6 +262,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
// Setup config for this frame
if (!WebPConfigInit(&config)) {
@ -441,6 +445,7 @@ _anim_decoder_dealloc(PyObject *self) {
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
Py_TYPE(self)->tp_free(self);
}
PyObject *
@ -503,6 +508,9 @@ _anim_decoder_get_next(PyObject *self) {
bytes = PyBytes_FromStringAndSize(
(char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height
);
if (!bytes) {
return NULL;
}
ret = Py_BuildValue("Si", bytes, timestamp);
@ -605,6 +613,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
// Setup config for this frame
if (!WebPConfigInit(&config)) {

View File

@ -163,7 +163,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) {
x0 = y0 = x1 = y1 = 0;
/* FIXME: should publish the ImagingType descriptor */
if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) {
if (!PyArg_ParseTuple(args, "O(iiii)", &op, &x0, &y0, &x1, &y1)) {
return NULL;
}
im = PyImaging_AsImaging(op);
@ -176,15 +176,10 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) {
state = &decoder->state;
/* Setup decoding tile extent */
if (x0 == 0 && x1 == 0) {
state->xsize = im->xsize;
state->ysize = im->ysize;
} else {
state->xoff = x0;
state->yoff = y0;
state->xsize = x1 - x0;
state->ysize = y1 - y0;
}
state->xoff = x0;
state->yoff = y0;
state->xsize = x1 - x0;
state->ysize = y1 - y0;
if (state->xoff < 0 || state->xsize <= 0 ||
state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 ||
@ -910,6 +905,7 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) {
} else if (strcmp(format, "jp2") == 0) {
codec_format = OPJ_CODEC_JP2;
} else {
PyErr_SetString(PyExc_ValueError, "unknown codec format");
return NULL;
}

View File

@ -480,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
GlobalUnlock(handle);
CloseClipboard();
if (!result) {
return NULL;
}
return Py_BuildValue("zN", format_names[format], result);
}

View File

@ -232,27 +232,26 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) {
x0 = y0 = x1 = y1 = 0;
/* FIXME: should publish the ImagingType descriptor */
if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) {
if (!PyArg_ParseTuple(args, "O(nnnn)", &op, &x0, &y0, &x1, &y1)) {
return NULL;
}
im = PyImaging_AsImaging(op);
if (!im) {
return NULL;
}
if (im->xsize == 0 || im->ysize == 0) {
PyErr_SetString(PyExc_ValueError, "cannot write empty image");
return NULL;
}
encoder->im = im;
state = &encoder->state;
if (x0 == 0 && x1 == 0) {
state->xsize = im->xsize;
state->ysize = im->ysize;
} else {
state->xoff = x0;
state->yoff = y0;
state->xsize = x1 - x0;
state->ysize = y1 - y0;
}
state->xoff = x0;
state->yoff = y0;
state->xsize = x1 - x0;
state->ysize = y1 - y0;
if (state->xoff < 0 || state->xsize <= 0 ||
state->xsize + state->xoff > im->xsize || state->yoff < 0 ||
@ -728,6 +727,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
const RawModeID rawmode = findRawModeID(rawmode_name);
if (get_packer(encoder, mode, rawmode) < 0) {
Py_DECREF(encoder);
return NULL;
}
@ -743,6 +743,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
for (pos = 0; pos < tags_size; pos++) {
item = PyList_GetItemRef(tags, pos);
if (item == NULL) {
Py_DECREF(encoder);
return NULL;
}
@ -767,6 +768,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
if (!is_core_tag) {
PyObject *tag_type;
if (PyDict_GetItemRef(types, key, &tag_type) < 0) {
Py_DECREF(encoder);
return NULL; // Exception has been already set
}
if (tag_type) {
@ -838,6 +840,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
if (key_int == TIFFTAG_COLORMAP) {
int stride = 256;
if (len != 768) {
Py_DECREF(encoder);
PyErr_SetString(
PyExc_ValueError, "Requiring 768 items for Colormap"
);
@ -999,8 +1002,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, PyBytes_AsString(value)
);
} else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL ||
type == TIFF_RATIONAL) {
} else if (
type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL
) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)
);
@ -1343,11 +1347,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
if (strcmp(format, "j2k") == 0) {
codec_format = OPJ_CODEC_J2K;
} else if (strcmp(format, "jpt") == 0) {
codec_format = OPJ_CODEC_JPT;
} else if (strcmp(format, "jp2") == 0) {
codec_format = OPJ_CODEC_JP2;
} else {
PyErr_SetString(PyExc_ValueError, "unknown codec format");
return NULL;
}
@ -1362,6 +1365,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
} else if (strcmp(progression, "CPRL") == 0) {
prog_order = OPJ_CPRL;
} else {
PyErr_SetString(PyExc_ValueError, "unknown progression");
return NULL;
}
@ -1374,6 +1378,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
} else if (strcmp(cinema_mode, "cinema4k-24") == 0) {
cine_mode = OPJ_CINEMA4K_24;
} else {
PyErr_SetString(PyExc_ValueError, "unknown cinema mode");
return NULL;
}

View File

@ -170,16 +170,17 @@ export_named_type(struct ArrowSchema *schema, char *format, const char *name) {
strncpy(formatp, format, format_len);
strncpy(namep, name, name_len);
*schema = (struct ArrowSchema){// Type description
.format = formatp,
.name = namep,
.metadata = NULL,
.flags = 0,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &ReleaseExportedSchema
*schema = (struct ArrowSchema){
// Type description
.format = formatp,
.name = namep,
.metadata = NULL,
.flags = 0,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &ReleaseExportedSchema
};
return 0;
}
@ -287,17 +288,18 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) {
im->refcount++;
MUTEX_UNLOCK(&im->mutex);
// Initialize primitive fields
*array = (struct ArrowArray){// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array = (struct ArrowArray){
// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
// Allocate list of buffers
@ -332,17 +334,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
// Initialize primitive fields
// Fixed length arrays are 1 buffer of validity, and the length in pixels.
// Data is in a child array.
*array = (struct ArrowArray){// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 1,
.n_children = 1,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array = (struct ArrowArray){
// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 1,
.n_children = 1,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
// Allocate list of buffers
@ -367,17 +370,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
MUTEX_LOCK(&im->mutex);
im->refcount++;
MUTEX_UNLOCK(&im->mutex);
*array->children[0] = (struct ArrowArray){// Data description
.length = length * 4,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array->children[0] = (struct ArrowArray){
// Data description
.length = length * 4,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
array->children[0]->buffers =

View File

@ -1695,16 +1695,20 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b)
if (mode == IMAGING_MODE_RGBa) {
premultiplied = 1;
}
} else if (imIn->mode == IMAGING_MODE_RGB &&
(mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) {
} else if (
imIn->mode == IMAGING_MODE_RGB &&
(mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)
) {
convert = rgb2la;
source_transparency = 1;
if (mode == IMAGING_MODE_La) {
premultiplied = 1;
}
} else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I ||
imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) &&
(mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) {
} else if (
(imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I ||
imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) &&
(mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)
) {
if (imIn->mode == IMAGING_MODE_1) {
convert = bit2rgb;
} else if (imIn->mode == IMAGING_MODE_I) {

View File

@ -537,8 +537,9 @@ polygon_generic(
// Needed to draw consistent polygons
xx[j] = xx[j - 1];
j++;
} else if ((ymin == current->ymin || ymin == current->ymax) &&
current->dx != 0) {
} else if (
(ymin == current->ymin || ymin == current->ymax) && current->dx != 0
) {
// Connect discontiguous corners
for (k = 0; k < i; k++) {
Edge *other_edge = edge_table[k];
@ -570,8 +571,10 @@ polygon_generic(
adjacent_line_x, adjacent_line_x_other_edge
)) +
1;
} else if (xx[j - 1] < adjacent_line_x - 1 &&
xx[j - 1] < adjacent_line_x_other_edge - 1) {
} else if (
xx[j - 1] < adjacent_line_x - 1 &&
xx[j - 1] < adjacent_line_x_other_edge - 1
) {
xx[j - 1] =
roundf(fmin(
adjacent_line_x, adjacent_line_x_other_edge

View File

@ -89,10 +89,12 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
INT32 mask = 0xffffffff;
if (im->bands == 3) {
((UINT8 *)&mask)[3] = 0;
} else if (alpha_only &&
(im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA ||
im->mode == IMAGING_MODE_PA)) {
} else if (
alpha_only &&
(im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA ||
im->mode == IMAGING_MODE_PA)
) {
#ifdef WORDS_BIGENDIAN
mask = 0x000000ff;
#else

View File

@ -601,6 +601,7 @@ j2ku_sycca_rgba(
static const struct j2k_decode_unpacker j2k_unpackers[] = {
{IMAGING_MODE_L, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l},
{IMAGING_MODE_P, OPJ_CLRSPC_SRGB, 1, 0, j2ku_gray_l},
{IMAGING_MODE_P, OPJ_CLRSPC_CMYK, 1, 0, j2ku_gray_l},
{IMAGING_MODE_PA, OPJ_CLRSPC_SRGB, 2, 0, j2ku_graya_la},
{IMAGING_MODE_I_16, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i},
{IMAGING_MODE_I_16B, OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i},

View File

@ -330,8 +330,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
components = 4;
color_space = OPJ_CLRSPC_SRGB;
pack = j2k_pack_rgba;
#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2)
#if ( \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \
)
} else if (im->mode == IMAGING_MODE_CMYK) {
components = 4;
color_space = OPJ_CLRSPC_CMYK;

View File

@ -206,8 +206,10 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by
context->cinfo.out_color_space = JCS_EXT_RGBX;
}
#endif
else if (context->rawmode == IMAGING_RAWMODE_CMYK ||
context->rawmode == IMAGING_RAWMODE_CMYK_I) {
else if (
context->rawmode == IMAGING_RAWMODE_CMYK ||
context->rawmode == IMAGING_RAWMODE_CMYK_I
) {
context->cinfo.out_color_space = JCS_CMYK;
} else if (context->rawmode == IMAGING_RAWMODE_YCbCr) {
context->cinfo.out_color_space = JCS_YCbCr;

View File

@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) {
}
}
ImagingSectionLeave(&cookie);
} else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB ||
mode == IMAGING_MODE_RGB) {
} else if (
mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB
) {
imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
if (!imOut) {
return NULL;

View File

@ -325,6 +325,19 @@ ImagingPackXBGR(UINT8 *out, const UINT8 *in, int pixels) {
}
}
void
ImagingPackCMYK2RGB(UINT8 *out, const UINT8 *in, int xsize) {
int x, nk, tmp;
for (x = 0; x < xsize; x++) {
nk = 255 - in[3];
out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp));
out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp));
out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp));
out += 3;
in += 4;
}
}
void
ImagingPackBGRA(UINT8 *out, const UINT8 *in, int pixels) {
int i;
@ -605,6 +618,7 @@ static struct {
{IMAGING_MODE_CMYK, IMAGING_RAWMODE_M, 8, band1},
{IMAGING_MODE_CMYK, IMAGING_RAWMODE_Y, 8, band2},
{IMAGING_MODE_CMYK, IMAGING_RAWMODE_K, 8, band3},
{IMAGING_MODE_CMYK, IMAGING_RAWMODE_RGB, 24, ImagingPackCMYK2RGB},
/* video (YCbCr) */
{IMAGING_MODE_YCbCr, IMAGING_RAWMODE_YCbCr, 24, ImagingPackRGB},

View File

@ -27,7 +27,8 @@ ImagingPaletteNew(const ModeID mode) {
int i;
ImagingPalette palette;
if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA) {
if (mode != IMAGING_MODE_RGB && mode != IMAGING_MODE_RGBA &&
mode != IMAGING_MODE_CMYK) {
return (ImagingPalette)ImagingError_ModeError();
}

View File

@ -352,16 +352,16 @@ ImagingPaste(
static inline void
fill(
Imaging imOut, const void *ink_, int dx, int dy, int xsize, int ysize, int pixelsize
Imaging imOut, const UINT8 *ink, int dx, int dy, int xsize, int ysize, int pixelsize
) {
/* fill opaque region */
int x, y;
int x, y, i;
UINT8 ink8 = 0;
INT32 ink32 = 0L;
memcpy(&ink32, ink_, pixelsize);
memcpy(&ink8, ink_, sizeof(ink8));
memcpy(&ink32, ink, pixelsize);
memcpy(&ink8, ink, sizeof(ink8));
if (imOut->image8 || ink32 == 0L) {
dx *= pixelsize;
@ -371,12 +371,24 @@ fill(
}
} else {
#if defined _WIN32 && !defined _WIN64
dx *= pixelsize;
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx;
for (x = 0; x < xsize; x++) {
for (i = 0; i < pixelsize; i++) {
*out++ = ink[i];
}
}
}
#else
for (y = 0; y < ysize; y++) {
INT32 *out = imOut->image32[y + dy] + dx;
for (x = 0; x < xsize; x++) {
out[x] = ink32;
}
}
#endif
}
}

View File

@ -175,8 +175,15 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
/* Get all data from File descriptor */
c = (SGISTATE *)state->context;
_imaging_seek_pyFd(state->fd, 0L, SEEK_END);
if (_imaging_seek_pyFd(state->fd, 0L, SEEK_END) == -1) {
state->errcode = IMAGING_CODEC_UNKNOWN;
return -1;
}
c->bufsize = _imaging_tell_pyFd(state->fd);
if (c->bufsize == -1) {
state->errcode = IMAGING_CODEC_UNKNOWN;
return -1;
}
c->bufsize -= SGI_HEADER_SIZE;
c->tablen = im->bands * im->ysize;
@ -194,8 +201,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
state->errcode = IMAGING_CODEC_MEMORY;
return -1;
}
_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET);
if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) {
if (_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET) == -1 ||
_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) {
free(ptr);
state->errcode = IMAGING_CODEC_UNKNOWN;
return -1;

View File

@ -677,9 +677,15 @@ ImagingNewArrow(
Imaging im;
struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema");
if (!schema) {
return NULL;
}
struct ArrowArray *external_array =
(struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array");
if (!external_array) {
return NULL;
}
if (xsize < 0 || ysize < 0) {
return (Imaging)ImagingError_ValueError("bad image size");

View File

@ -12,6 +12,9 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) {
int bytes_result;
result = PyObject_CallMethod(fd, "read", "n", bytes);
if (result == NULL) {
goto err;
}
bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length);
if (bytes_result == -1) {
@ -28,7 +31,7 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) {
return length;
err:
Py_DECREF(result);
Py_XDECREF(result);
return -1;
}
@ -38,9 +41,16 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) {
PyObject *byteObj;
byteObj = PyBytes_FromStringAndSize(src, bytes);
if (!byteObj) {
return -1;
}
result = PyObject_CallMethod(fd, "write", "O", byteObj);
Py_DECREF(byteObj);
if (result == NULL) {
return -1;
}
Py_DECREF(result);
return bytes;
@ -51,6 +61,9 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) {
PyObject *result;
result = PyObject_CallMethod(fd, "seek", "ni", offset, whence);
if (result == NULL) {
return -1;
}
Py_DECREF(result);
return 0;
@ -62,6 +75,9 @@ _imaging_tell_pyFd(PyObject *fd) {
Py_ssize_t location;
result = PyObject_CallMethod(fd, "tell", NULL);
if (result == NULL) {
return -1;
}
location = PyLong_AsSsize_t(result);
Py_DECREF(result);

View File

@ -114,14 +114,14 @@ ARCHITECTURES = {
V = {
"BROTLI": "1.2.0",
"FREETYPE": "2.14.1",
"FREETYPE": "2.14.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "12.3.2",
"HARFBUZZ": "13.2.1",
"JPEGTURBO": "3.1.3",
"LCMS2": "2.18",
"LIBAVIF": "1.3.0",
"LIBAVIF": "1.4.1",
"LIBIMAGEQUANT": "4.4.1",
"LIBPNG": "1.6.54",
"LIBPNG": "1.6.56",
"LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.4",
"TIFF": "4.7.1",
@ -542,14 +542,11 @@ def write_script(
def get_footer(dep: dict[str, Any]) -> list[str]:
lines = []
for out in dep.get("headers", []):
lines.append(cmd_copy(out, "{inc_dir}"))
for out in dep.get("libs", []):
lines.append(cmd_copy(out, "{lib_dir}"))
for out in dep.get("bins", []):
lines.append(cmd_copy(out, "{bin_dir}"))
return lines
return (
[cmd_copy(out, "{inc_dir}") for out in dep.get("headers", [])]
+ [cmd_copy(out, "{lib_dir}") for out in dep.get("libs", [])]
+ [cmd_copy(out, "{bin_dir}") for out in dep.get("bins", [])]
)
def build_env(prefs: dict[str, str], verbose: bool) -> None: