Skip to content

Commit

Permalink
Merge branch 'main' into fp
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Dec 28, 2024
2 parents ab708da + 41a89ea commit b8f8799
Show file tree
Hide file tree
Showing 44 changed files with 191 additions and 149 deletions.
Binary file modified Tests/images/imagedraw/discontiguous_corners_polygon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Tests/test_file_bufrstub.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
im.save(temp_file)
assert handler.saved

BufrStubImagePlugin._handler = None
BufrStubImagePlugin.register_handler(None)
10 changes: 4 additions & 6 deletions Tests/test_file_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

from PIL import ContainerIO, Image

from .helper import hopper

TEST_FILE = "Tests/images/dummy.container"


Expand All @@ -15,15 +13,15 @@ def test_sanity() -> None:


def test_isatty() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 0, 0)

assert container.isatty() is False


def test_seekable() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 0, 0)

assert container.seekable() is True

Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_gribstub.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
im.save(temp_file)
assert handler.saved

GribStubImagePlugin._handler = None
GribStubImagePlugin.register_handler(None)
2 changes: 1 addition & 1 deletion Tests/test_file_hdf5stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
im.save(temp_file)
assert handler.saved

Hdf5StubImagePlugin._handler = None
Hdf5StubImagePlugin.register_handler(None)
9 changes: 7 additions & 2 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,13 @@ def test_save_xmp(self, tmp_path: Path) -> None:
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"

im.info["xmp"] = b"1" * 65504
im.save(f)
# Check that XMP is not saved from image info
reloaded.save(f)

with Image.open(f) as reloaded:
assert "xmp" not in reloaded.info

im.save(f, xmp=b"1" * 65504)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504

Expand Down
5 changes: 3 additions & 2 deletions Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,9 @@ def test_pclr() -> None:


def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
with Image.open(path) as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"

# Test an image that is truncated partway through a codestream
with open("Tests/images/comment.jp2", "rb") as fp:
Expand Down
12 changes: 12 additions & 0 deletions Tests/test_file_mpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,15 @@ def test_save_all() -> None:
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info


def test_save_xmp() -> None:
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im2.encoderinfo = {"xmp": b"Second frame"}
im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])

assert im_reloaded.info["xmp"] == b"First frame"

im_reloaded.seek(1)
assert im_reloaded.info["xmp"] == b"Second frame"
8 changes: 2 additions & 6 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,22 +772,18 @@ def test_seek(self) -> None:
im.seek(1)

@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:

class MyStdOut:
buffer = BytesIO()

mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()

sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)

with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")

# Reset stdout
sys.stdout = old_stdout

if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
Expand Down
8 changes: 2 additions & 6 deletions Tests/test_file_ppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None:


@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:

class MyStdOut:
buffer = BytesIO()

mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()

sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)

with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")

# Reset stdout
sys.stdout = old_stdout

if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
Expand Down
10 changes: 5 additions & 5 deletions Tests/test_image_thumbnail.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,20 @@ def test_transposed() -> None:
assert im.size == (590, 88)


def test_load_first_unless_jpeg() -> None:
def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None:
# Test that thumbnail() still uses draft() for JPEG
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
original_draft = im.draft

def im_draft(
mode: str, size: tuple[int, int]
mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None:
result = draft(mode, size)
result = original_draft(mode, size)
assert result is not None

return result

im.draft = im_draft
monkeypatch.setattr(im, "draft", im_draft)

im.thumbnail((64, 64))

Expand Down
3 changes: 3 additions & 0 deletions Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None:
def test_discontiguous_corners_polygon() -> None:
img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
draw.polygon(
((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK
)
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
draw.polygon(
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
Expand Down
13 changes: 13 additions & 0 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ def test_ico(self) -> None:
assert p.image is not None
assert (48, 48) == p.image.size

@pytest.mark.filterwarnings("ignore:Corrupt EXIF data")
def test_incremental_tiff(self) -> None:
with ImageFile.Parser() as p:
with open("Tests/images/hopper.tif", "rb") as f:
p.feed(f.read(1024))

# Check that insufficient data was given in the first feed
assert not p.image

p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size

@skip_unless_feature("webp")
def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p:
Expand Down
11 changes: 11 additions & 0 deletions Tests/test_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ def test_pickle_image(
helper_pickle_file(tmp_path, protocol, test_file, test_mode)


def test_pickle_jpeg() -> None:
# Arrange
with Image.open("Tests/images/hopper.jpg") as image:
# Act: roundtrip
unpickled_image = pickle.loads(pickle.dumps(image))

# Assert
assert len(unpickled_image.layer) == 3
assert unpickled_image.layers == 3


def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
filename = str(tmp_path / "temp.pkl")
Expand Down
6 changes: 6 additions & 0 deletions docs/releasenotes/11.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ zlib library, and what version of zlib-ng is being used::
Other Changes
=============

Reading JPEG 2000 comments
^^^^^^^^^^^^^^^^^^^^^^^^^^

When opening a JPEG 2000 image, the comment may now be read into
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.

zlib-ng in wheels
^^^^^^^^^^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def __iter__(self) -> Iterator[str]:
for x in ("raqm", "fribidi")
]
+ [
("disable-platform-guessing", None, "Disable platform guessing on Linux"),
("disable-platform-guessing", None, "Disable platform guessing"),
("debug", None, "Debug logging"),
]
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/BlpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def _open(self) -> None:
raise BLPFormatError(msg)

self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)]


class _BLPBaseDecoder(ImageFile.PyDecoder):
Expand Down
4 changes: 1 addition & 3 deletions src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,9 +561,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
)
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])


def _accept(prefix: bytes) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/EpsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(fp, "flush"):
fp.flush()

ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)])
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])

fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n")
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/FliImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def _seek(self, frame: int) -> None:
framesize = i32(s)

self.decodermaxblock = framesize
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)]
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]

self.__offset += framesize

Expand Down
2 changes: 1 addition & 1 deletion src/PIL/FpxImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
"raw",
(x, y, x1, y1),
i32(s, i) + 28,
(self.rawmode,),
self.rawmode,
)
)

Expand Down
2 changes: 1 addition & 1 deletion src/PIL/FtexImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _open(self) -> None:
self._mode = "RGBA"
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED:
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
else:
msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg)
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/GdImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _open(self) -> None:
"raw",
(0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4,
("L", 0, 1),
"L",
)
]

Expand Down
9 changes: 7 additions & 2 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ def __getstate__(self) -> list[Any]:

def __setstate__(self, state: list[Any]) -> None:
Image.__init__(self)
info, mode, size, palette, data = state
info, mode, size, palette, data = state[:5]
self.info = info
self._mode = mode
self._size = size
Expand Down Expand Up @@ -2556,7 +2556,7 @@ def save(
self._ensure_mutable()

save_all = params.pop("save_all", False)
self.encoderinfo = params
self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
self.encoderconfig: tuple[Any, ...] = ()

preinit()
Expand Down Expand Up @@ -2603,6 +2603,11 @@ def save(
except PermissionError:
pass
raise
finally:
try:
del self.encoderinfo
except AttributeError:
pass
if open_fp:
fp.close()

Expand Down
4 changes: 2 additions & 2 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ def _tilesort(t: _Tile) -> int:
class _Tile(NamedTuple):
codec_name: str
extents: tuple[int, int, int, int] | None
offset: int
args: tuple[Any, ...] | str | None
offset: int = 0
args: tuple[Any, ...] | str | None = None


#
Expand Down
31 changes: 10 additions & 21 deletions src/PIL/ImageGrab.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,28 +104,17 @@ def grab(

def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
commands = [
'set theFile to (open for access POSIX file "'
+ filepath
+ '" with write permission)',
"try",
" write (the clipboard as «class PNGf») to theFile",
"end try",
"close access theFile",
]
script = ["osascript"]
for command in commands:
script += ["-e", command]
subprocess.call(script)
p = subprocess.run(
["osascript", "-e", "get the clipboard as «class PNGf»"],
capture_output=True,
)
if p.returncode != 0:
return None

im = None
if os.stat(filepath).st_size != 0:
im = Image.open(filepath)
im.load()
os.unlink(filepath)
return im
import binascii

data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
return Image.open(data)
elif sys.platform == "win32":
fmt, data = Image.core.grabclipboard_win32()
if fmt == "file": # CF_HDROP
Expand Down
7 changes: 4 additions & 3 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
8: Image.Transpose.ROTATE_90,
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
if in_place:
image.im = transposed_image.im
image._size = transposed_image._size
image.im = image.im.transpose(method)
image._size = image.im.size
else:
transposed_image = image.transpose(method)
exif_image = image if in_place else transposed_image

exif = exif_image.getexif()
Expand Down
Loading

0 comments on commit b8f8799

Please sign in to comment.