Merge branch 'main' into feature/png-bit-depth
This commit is contained in:
commit
9c7a5c9946
@ -1 +1 @@
|
||||
cibuildwheel==3.4.0
|
||||
cibuildwheel==3.4.1
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
mypy==1.19.1
|
||||
mypy==1.20.2
|
||||
arro3-compute
|
||||
arro3-core
|
||||
IceSpringPySideStubs-PyQt6
|
||||
|
||||
6
.github/dependencies.json
vendored
6
.github/dependencies.json
vendored
@ -3,12 +3,12 @@
|
||||
"bzip2": "1.0.8",
|
||||
"freetype": "2.14.3",
|
||||
"fribidi": "1.0.16",
|
||||
"harfbuzz": "13.2.1",
|
||||
"harfbuzz": "14.2.0",
|
||||
"jpegturbo": "3.1.4.1",
|
||||
"lcms2": "2.18",
|
||||
"lcms2": "2.19",
|
||||
"libavif": "1.4.1",
|
||||
"libimagequant": "4.4.1",
|
||||
"libpng": "1.6.56",
|
||||
"libpng": "1.6.58",
|
||||
"libwebp": "1.6.0",
|
||||
"libxcb": "1.17.0",
|
||||
"openjpeg": "2.5.4",
|
||||
|
||||
4
.github/workflows/cifuzz.yml
vendored
4
.github/workflows/cifuzz.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master
|
||||
with:
|
||||
oss-fuzz-project-name: 'pillow'
|
||||
language: python
|
||||
dry-run: false
|
||||
- name: Run Fuzzers
|
||||
id: run
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@e41e2f295eb18d630932fdd33d072527ba74c87b # master
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@c11174f47deee98f260dede5d661614bda78ae39 # master
|
||||
with:
|
||||
oss-fuzz-project-name: 'pillow'
|
||||
fuzz-seconds: 600
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
- name: Lint
|
||||
run: uvx --with tox-uv tox -e lint
|
||||
- name: Mypy
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@ -270,7 +270,7 @@ jobs:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload wheels to scientific-python-nightly-wheels
|
||||
uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3
|
||||
uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4
|
||||
with:
|
||||
artifacts_path: dist
|
||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||
|
||||
@ -516,8 +516,9 @@ class TestFilePng:
|
||||
im = roundtrip(im)
|
||||
assert im.info["transparency"] == (248, 248, 248)
|
||||
|
||||
im = roundtrip(im, transparency=(0, 1, 2))
|
||||
assert im.info["transparency"] == (0, 1, 2)
|
||||
for transparency in ((0, 1, 2), [0, 1, 2]):
|
||||
im = roundtrip(im, transparency=transparency)
|
||||
assert im.info["transparency"] == (0, 1, 2)
|
||||
|
||||
def test_trns_p(self, tmp_path: Path) -> None:
|
||||
# Check writing a transparency of 0, issue #528
|
||||
@ -532,6 +533,36 @@ class TestFilePng:
|
||||
|
||||
assert_image_equal(im2.convert("RGBA"), im.convert("RGBA"))
|
||||
|
||||
def test_trns_invalid(self, tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.png"
|
||||
|
||||
for mode in ("1", "L", "I;16"):
|
||||
im = Image.new(mode, (1, 1))
|
||||
with pytest.raises(
|
||||
ValueError, match=f"transparency for {mode} must be an integer"
|
||||
):
|
||||
im.save(out, transparency="invalid")
|
||||
|
||||
im = Image.new("I", (1, 1))
|
||||
with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"):
|
||||
with pytest.raises(ValueError):
|
||||
im.save(out, transparency="invalid")
|
||||
|
||||
im = Image.new("P", (1, 1))
|
||||
with pytest.raises(
|
||||
ValueError, match="transparency for P must be an integer or bytes"
|
||||
):
|
||||
im.save(out, transparency="invalid")
|
||||
|
||||
im = Image.new("RGB", (1, 1))
|
||||
with pytest.raises(
|
||||
ValueError, match="transparency for RGB must be list or tuple"
|
||||
):
|
||||
im.save(out, transparency="invalid")
|
||||
|
||||
with pytest.raises(ValueError, match="transparency for RGB must have length 3"):
|
||||
im.save(out, transparency=(1, 2))
|
||||
|
||||
def test_trns_null(self) -> None:
|
||||
# Check reading images with null tRNS value, issue #1239
|
||||
test_file = "Tests/images/tRNS_null_1x1.png"
|
||||
|
||||
@ -862,7 +862,7 @@ class TestImage:
|
||||
def test_exif_webp(self, tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
exif = im.getexif()
|
||||
assert exif == {}
|
||||
assert dict(exif) == {}
|
||||
|
||||
out = tmp_path / "temp.webp"
|
||||
exif[258] = 8
|
||||
@ -884,7 +884,7 @@ class TestImage:
|
||||
def test_exif_png(self, tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
exif = im.getexif()
|
||||
assert exif == {274: 1}
|
||||
assert dict(exif) == {274: 1}
|
||||
|
||||
out = tmp_path / "temp.png"
|
||||
exif[258] = 8
|
||||
|
||||
@ -627,3 +627,37 @@ class TestCoreResampleBox:
|
||||
0.4,
|
||||
f">>> {size} {box} {flt}",
|
||||
)
|
||||
|
||||
|
||||
class TestCoreResample16bpc:
|
||||
# Lanczos weighting during downsampling can push accumulated float sums
|
||||
@pytest.mark.parametrize(
|
||||
"offset",
|
||||
(
|
||||
# below 0. These must be clamped to 0, not corrupted byte-by-byte.
|
||||
0, # Left half = 65535, right half = 0
|
||||
# above 65535. These must be clamped to 65535, not corrupted byte-by-byte.
|
||||
50, # # Left half = 0, right half = 65535
|
||||
),
|
||||
)
|
||||
def test_resampling_clamp_overflow(self, offset: int) -> None:
|
||||
ims = {}
|
||||
width, height = 100, 10
|
||||
for mode in ("I;16", "F"):
|
||||
im = Image.new(mode, (width, height))
|
||||
im.paste(65535, (offset, 0, offset + width // 2, height))
|
||||
|
||||
# 5x downsampling with Lanczos
|
||||
# creates ~8.7% overshoot or undershoot at the step edge
|
||||
ims[mode] = im.resize((20, height), Image.Resampling.LANCZOS)
|
||||
|
||||
for y in range(height):
|
||||
for x in range(20):
|
||||
v = ims["F"].getpixel((x, y))
|
||||
assert isinstance(v, float)
|
||||
expected = max(0, min(65535, round(v)))
|
||||
|
||||
value = ims["I;16"].getpixel((x, y))
|
||||
assert (
|
||||
value == expected
|
||||
), f"Pixel ({x}, {y}): expected {expected}, got {value}"
|
||||
|
||||
@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
|
||||
* **littlecms** provides color management
|
||||
|
||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
||||
above uses liblcms2. Tested with **1.19** and **2.7-2.18**.
|
||||
above uses liblcms2. Tested with **1.19** and **2.7-2.19**.
|
||||
|
||||
* **libwebp** provides the WebP format.
|
||||
|
||||
|
||||
@ -2639,11 +2639,8 @@ class Image:
|
||||
if is_path(fp):
|
||||
filename = os.fspath(fp)
|
||||
open_fp = True
|
||||
elif fp == sys.stdout:
|
||||
try:
|
||||
fp = sys.stdout.buffer
|
||||
except AttributeError:
|
||||
pass
|
||||
elif fp == sys.stdout and isinstance(sys.stdout, io.TextIOWrapper):
|
||||
fp = sys.stdout.buffer
|
||||
if not filename and hasattr(fp, "name") and is_path(fp.name):
|
||||
# only set the name for metadata purposes
|
||||
filename = os.fspath(fp.name)
|
||||
|
||||
@ -383,7 +383,7 @@ class PdfParser:
|
||||
msg = "specify buf or f or filename, but not both buf and f"
|
||||
raise RuntimeError(msg)
|
||||
self.filename = filename
|
||||
self.buf: bytes | bytearray | mmap.mmap | None = buf
|
||||
self.buf: bytes | bytearray | memoryview | mmap.mmap | None = buf
|
||||
self.f = f
|
||||
self.start_offset = start_offset
|
||||
self.should_close_buf = False
|
||||
@ -435,7 +435,9 @@ class PdfParser:
|
||||
self.seek_end()
|
||||
|
||||
def close_buf(self) -> None:
|
||||
if isinstance(self.buf, mmap.mmap):
|
||||
if isinstance(self.buf, memoryview):
|
||||
self.buf.release()
|
||||
elif isinstance(self.buf, mmap.mmap):
|
||||
self.buf.close()
|
||||
self.buf = None
|
||||
|
||||
|
||||
@ -1446,35 +1446,47 @@ def _save(
|
||||
palette_bytes += b"\0"
|
||||
chunk(fp, b"PLTE", palette_bytes)
|
||||
|
||||
transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))
|
||||
transparency = im.encoderinfo.get("transparency", im.info.get("transparency"))
|
||||
|
||||
if transparency or transparency == 0:
|
||||
if transparency is not None:
|
||||
if im.mode == "P":
|
||||
# limit to actual palette size
|
||||
alpha_bytes = colors
|
||||
if isinstance(transparency, bytes):
|
||||
chunk(fp, b"tRNS", transparency[:alpha_bytes])
|
||||
else:
|
||||
elif isinstance(transparency, int):
|
||||
transparency = max(0, min(255, transparency))
|
||||
alpha = b"\xff" * transparency + b"\0"
|
||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||
else:
|
||||
msg = "transparency for P must be an integer or bytes"
|
||||
raise ValueError(msg)
|
||||
elif im.mode in ("1", "L", "I", "I;16"):
|
||||
transparency = max(0, min(65535, transparency))
|
||||
chunk(fp, b"tRNS", o16(transparency))
|
||||
if isinstance(transparency, int):
|
||||
transparency = max(0, min(65535, transparency))
|
||||
chunk(fp, b"tRNS", o16(transparency))
|
||||
else:
|
||||
msg = f"transparency for {im.mode} must be an integer"
|
||||
raise ValueError(msg)
|
||||
elif im.mode == "RGB":
|
||||
red, green, blue = transparency
|
||||
chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
|
||||
else:
|
||||
if "transparency" in im.encoderinfo:
|
||||
# don't bother with transparency if it's an RGBA
|
||||
# and it's in the info dict. It's probably just stale.
|
||||
msg = "cannot use transparency for this mode"
|
||||
raise OSError(msg)
|
||||
else:
|
||||
if im.mode == "P" and im.im.getpalettemode() == "RGBA":
|
||||
alpha = im.im.getpalette("RGBA", "A")
|
||||
alpha_bytes = colors
|
||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||
if not isinstance(transparency, (list, tuple)):
|
||||
msg = "transparency for RGB must be list or tuple"
|
||||
raise ValueError(msg)
|
||||
elif len(transparency) != 3:
|
||||
msg = "transparency for RGB must have length 3"
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
red, green, blue = transparency
|
||||
chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
|
||||
elif im.encoderinfo.get("transparency") is not None:
|
||||
# don't bother with transparency if it's an RGBA
|
||||
# and it's in the info dict. It's probably just stale.
|
||||
msg = "cannot use transparency for this mode"
|
||||
raise OSError(msg)
|
||||
elif im.mode == "P" and im.im.getpalettemode() == "RGBA":
|
||||
alpha = im.im.getpalette("RGBA", "A")
|
||||
alpha_bytes = colors
|
||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||
|
||||
if dpi := im.encoderinfo.get("dpi"):
|
||||
chunk(
|
||||
|
||||
@ -37,8 +37,6 @@
|
||||
#define MAX(a, b) (a) > (b) ? (a) : (b)
|
||||
#define MIN(a, b) (a) < (b) ? (a) : (b)
|
||||
|
||||
#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v))
|
||||
|
||||
/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */
|
||||
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114)
|
||||
#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000)
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
|
||||
#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255)
|
||||
|
||||
#define CLIP16(v) ((v) <= 0 ? 0 : (v) < 65536 ? (v) : 65535)
|
||||
|
||||
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
|
||||
GCC generates code with partial dependency which is 3 times slower.
|
||||
See: https://stackoverflow.com/a/26588074/253146 */
|
||||
|
||||
@ -492,9 +492,9 @@ ImagingResampleHorizontal_16bpc(
|
||||
<< 8)) *
|
||||
k[x];
|
||||
}
|
||||
ss_int = ROUND_UP(ss);
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
|
||||
ss_int = CLIP16(ROUND_UP(ss));
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
|
||||
}
|
||||
}
|
||||
ImagingSectionLeave(&cookie);
|
||||
@ -531,9 +531,9 @@ ImagingResampleVertical_16bpc(
|
||||
(imIn->image8[y + ymin][xx * 2 + (bigendian ? 0 : 1)] << 8)) *
|
||||
k[y];
|
||||
}
|
||||
ss_int = ROUND_UP(ss);
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = CLIP8(ss_int % 256);
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = CLIP8(ss_int >> 8);
|
||||
ss_int = CLIP16(ROUND_UP(ss));
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 1 : 0)] = ss_int & 0xFF;
|
||||
imOut->image8[yy][xx * 2 + (bigendian ? 0 : 1)] = ss_int >> 8;
|
||||
}
|
||||
}
|
||||
ImagingSectionLeave(&cookie);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user