Merge branch 'main' into deprecate-getsize

This commit is contained in:
Andrew Murray 2022-06-30 20:37:37 +10:00
commit 0d91d13a6e
29 changed files with 159 additions and 74 deletions

View File

@ -5,6 +5,36 @@ Changelog (Pillow)
9.2.0 (unreleased)
------------------
- Fixed null check for fribidi_version_info in FriBiDi shim #6376
[nulano]
- Added GIF decompression bomb check #6402
[radarhere]
- Handle PCF fonts files with less than 256 characters #6386
[dawidcrivelli, radarhere]
- Improved GIF optimize condition #6378
[raygard, radarhere]
- Reverted to __array_interface__ with the release of NumPy 1.23 #6394
[radarhere]
- Pad PCX palette to 768 bytes when saving #6391
[radarhere]
- Fixed bug with rounding pixels to palette colors #6377
[btrekkie, radarhere]
- Use gnome-screenshot on Linux if available #6361
[radarhere, nulano]
- Fixed loading L mode BMP RLE8 images #6384
[radarhere]
- Fixed incorrect operator in ImageCms error #6370
[LostBenjamin, hugovk, radarhere]
- Limit FPX tile size to avoid extending outside image #6368
[radarhere]

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -61,6 +61,11 @@ class TestDecompressionBomb:
with Image.open("Tests/images/decompression_bomb.gif"):
pass
def test_exception_gif_extents(self):
with Image.open("Tests/images/decompression_bomb_extents.gif") as im:
with pytest.raises(Image.DecompressionBombError):
im.seek(1)
def test_exception_bmp(self):
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/bmp/b/reallybig.bmp"):

View File

@ -134,6 +134,9 @@ def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
# This test image has been manually hexedited
# to have rows with too much data
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:

View File

@ -158,6 +158,9 @@ def test_optimize_correctness():
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
# These do optimize the palette
check(256, 511, 256)
check(255, 511, 255)
check(129, 511, 129)
check(128, 511, 128)
check(64, 511, 64)
check(4, 511, 4)
@ -167,11 +170,6 @@ def test_optimize_correctness():
check(64, 513, 256)
check(4, 513, 256)
# Other limits that don't optimize the palette
check(129, 511, 256)
check(255, 511, 256)
check(256, 511, 256)
def test_optimize_full_l():
im = Image.frombytes("L", (16, 16), bytes(range(256)))
@ -180,6 +178,19 @@ def test_optimize_full_l():
assert im.mode == "L"
def test_optimize_if_palette_can_be_reduced_by_half():
with Image.open("Tests/images/test.colors.gif") as im:
# Reduce dimensions because original is too big for _get_optimize()
im = im.resize((591, 443))
im_rgb = im.convert("RGB")
for (optimize, colors) in ((False, 256), (True, 8)):
out = BytesIO()
im_rgb.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded:
assert len(reloaded.palette.palette) // 3 == colors
def test_roundtrip(tmp_path):
out = str(tmp_path / "temp.gif")
im = hopper()
@ -982,8 +993,8 @@ def test_append_images(tmp_path):
def test_transparent_optimize(tmp_path):
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency.
# Need a palette that isn't using the 0 color, and one that's > 128 items where the
# transparent color is actually the top palette entry to trigger the bug.
# Need a palette that isn't using the 0 color,
# where the transparent color is actually the top palette entry to trigger the bug.
data = bytes(range(1, 254))
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
@ -993,10 +1004,10 @@ def test_transparent_optimize(tmp_path):
im.putpalette(palette)
out = str(tmp_path / "temp.gif")
im.save(out, transparency=253)
with Image.open(out) as reloaded:
im.save(out, transparency=im.getpixel((252, 0)))
assert reloaded.info["transparency"] == 253
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
def test_rgb_transparency(tmp_path):

View File

@ -20,6 +20,11 @@ def test_sanity(tmp_path):
for mode in ("1", "L", "P", "RGB"):
_roundtrip(tmp_path, hopper(mode))
# Test a palette with less than 256 colors
im = Image.new("P", (1, 1))
im.putpalette((255, 0, 0))
_roundtrip(tmp_path, im)
# Test an unsupported mode
f = str(tmp_path / "temp.pcx")
im = hopper("RGBA")

View File

@ -49,6 +49,14 @@ def test_sanity(request, tmp_path):
save_font(request, tmp_path)
def test_less_than_256_characters():
with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile)
# check the number of characters in the font
assert len([_f for _f in font.glyph if _f]) == 127
def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):

View File

@ -1,4 +1,5 @@
import pytest
from packaging.version import parse as parse_version
from PIL import Image
@ -34,6 +35,7 @@ def test_toarray():
test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8)
if parse_version(numpy.__version__) >= parse_version("1.23"):
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with pytest.raises(OSError):
numpy.array(im_truncated)

View File

@ -9,7 +9,7 @@ def test_entropy():
assert round(abs(entropy("L") - 7.063008716585465), 7) == 0
assert round(abs(entropy("I") - 7.063008716585465), 7) == 0
assert round(abs(entropy("F") - 7.063008716585465), 7) == 0
assert round(abs(entropy("P") - 5.0530452472519745), 7) == 0
assert round(abs(entropy("P") - 5.082506854662517), 7) == 0
assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0
assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0
assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0

View File

@ -16,7 +16,7 @@ def test_getcolors():
assert getcolors("L") == 255
assert getcolors("I") == 255
assert getcolors("F") == 255
assert getcolors("P") == 90 # fixed palette
assert getcolors("P") == 96 # fixed palette
assert getcolors("RGB") is None
assert getcolors("RGBA") is None
assert getcolors("CMYK") is None

View File

@ -10,7 +10,7 @@ def test_histogram():
assert histogram("L") == (256, 0, 662)
assert histogram("I") == (256, 0, 662)
assert histogram("F") == (256, 0, 662)
assert histogram("P") == (256, 0, 1871)
assert histogram("P") == (256, 0, 1551)
assert histogram("RGB") == (768, 4, 675)
assert histogram("RGBA") == (1024, 0, 16384)
assert histogram("CMYK") == (1024, 0, 16384)

View File

@ -65,6 +65,22 @@ def test_quantize_no_dither():
assert converted.palette.palette == palette.palette.palette
def test_quantize_no_dither2():
im = Image.new("RGB", (9, 1))
im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
palette = Image.new("P", (1, 1))
data = (0, 0, 0, 32, 32, 32)
palette.putpalette(data)
quantized = im.quantize(dither=Image.Dither.NONE, palette=palette)
assert tuple(quantized.palette.palette) == data
px = quantized.load()
for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1)
def test_quantize_dither_diff():
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:

View File

@ -94,4 +94,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -15,7 +15,10 @@ or the clipboard to a PIL image memory.
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
If the bounding box is omitted, the entire screen is copied.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux (X11))
On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it
is installed. To capture the default X11 display instead, pass ``xdisplay=""``.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
:param bbox: What region to copy. Default is the entire screen.
Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used.

View File

@ -1,12 +1,6 @@
9.2.0
-----
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
@ -46,14 +40,6 @@ Image.coerce_e
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
@ -68,15 +54,14 @@ The image's palette mode will become "RGBA", and "transparency" will be removed
Security
========
TODO
^^^^
TODO
An additional decompression bomb check has been added for the GIF format.
Other Changes
=============
TODO
^^^^
Using gnome-screenshot on Linux
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then
``gnome-screenshot`` will be used to capture the display if it is installed. To capture
the default X11 display instead, pass ``xdisplay=""``.

View File

@ -321,7 +321,8 @@ class BmpRleDecoder(ImageFile.PyDecoder):
# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1]))
return -1, 0

View File

@ -265,6 +265,7 @@ class GifImageFile(ImageFile.ImageFile):
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
self._size = max(x1, self.size[0]), max(y1, self.size[1])
Image._decompression_bomb_check(self._size)
frame_dispose_extent = x0, y0, x1, y1
flags = s[8]
@ -824,9 +825,18 @@ def _get_optimize(im, info):
if count:
used_palette_colors.append(i)
if optimise or (
len(used_palette_colors) <= 128
and max(used_palette_colors) > len(used_palette_colors)
if optimise or max(used_palette_colors) >= len(used_palette_colors):
return used_palette_colors
num_palette_colors = len(im.palette.palette) // Image.getmodebands(
im.palette.mode
)
current_palette_size = 1 << (num_palette_colors - 1).bit_length()
if (
# check that the palette would become smaller when saved
len(used_palette_colors) <= current_palette_size // 2
# check that the palette is not already the smallest possible size
and current_palette_size > 2
):
return used_palette_colors

View File

@ -671,14 +671,9 @@ class Image:
raise ValueError("Could not save to PNG for display") from e
return b.getvalue()
class _ArrayData:
def __init__(self, new):
self.__array_interface__ = new
def __array__(self, dtype=None):
@property
def __array_interface__(self):
# numpy array interface support
import numpy as np
new = {}
shape, typestr = _conv_type_shape(self)
new["shape"] = shape
@ -690,8 +685,7 @@ class Image:
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
return np.array(self._ArrayData(new), dtype)
return new
def __getstate__(self):
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]

View File

@ -15,15 +15,14 @@
# See the README file for information on usage and redistribution.
#
import os
import shutil
import subprocess
import sys
import tempfile
from . import Image
if sys.platform == "darwin":
import os
import subprocess
import tempfile
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
if xdisplay is None:
@ -62,6 +61,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
elif shutil.which("gnome-screenshot"):
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
subprocess.call(["gnome-screenshot", "-f", filepath])
im = Image.open(filepath)
im.load()
os.unlink(filepath)
if bbox:
im_cropped = im.crop(bbox)
im.close()
return im_cropped
return im
# use xdisplay=None for default display on non-win32/macOS systems
if not Image.core.HAVE_XCB:
raise OSError("Pillow was built without XCB support")

View File

@ -84,8 +84,7 @@ class PcfFontFile(FontFile.FontFile):
#
# create glyph structure
for ch in range(256):
ix = encoding[ch]
for ch, ix in enumerate(encoding):
if ix is not None:
x, y, l, r, w, a, d, f = metrics[ix]
glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix]
@ -219,10 +218,6 @@ class PcfFontFile(FontFile.FontFile):
return bitmaps
def _load_encoding(self):
# map character code to bitmap index
encoding = [None] * 256
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
@ -232,6 +227,9 @@ class PcfFontFile(FontFile.FontFile):
nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)
# map character code to bitmap index
encoding = [None] * min(256, nencoding)
encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]
for i in range(first_col, len(encoding)):

View File

@ -198,7 +198,9 @@ def _save(im, fp, filename):
if im.mode == "P":
# colour palette
fp.write(o8(12))
fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes
palette = im.im.getpalette("RGB", "RGB")
palette += b"\x00" * (768 - len(palette))
fp.write(palette) # 768 bytes
elif im.mode == "L":
# greyscale palette
fp.write(o8(12))

View File

@ -200,15 +200,15 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {
/* Find min and max distances to any point in the box */
r = palette->palette[i * 4 + 0];
tmin = (r < r0) ? RDIST(r, r1) : (r > r1) ? RDIST(r, r0) : 0;
tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0;
tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0);
g = palette->palette[i * 4 + 1];
tmin += (g < g0) ? GDIST(g, g1) : (g > g1) ? GDIST(g, g0) : 0;
tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0;
tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0);
b = palette->palette[i * 4 + 2];
tmin += (b < b0) ? BDIST(b, b1) : (b > b1) ? BDIST(b, b0) : 0;
tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0;
tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0);
dmin[i] = tmin;

View File

@ -33,6 +33,7 @@ static void fribidi_get_bracket_types_compat(
int load_fribidi(void) {
int error = 0;
const char **p_fribidi_version_info = 0;
p_fribidi = 0;
@ -87,20 +88,21 @@ int load_fribidi(void) {
LOAD_FUNCTION(fribidi_get_par_embedding_levels);
#ifndef _WIN32
fribidi_version_info = *(const char**)dlsym(p_fribidi, "fribidi_version_info");
if (error || (fribidi_version_info == 0)) {
p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info");
if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) {
dlclose(p_fribidi);
p_fribidi = 0;
return 2;
}
#else
fribidi_version_info = *(const char**)GetProcAddress(p_fribidi, "fribidi_version_info");
if (error || (fribidi_version_info == 0)) {
p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info");
if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) {
FreeLibrary(p_fribidi);
p_fribidi = 0;
return 2;
}
#endif
fribidi_version_info = *p_fribidi_version_info;
return 0;
}

View File

@ -281,9 +281,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip",
"filename": "harfbuzz-4.3.0.zip",
"dir": "harfbuzz-4.3.0",
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip",
"filename": "harfbuzz-4.4.1.zip",
"dir": "harfbuzz-4.4.1",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),