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 # to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im: with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") 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: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") 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: def test_open_missing_samplesperpixel(self) -> None:
with Image.open( with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" "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) im.save(outfile)
converted = open_with_magick(magick, tmp_path, 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) assert_image_equal(converted, im)
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
roundtrip(tmp_path, mode) roundtrip(tmp_path, mode)
@pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path: Path) -> None: def test_p_mode(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "P" mode = "P"

View File

@ -8,7 +8,7 @@ import pytest
from PIL import Image, ImageFile, WmfImagePlugin 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: def test_load_raw() -> None:
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
pass 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: def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler): class TestHandler(ImageFile.StubHandler):
methodCalled = False 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) 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")) @pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext: str, tmp_path: Path) -> None: def test_save(ext: str, tmp_path: Path) -> None:

View File

@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
BLACK, BLACK,
) )
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") 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: def test_polygon2() -> None:

View File

@ -131,6 +131,26 @@ class TestImageFile:
assert_image_equal(im1, im2) 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: def test_raise_oserror(self) -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -4,21 +4,12 @@
Security Security
======== ========
TODO Undefined shift when loading compressed DDS images
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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
:cve:`YYYY-XXXXX`: TODO present since Pillow 3.4.0.
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations Deprecations
============ ============
@ -36,10 +27,14 @@ an :py:class:`PIL.ImageFile.ImageFile` instance.
API Changes 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 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:: DXT5, BC2, BC3 and BC5 are supported::
im.save("out.dds", pixel_format="DXT1") im.save("out.dds", pixel_format="DXT1")
Other Changes
=============
TODO
^^^^
TODO

View File

@ -48,6 +48,8 @@ BIT2MODE = {
32: ("RGB", "BGRX"), 32: ("RGB", "BGRX"),
} }
USE_RAW_ALPHA = False
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"BM") return prefix.startswith(b"BM")
@ -242,7 +244,9 @@ class BmpImageFile(ImageFile.ImageFile):
msg = "Unsupported BMP bitfields layout" msg = "Unsupported BMP bitfields layout"
raise OSError(msg) raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]: 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" raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in ( elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"], self.COMPRESSIONS["RLE8"],

View File

@ -24,6 +24,7 @@ from __future__ import annotations
from . import Image from . import Image
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._util import DeferredError
from .PcxImagePlugin import PcxImageFile from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile):
def seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame self.frame = frame
self.fp = self._fp self.fp = self._fp
self.fp.seek(self._offset[frame]) 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 i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._binary import o8 from ._binary import o8
from ._util import DeferredError
# #
# decoder # decoder
@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile):
self._seek(f) self._seek(f)
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ from . import (
TiffImagePlugin, TiffImagePlugin,
) )
from ._binary import o32le from ._binary import o32le
from ._util import DeferredError
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.readonly = 1 self.readonly = 1
def load_seek(self, pos: int) -> None: def load_seek(self, pos: int) -> None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(pos) self._fp.seek(pos)
def seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.fp = self._fp self.fp = self._fp
self.offset = self.__mpoffsets[frame] 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: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P": 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" rawmode = "P"
bpp = 8 bpp = 8
version = 1 version = 1
@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
compression_type = _COMPRESSION_TYPES["none"] compression_type = _COMPRESSION_TYPES["none"]
flags = 0 flags = 0
if im.mode == "P" and "custom-colormap" in im.info: if im.mode == "P":
assert im.palette is not None flags |= _FLAGS["custom-colormap"]
flags = flags & _FLAGS["custom-colormap"] colormap = im.im.getpalette()
colormapsize = 4 * 256 + 2 colors = len(colormap) // 3
colormapmode = im.palette.mode colormapsize = 4 * colors + 2
colormap = im.getdata().getpalette()
else: else:
colormapsize = 0 colormapsize = 0
@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# now write colormap if necessary # now write colormap if necessary
if colormapsize > 0: if colormapsize:
fp.write(o16b(256)) fp.write(o16b(colors))
for i in range(256): for i in range(colors):
fp.write(o8(i)) fp.write(o8(i))
if colormapmode == "RGB": fp.write(colormap[3 * i : 3 * i + 3])
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])
)
# now convert data to raw form # now convert data to raw form
ImageFile._save( ImageFile._save(

View File

@ -48,6 +48,7 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
from ._util import DeferredError
if TYPE_CHECKING: if TYPE_CHECKING:
from . import _imaging from . import _imaging
@ -869,6 +870,8 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame: int, rewind: bool = False) -> None: def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None assert self.png is not None
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.dispose: _imaging.ImagingCore | None self.dispose: _imaging.ImagingCore | None
dispose_extent = 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 i32be as i32
from ._binary import si16be as si16 from ._binary import si16be as si16
from ._binary import si32be as si32 from ._binary import si32be as si32
from ._util import DeferredError
MODES = { MODES = {
# (photoshop mode, bits) -> (pil mode, required channels) # (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]]]: ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
layers = [] layers = []
if self._layers_position is not None: if self._layers_position is not None:
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self._fp.seek(self._layers_position) self._fp.seek(self._layers_position)
_layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
layers = _layerinfo(_layer_data, self._layers_size) layers = _layerinfo(_layer_data, self._layers_size)
@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile):
def seek(self, layer: int) -> None: def seek(self, layer: int) -> None:
if not self._seek_check(layer): if not self._seek_check(layer):
return return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
# seek to given layer (1..max) # seek to given layer (1..max)
_, mode, _, tile = self.layers[layer - 1] _, mode, _, tile = self.layers[layer - 1]

View File

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

View File

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

View File

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

View File

@ -687,6 +687,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
#define GET32(p, o) ((DWORD *)(p + o))[0] #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 * PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args) { PyImaging_DrawWmf(PyObject *self, PyObject *args) {
HBITMAP bitmap; HBITMAP bitmap;
@ -767,10 +775,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) {
/* FIXME: make background transparent? configurable? */ /* FIXME: make background transparent? configurable? */
FillRect(dc, &rect, GetStockObject(WHITE_BRUSH)); FillRect(dc, &rect, GetStockObject(WHITE_BRUSH));
if (!PlayEnhMetaFile(dc, meta, &rect)) { EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect);
PyErr_SetString(PyExc_OSError, "cannot render metafile");
goto error;
}
/* step 4: extract bits from bitmap */ /* step 4: extract bits from bitmap */

View File

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

View File

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