Merge branch 'main' into image_grab_wayland_kde

This commit is contained in:
Adian Kozlica 2025-03-29 23:51:29 +01:00 committed by GitHub
commit ec11f7aaed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 188 additions and 106 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View File

@ -224,3 +224,13 @@ def test_offset() -> None:
# to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]
assert im.mode == "RGB"
monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True)
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.mode == "RGBA"

View File

@ -1026,6 +1026,17 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_old_style_jpeg_orientation(self) -> None:
with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp:
data = fp.read()
# Set EXIF Orientation to 2
data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im:
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_open_missing_samplesperpixel(self) -> None:
with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"

View File

@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
im.save(outfile)
converted = open_with_magick(magick, tmp_path, outfile)
if mode == "P":
assert converted.mode == "P"
im = im.convert("RGB")
converted = converted.convert("RGB")
assert_image_equal(converted, im)
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
roundtrip(tmp_path, mode)
@pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path: Path) -> None:
# Arrange
mode = "P"

View File

@ -8,7 +8,7 @@ import pytest
from PIL import Image, ImageFile, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper
from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper
def test_load_raw() -> None:
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
pass
def test_render() -> None:
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()
b = BytesIO(data[:808] + b"\x00" + data[809:])
with Image.open(b) as im:
if hasattr(Image.core, "drawwmf"):
assert_image_equal_tofile(im, "Tests/images/drawing.emf")
def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler):
methodCalled = False
@ -88,6 +97,20 @@ def test_load_set_dpi() -> None:
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
with Image.open("Tests/images/drawing.emf") as im:
assert im.size == (1625, 1625)
if not hasattr(Image.core, "drawwmf"):
return
im.load(im.info["dpi"])
assert im.size == (1625, 1625)
with Image.open("Tests/images/drawing.emf") as im:
im.load((72, 144))
assert im.size == (82, 164)
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png")
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext: str, tmp_path: Path) -> None:

View File

@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
BLACK,
)
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1)
assert_image_equal_tofile(img, expected)
def test_polygon2() -> None:

View File

@ -131,6 +131,26 @@ class TestImageFile:
assert_image_equal(im1, im2)
def test_tile_size(self) -> None:
with open("Tests/images/hopper.tif", "rb") as im_fp:
data = im_fp.read()
reads = []
class FP(BytesIO):
def read(self, size: int | None = None) -> bytes:
reads.append(size)
return super().read(size)
fp = FP(data)
with Image.open(fp) as im:
assert len(im.tile) == 7
im.load()
# Despite multiple tiles, assert only one tile caused a read of maxblock size
assert reads.count(im.decodermaxblock) == 1
def test_raise_oserror(self) -> None:
with pytest.warns(DeprecationWarning):
with pytest.raises(OSError):

View File

@ -4,21 +4,12 @@
Security
========
TODO
^^^^
Undefined shift when loading compressed DDS images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
When loading some compressed DDS formats, an integer was bitshifted by 24 places to
generate the 32 bits of the lookup table. This was undefined behaviour, and has been
present since Pillow 3.4.0.
Deprecations
============
@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes
===========
TODO
^^^^
``append_images`` no longer requires ``save_all``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Previously, ``save_all`` was required to in order to use ``append_images``. Now,
``save_all`` will default to ``True`` if ``append_images`` is not empty and the format
supports saving multiple frames::
im.save("out.gif", append_images=ims)
API Additions
=============
@ -73,11 +68,3 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1,
DXT5, BC2, BC3 and BC5 are supported::
im.save("out.dds", pixel_format="DXT1")
Other Changes
=============
TODO
^^^^
TODO

View File

@ -48,6 +48,8 @@ BIT2MODE = {
32: ("RGB", "BGRX"),
}
USE_RAW_ALPHA = False
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"BM")
@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile):
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
if file_info["bits"] == 32 and (
header == 22 or USE_RAW_ALPHA # 32-bit .cur offset
):
raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],

View File

@ -24,6 +24,7 @@ from __future__ import annotations
from . import Image
from ._binary import i32le as i32
from ._util import DeferredError
from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame
self.fp = self._fp
self.fp.seek(self._offset[frame])

View File

@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
from ._util import DeferredError
#
# decoder
@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile):
self._seek(f)
def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
if frame == 0:
self.__frame = -1
self._fp.seek(self.__rewind)

View File

@ -45,6 +45,7 @@ from . import (
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16
from ._util import DeferredError
if TYPE_CHECKING:
from . import _imaging
@ -167,6 +168,8 @@ class GifImageFile(ImageFile.ImageFile):
raise EOFError(msg) from e
def _seek(self, frame: int, update_image: bool = True) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
if frame == 0:
# rewind
self.__offset = 0

View File

@ -31,6 +31,7 @@ import re
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
from ._util import DeferredError
# --------------------------------------------------------------------
# Standard tags
@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame

View File

@ -621,6 +621,8 @@ class Image:
more information.
"""
if getattr(self, "map", None):
if sys.platform == "win32" and hasattr(sys, "pypy_version_info"):
self.map.close()
self.map: mmap.mmap | None = None
# Instead of simply setting to None, we're setting up a

View File

@ -34,7 +34,6 @@ import itertools
import logging
import os
import struct
import sys
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import ExifTags, Image
@ -167,7 +166,7 @@ class ImageFile(Image.Image):
pass
def _close_fp(self):
if getattr(self, "_fp", False):
if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
@ -278,8 +277,6 @@ class ImageFile(Image.Image):
self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1
# As of pypy 2.1.0, memory mapping was failing here.
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
readonly = 0
@ -345,7 +342,7 @@ class ImageFile(Image.Image):
self.tile, lambda tile: (tile[0], tile[1], tile[3])
)
]
for decoder_name, extents, offset, args in self.tile:
for i, (decoder_name, extents, offset, args) in enumerate(self.tile):
seek(offset)
decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig
@ -358,8 +355,13 @@ class ImageFile(Image.Image):
else:
b = prefix
while True:
read_bytes = self.decodermaxblock
if i + 1 < len(self.tile):
next_offset = self.tile[i + 1].offset
if next_offset > offset:
read_bytes = next_offset - offset
try:
s = read(self.decodermaxblock)
s = read(read_bytes)
except (IndexError, struct.error) as e:
# truncated png/gif
if LOAD_TRUNCATED_IMAGES:

View File

@ -32,6 +32,7 @@ from . import (
TiffImagePlugin,
)
from ._binary import o32le
from ._util import DeferredError
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.readonly = 1
def load_seek(self, pos: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(pos)
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.fp = self._fp
self.offset = self.__mpoffsets[frame]

View File

@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P":
# we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field
rawmode = "P"
bpp = 8
version = 1
@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
compression_type = _COMPRESSION_TYPES["none"]
flags = 0
if im.mode == "P" and "custom-colormap" in im.info:
assert im.palette is not None
flags = flags & _FLAGS["custom-colormap"]
colormapsize = 4 * 256 + 2
colormapmode = im.palette.mode
colormap = im.getdata().getpalette()
if im.mode == "P":
flags |= _FLAGS["custom-colormap"]
colormap = im.im.getpalette()
colors = len(colormap) // 3
colormapsize = 4 * colors + 2
else:
colormapsize = 0
@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# now write colormap if necessary
if colormapsize > 0:
fp.write(o16b(256))
for i in range(256):
if colormapsize:
fp.write(o16b(colors))
for i in range(colors):
fp.write(o8(i))
if colormapmode == "RGB":
fp.write(
o8(colormap[3 * i])
+ o8(colormap[3 * i + 1])
+ o8(colormap[3 * i + 2])
)
elif colormapmode == "RGBA":
fp.write(
o8(colormap[4 * i])
+ o8(colormap[4 * i + 1])
+ o8(colormap[4 * i + 2])
)
fp.write(colormap[3 * i : 3 * i + 3])
# now convert data to raw form
ImageFile._save(

View File

@ -48,6 +48,7 @@ from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
from ._util import DeferredError
if TYPE_CHECKING:
from . import _imaging
@ -869,6 +870,8 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.dispose: _imaging.ImagingCore | None
dispose_extent = None

View File

@ -27,6 +27,7 @@ from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import si16be as si16
from ._binary import si32be as si32
from ._util import DeferredError
MODES = {
# (photoshop mode, bits) -> (pil mode, required channels)
@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile):
) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
layers = []
if self._layers_position is not None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(self._layers_position)
_layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
layers = _layerinfo(_layer_data, self._layers_size)
@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile):
def seek(self, layer: int) -> None:
if not self._seek_check(layer):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
# seek to given layer (1..max)
_, mode, _, tile = self.layers[layer - 1]

View File

@ -40,6 +40,7 @@ import sys
from typing import IO, TYPE_CHECKING, Any, cast
from . import Image, ImageFile
from ._util import DeferredError
def isInt(f: Any) -> int:
@ -178,6 +179,8 @@ class SpiderImageFile(ImageFile.ImageFile):
raise EOFError(msg)
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
self.fp = self._fp
self.fp.seek(self.stkoffset)

View File

@ -58,7 +58,7 @@ from ._binary import i32be as i32
from ._binary import o8
from ._deprecate import deprecate
from ._typing import StrOrBytesPath
from ._util import is_path
from ._util import DeferredError, is_path
from .TiffTags import TYPES
if TYPE_CHECKING:
@ -1222,6 +1222,8 @@ class TiffImageFile(ImageFile.ImageFile):
self._im = None
def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.fp = self._fp
while len(self._frame_pos) <= frame:

View File

@ -80,8 +80,6 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format_description = "Windows Metafile"
def _open(self) -> None:
self._inch = None
# check placable header
s = self.fp.read(80)
@ -89,10 +87,11 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# placeable windows metafile
# get units per inch
self._inch = word(s, 14)
if self._inch == 0:
inch = word(s, 14)
if inch == 0:
msg = "Invalid inch"
raise ValueError(msg)
self._inch: tuple[float, float] = inch, inch
# get bounding box
x0 = short(s, 6)
@ -103,8 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# normalize size to 72 dots per inch
self.info["dpi"] = 72
size = (
(x1 - x0) * self.info["dpi"] // self._inch,
(y1 - y0) * self.info["dpi"] // self._inch,
(x1 - x0) * self.info["dpi"] // inch,
(y1 - y0) * self.info["dpi"] // inch,
)
self.info["wmf_bbox"] = x0, y0, x1, y1
@ -138,6 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
self.info["dpi"] = xdpi
else:
self.info["dpi"] = xdpi, ydpi
self._inch = xdpi, ydpi
else:
msg = "Unsupported file format"
@ -153,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None:
if dpi is not None and self._inch is not None:
def load(
self, dpi: float | tuple[float, float] | None = None
) -> Image.core.PixelAccess | None:
if dpi is not None:
self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]
if not isinstance(dpi, tuple):
dpi = dpi, dpi
self._size = (
(x1 - x0) * self.info["dpi"] // self._inch,
(y1 - y0) * self.info["dpi"] // self._inch,
int((x1 - x0) * dpi[0] / self._inch[0]),
int((y1 - y0) * dpi[1] / self._inch[1]),
)
return super().load()

View File

@ -687,6 +687,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
#define GET32(p, o) ((DWORD *)(p + o))[0]
static int CALLBACK
enhMetaFileProc(
HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data
) {
PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles);
return 1;
}
PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args) {
HBITMAP bitmap;
@ -767,10 +775,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) {
/* FIXME: make background transparent? configurable? */
FillRect(dc, &rect, GetStockObject(WHITE_BRUSH));
if (!PlayEnhMetaFile(dc, meta, &rect)) {
PyErr_SetString(PyExc_OSError, "cannot render metafile");
goto error;
}
EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect);
/* step 4: extract bits from bitmap */

View File

@ -501,55 +501,49 @@ polygon_generic(
// Needed to draw consistent polygons
xx[j] = xx[j - 1];
j++;
} else if (current->dx != 0 && j % 2 == 1 &&
roundf(xx[j - 1]) == xx[j - 1]) {
} 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];
if ((current->dx > 0 && other_edge->dx <= 0) ||
(current->dx < 0 && other_edge->dx >= 0)) {
if ((ymin != other_edge->ymin && ymin != other_edge->ymax) ||
other_edge->dx == 0) {
continue;
}
// Check if the two edges join to make a corner
if (xx[j - 1] ==
(ymin - other_edge->y0) * other_edge->dx + other_edge->x0) {
if (roundf(xx[j - 1]) ==
roundf(
(ymin - other_edge->y0) * other_edge->dx +
other_edge->x0
)) {
// Determine points from the edges on the next row
// Or if this is the last row, check the previous row
int offset = ymin == ymax ? -1 : 1;
int offset = ymin == current->ymax ? -1 : 1;
adjacent_line_x =
(ymin + offset - current->y0) * current->dx +
current->x0;
adjacent_line_x_other_edge =
(ymin + offset - other_edge->y0) * other_edge->dx +
other_edge->x0;
if (ymin == current->ymax) {
if (current->dx > 0) {
xx[k] =
fmax(
if (ymin + offset >= other_edge->ymin &&
ymin + offset <= other_edge->ymax) {
adjacent_line_x_other_edge =
(ymin + offset - other_edge->y0) * other_edge->dx +
other_edge->x0;
if (xx[j - 1] > adjacent_line_x + 1 &&
xx[j - 1] > adjacent_line_x_other_edge + 1) {
xx[j - 1] =
roundf(fmax(
adjacent_line_x, adjacent_line_x_other_edge
) +
)) +
1;
} else {
xx[k] =
fmin(
} 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
) -
1;
}
} else {
if (current->dx > 0) {
xx[k] = fmin(
adjacent_line_x, adjacent_line_x_other_edge
);
} else {
xx[k] =
fmax(
adjacent_line_x, adjacent_line_x_other_edge
) +
)) -
1;
}
break;
}
break;
}
}
}

View File

@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
return -1;
}
img.orientation = ORIENTATION_TOPLEFT;
img.req_orientation = ORIENTATION_TOPLEFT;
img.col_offset = 0;