Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
32a6c22
did not work with all .cur files
Sep 21, 2022
e28357e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 21, 2022
f4fde29
added ability to save as cursor
Sep 22, 2022
95b8061
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 22, 2022
fbcf1b6
removed unused imports
Sep 22, 2022
6a5878d
reverted to original as change no longer necessary
Sep 22, 2022
d722d0d
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 22, 2022
2e08e89
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 22, 2022
73e5955
hotspots now accessible in info
Sep 22, 2022
a18bb13
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 22, 2022
6f22968
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 22, 2022
7585c21
added exceptions to pass tests
Sep 22, 2022
569feee
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 22, 2022
69b4ebf
Hopefully corrected bpp issue with older .cur
Sep 22, 2022
1d846c2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 22, 2022
3eda472
added more cursors for testing
Sep 23, 2022
c74859d
fixed bug with transparency
Sep 23, 2022
0b3bc19
added another test
Sep 23, 2022
c11dea9
added .ani plugin
Sep 23, 2022
9b8c29f
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 23, 2022
86476a9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 23, 2022
17453f2
fixed bug in test
Sep 23, 2022
ce07c0b
Merge branch 'python-pillow:main' into main
jlwoolf Sep 27, 2022
cd4ee04
docstring for cur plugin
Sep 27, 2022
da093b9
doc string for ani plugin
Sep 27, 2022
fc8a288
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2022
084704d
linting
Sep 27, 2022
0c1ff74
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 27, 2022
bac4ae4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2022
01d71be
bitmaps should be transparent if 32bits
Sep 27, 2022
551e2a6
completed cursor tests
Sep 27, 2022
5eb3e53
added animated cursor tests
Sep 27, 2022
8aa0a86
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 27, 2022
3547591
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2022
915df6c
fixed bug regarding bitmap (added alpha flag)
Sep 27, 2022
7045898
Merge branch 'main' of https://github.com/jlwoolf/Pillow
Sep 27, 2022
d46f45a
Merge branch 'python-pillow:main' into main
jlwoolf Sep 27, 2022
e8684d7
Update src/PIL/CurImagePlugin.py
jlwoolf Oct 3, 2022
1d57114
Merge branch 'python-pillow:main' into main
jlwoolf Oct 4, 2022
792f519
Do not use PyAccess after re-assigning im
radarhere Oct 20, 2022
05be477
Merge branch 'main' into main
radarhere Oct 24, 2022
444a58d
Merge branch 'jlwoolf'
radarhere Oct 27, 2022
6ef6973
Merge branch 'python-pillow'
radarhere Dec 30, 2022
23f5550
Merge branch 'python-pillow'
radarhere Feb 25, 2023
c5e8d12
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 25, 2023
cd1fec4
Merge branch 'main' into main
radarhere Sep 2, 2023
5f6d3f1
mode is read-only
radarhere Sep 2, 2023
c52ade8
Merge branch 'main' into main
radarhere Dec 9, 2023
227c84f
Removed unnecessary list comprehension
radarhere Dec 9, 2023
bc01ee2
Merge branch 'main' into main
radarhere Dec 24, 2023
76179a2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 24, 2023
4128e8c
Merge branch 'main' into main
radarhere Jan 21, 2024
4524742
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 21, 2024
50cbabe
Merge branch 'main' into main
radarhere Feb 24, 2024
7268920
Merge branch 'main' into main
radarhere Mar 1, 2024
374f4d0
Merge branch 'main' into main
radarhere Apr 21, 2024
88750f4
Merge branch 'main' into main
radarhere May 18, 2024
7188a65
Merge branch 'main' into main
radarhere Jul 19, 2024
2ef7a51
Merge branch 'main' into main
radarhere Oct 4, 2024
b16b579
Merge branch 'main' into main
radarhere Mar 3, 2025
4c94fa4
Merge branch 'main' into main
radarhere Apr 26, 2025
26be2ee
Lint fixes
radarhere Apr 26, 2025
8eba937
Replace slice and comparison with startswith
radarhere May 9, 2025
1530795
Use IcoFile sizes method
radarhere Apr 26, 2025
0842985
Added type hints
radarhere May 9, 2025
0bf71a4
Use _accept check in _open
radarhere May 10, 2025
eb8bc46
_min_frame is already zero
radarhere May 10, 2025
988abb4
Return PixelAccess from load
radarhere May 10, 2025
f5d9838
Do not load at end of seek
radarhere May 10, 2025
ccfc252
Use n_frames and _seek_check
radarhere May 10, 2025
54c3c93
Merge branch 'main' into main
radarhere May 28, 2025
60c89cb
Removed alpha property
radarhere May 10, 2025
879f4b7
Moved hotspots into separate list to avoid conflicting header types
radarhere Jul 28, 2025
f5d866f
Use assert_image_equal and assert_image_equal_tofile
radarhere Jul 28, 2025
fe21dfa
Simplified code
radarhere Sep 3, 2025
ff1e565
Removed unused method
radarhere Sep 3, 2025
7a161f6
Merge branch 'main' into main
radarhere Oct 20, 2025
bdf1f31
Raise error earlier
radarhere Dec 22, 2025
83d6815
Added tell()
radarhere Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/ani/aero_busy.ani
Binary file not shown.
Binary file added Tests/images/ani/aero_busy_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/ani/aero_busy_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/ani/posy_busy.ani
Binary file not shown.
Binary file added Tests/images/ani/posy_busy_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/ani/posy_busy_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/ani/stopwtch.ani
Binary file not shown.
Binary file added Tests/images/ani/stopwtch_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/ani/stopwtch_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/cur/aero_arrow.cur
Binary file not shown.
File renamed without changes.
File renamed without changes.
Binary file added Tests/images/cur/posy_link.cur
Binary file not shown.
Binary file added Tests/images/cur/posy_link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/cur/stopwtch.cur
Binary file not shown.
Binary file added Tests/images/cur/win98_arrow.cur
Binary file not shown.
Binary file added Tests/images/cur/win98_arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions Tests/test_file_ani.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from __future__ import annotations

from io import BytesIO

import pytest

from PIL import Image

from .helper import assert_image_equal_tofile


def test_aero_busy() -> None:
with Image.open("Tests/images/ani/aero_busy.ani") as im:
assert im.size == (64, 64)
assert im.n_frames == 18

assert_image_equal_tofile(im, "Tests/images/ani/aero_busy_0.png")

im.seek(8)
assert im.tell() == 8
assert_image_equal_tofile(im, "Tests/images/ani/aero_busy_8.png")

with pytest.raises(EOFError):
im.seek(-1)

with pytest.raises(EOFError):
im.seek(18)


def test_posy_busy() -> None:
with Image.open("Tests/images/ani/posy_busy.ani") as im:
assert im.size == (96, 96)
assert im.n_frames == 77

assert_image_equal_tofile(im, "Tests/images/ani/posy_busy_0.png")

im.seek(24)
assert_image_equal_tofile(im, "Tests/images/ani/posy_busy_24.png")

with pytest.raises(EOFError):
im.seek(77)


def test_seq_rate() -> None:
with Image.open("Tests/images/ani/stopwtch.ani") as im:
assert im.size == (32, 32)
assert im.n_frames == 8

assert im.info["seq"][:3] == [0, 1, 0]
assert im.info["rate"] == [8, 16, 16] + [8] * 42

assert_image_equal_tofile(im, "Tests/images/ani/stopwtch_0.png")

im.seek(5)
assert_image_equal_tofile(im, "Tests/images/ani/stopwtch_5.png")

with pytest.raises(EOFError):
im.seek(8)


def test_save() -> None:
filenames = [
"aero_busy_0.png",
"aero_busy_8.png",
"posy_busy_0.png",
"posy_busy_24.png",
"stopwtch_0.png",
"stopwtch_5.png",
]

images = [Image.open("Tests/images/ani/" + filename) for filename in filenames]

with BytesIO() as output:
images[0].save(
output, append_images=[images[1]], seq=[0, 1], rate=[5, 10], format="ANI"
)

with Image.open(output, formats=["ANI"]) as im:
assert im.tobytes() == images[0].tobytes()
im.seek(1)
assert im.tobytes() == images[1].tobytes()
assert im.info["seq"] == [0, 1]
assert im.info["rate"] == [5, 10]

with BytesIO() as output:
images[2].save(
output,
append_images=[images[3]],
seq=[1, 0],
rate=[2, 2],
format="ANI",
sizes=[(96, 96)],
)

with Image.open(output, formats=["ANI"]) as im:
assert im.tobytes() == images[2].tobytes()
im.seek(1)
assert im.tobytes() == images[3].tobytes()
assert im.info["seq"] == [1, 0]
assert im.info["rate"] == [2, 2]

with BytesIO() as output:
images[4].save(
output, append_images=[images[5]], seq=[0, 1], rate=[3, 4], format="ANI"
)

with Image.open(output, formats=["ANI"]) as im:
assert im.tobytes() == images[4].tobytes()
im.seek(1)
assert im.tobytes() == images[5].tobytes()
assert im.info["seq"] == [0, 1]
assert im.info["rate"] == [3, 4]

with BytesIO() as output:
images[0].save(
output,
append_images=images[1:],
seq=[0, 2, 4, 1, 3, 5, 0, 1, 0, 1],
rate=[1, 2, 3, 1, 2, 3, 1, 2, 3, 4],
format="ANI",
sizes=[(32, 32)],
)

with Image.open(output, formats=["ANI"]) as im:
assert im.n_frames == 6
assert im.info["seq"] == [0, 2, 4, 1, 3, 5, 0, 1, 0, 1]
assert im.info["rate"] == [1, 2, 3, 1, 2, 3, 1, 2, 3, 4]
assert im.size == (32, 32)

im.seek(4)
assert im.tobytes() == images[4].tobytes()

with BytesIO() as output:
with pytest.raises(ValueError):
images[0].save(
output,
append_images=images[1:],
seq=[0, 1, 8, 1, 2],
rate=[1, 1, 1, 1, 1],
format="ANI",
sizes=[(32, 32)],
)

with pytest.raises(ValueError):
images[0].save(
output,
append_images=images[1:],
seq=[0, 1, 1, 1, 2],
rate=[1, 1, 1, 1],
format="ANI",
sizes=[(32, 32)],
)

with pytest.raises(ValueError):
images[0].save(
output,
append_images=images[1:],
rate=[1, 1, 1, 1],
format="ANI",
sizes=[(32, 32)],
)
121 changes: 115 additions & 6 deletions Tests/test_file_cur.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32

TEST_FILE = "Tests/images/deerstalker.cur"
from .helper import assert_image_equal


def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
with Image.open("Tests/images/cur/deerstalker.cur") as im:
assert im.size == (32, 32)
assert im.info["hotspots"] == [(0, 0)]
assert isinstance(im, CurImagePlugin.CurImageFile)
# Check some pixel colors to ensure image is loaded properly

# Check pixel colors to ensure image is loaded properly
assert im.getpixel((10, 1)) == (0, 0, 0, 0)
assert im.getpixel((11, 1)) == (253, 254, 254, 1)
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
Expand All @@ -36,21 +38,128 @@ def test_largest_cursor() -> None:
+ o16(0) # planes
+ o16(1) # bits
)

fp = BytesIO()
im = Image.new("1", (8, 8))
im.save(fp, "PNG")
data += fp.getvalue()

with Image.open(BytesIO(data)) as im:
assert im.size == (8, 8)


def test_posy_link() -> None:
with Image.open("Tests/images/cur/posy_link.cur") as im:
assert im.size == (128, 128)
assert im.info["sizes"] == {(128, 128), (96, 96), (64, 64), (48, 48), (32, 32)}
assert im.info["hotspots"] == [(25, 7), (18, 5), (12, 3), (9, 2), (5, 1)]

# check pixel colors
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((20, 20)) == (0, 0, 0, 255)
assert im.getpixel((40, 40)) == (255, 255, 255, 255)

im.size = (32, 32)
im.load()
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((10, 10)) == (191, 191, 191, 255)


def test_stopwtch() -> None:
with Image.open("Tests/images/cur/stopwtch.cur") as im:
assert im.size == (32, 32)
assert im.info["hotspots"] == [(16, 19)]

assert im.getpixel((16, 16)) == (0, 0, 255, 255)
assert im.getpixel((8, 16)) == (255, 0, 0, 255)


def test_win98_arrow() -> None:
with Image.open("Tests/images/cur/win98_arrow.cur") as im:
assert im.size == (32, 32)
assert im.info["hotspots"] == [(10, 10)]

assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((16, 16)) == (0, 0, 0, 255)
assert im.getpixel((14, 19)) == (255, 255, 255, 255)


def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
invalid_file = "Tests/images/cur/posy_link.png"

with pytest.raises(SyntaxError):
CurImagePlugin.CurImageFile(invalid_file)

no_cursors_file = "Tests/images/no_cursors.cur"
no_cursors_file = "Tests/images/cur/no_cursors.cur"

cur = CurImagePlugin.CurImageFile(TEST_FILE)
cur = CurImagePlugin.CurImageFile("Tests/images/cur/deerstalker.cur")
assert cur.fp is not None
cur.fp.close()
with open(no_cursors_file, "rb") as cur.fp:
with pytest.raises(TypeError):
cur._open()


def test_save_win98_arrow() -> None:
with Image.open("Tests/images/cur/win98_arrow.png") as im:
# save the data
with BytesIO() as output:
im.save(
output,
format="CUR",
sizes=[(32, 32)],
hotspots=[(10, 10)],
bitmap_format="bmp",
)
with Image.open(output) as reloaded:
assert_image_equal(im, reloaded)

with BytesIO() as output:
im.save(output, format="CUR")

# check default save params
with Image.open(output) as reloaded:
assert reloaded.size == (32, 32)
assert reloaded.info["sizes"] == {(32, 32), (24, 24), (16, 16)}
assert reloaded.info["hotspots"] == [(0, 0), (0, 0), (0, 0)]


def test_save_posy_link() -> None:
sizes = [(128, 128), (96, 96), (64, 64), (48, 48), (32, 32)]
hotspots = [(25, 7), (18, 5), (12, 3), (9, 2), (5, 1)]

with Image.open("Tests/images/cur/posy_link.png") as im:
# save the data
with BytesIO() as output:
im.save(
output,
sizes=sizes,
hotspots=hotspots,
format="CUR",
bitmap_format="bmp",
)

# make sure saved output is readable
# and sizes/hotspots are correct
with Image.open(output, formats=["CUR"]) as reloaded:
assert (128, 128) == reloaded.size
assert set(sizes) == reloaded.info["sizes"]

with BytesIO() as output:
im.save(output, sizes=sizes[3:], hotspots=hotspots[3:], format="CUR")

# make sure saved output is readable
# and sizes/hotspots are correct
with Image.open(output, formats=["CUR"]) as reloaded:
assert reloaded.size == (48, 48)
assert reloaded.info["sizes"] == set(sizes[3:])

# check error is thrown when size and hotspot length don't match
with pytest.raises(ValueError):
im.save(
output,
sizes=sizes[2:],
hotspots=hotspots[3:],
format="CUR",
bitmap_format="bmp",
)
Loading
Loading