Skip to content

Commit

Permalink
Merge branch 'main' into context_manager
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Dec 28, 2024
2 parents f878cfd + e8dad19 commit 68e9ee5
Show file tree
Hide file tree
Showing 38 changed files with 160 additions and 125 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)
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 @@ -1030,8 +1030,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
12 changes: 12 additions & 0 deletions Tests/test_file_mpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,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"
9 changes: 2 additions & 7 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,20 +810,15 @@ 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:
b = BytesIO()
mystdout: TextIOWrapper | BytesIO = TextIOWrapper(b) if buffer else b

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

with Image.open(b) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)

Expand Down
9 changes: 2 additions & 7 deletions Tests/test_file_ppm.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,19 +368,14 @@ 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:
b = BytesIO()
mystdout: TextIOWrapper | BytesIO = TextIOWrapper(b) if buffer else b

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

with Image.open(b) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)
3 changes: 3 additions & 0 deletions Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,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 @@ -76,6 +76,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
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 @@ -164,7 +164,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 @@ -736,7 +736,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 @@ -2499,7 +2499,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 @@ -2546,6 +2546,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 @@ -101,8 +101,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
2 changes: 1 addition & 1 deletion src/PIL/ImtImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _open(self) -> None:
"raw",
(0, 0) + self.size,
self.fp.tell() - len(buffer),
(self.mode, 0, 1),
self.mode,
)
]

Expand Down
9 changes: 8 additions & 1 deletion src/PIL/JpegImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,13 @@ def __getattr__(self, name: str) -> Any:
return getattr(self, "_" + name)
raise AttributeError(name)

def __getstate__(self) -> list[Any]:
return super().__getstate__() + [self.layers, self.layer]

def __setstate__(self, state: list[Any]) -> None:
super().__setstate__(state)
self.layers, self.layer = state[5:]

def load_read(self, read_bytes: int) -> bytes:
"""
internal: read more image data
Expand Down Expand Up @@ -758,7 +765,7 @@ def validate_qtables(
extra = info.get("extra", b"")

MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp"))
xmp = info.get("xmp")
if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
Expand Down
6 changes: 3 additions & 3 deletions src/PIL/MspImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def _open(self) -> None:
self._size = i16(s, 4), i16(s, 6)

if s[:4] == b"DanM":
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
else:
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)]
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]


class MspDecoder(ImageFile.PyDecoder):
Expand Down Expand Up @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h))

# image body
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))])
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])


#
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/PcdImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _open(self) -> None:

self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)]
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]

def load_end(self) -> None:
if self.tile_post_rotate:
Expand Down
4 changes: 1 addition & 3 deletions src/PIL/PixarImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ def _open(self) -> None:
# FIXME: to be continued...

# create tile descriptor (assuming "dumped")
self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))
]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)]


#
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/QoiImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _open(self) -> None:
self._mode = "RGB" if channels == 3 else "RGBA"

self.fp.seek(1, os.SEEK_CUR) # colorspace
self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)]
self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]


class QoiDecoder(ImageFile.PyDecoder):
Expand Down
Loading

0 comments on commit 68e9ee5

Please sign in to comment.