Skip to content

Commit a6bc7f3

Browse files
authored
Merge pull request #3372 from pygame-community/ankith26-animation
Add `image.load_animation`
2 parents 1a8ce87 + 07a37e9 commit a6bc7f3

File tree

7 files changed

+155
-0
lines changed

7 files changed

+155
-0
lines changed

buildconfig/stubs/pygame/image.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ _from_bytes_format = Literal["P", "RGB", "RGBX", "RGBA", "ARGB", "BGRA", "ABGR"]
1515

1616
def load(file: FileLike, namehint: str = "") -> Surface: ...
1717
def load_sized_svg(file: FileLike, size: Point) -> Surface: ...
18+
def load_animation(file: FileLike, namehint: str = "") -> list[tuple[Surface, int]]: ...
1819
def save(surface: Surface, file: FileLike, namehint: str = "") -> None: ...
1920
def get_sdl_image_version(linked: bool = True) -> Optional[tuple[int, int, int]]: ...
2021
def get_extended() -> bool: ...

docs/reST/ref/image.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,28 @@ following formats.
135135

136136
.. ## pygame.image.load_sized_svg ##
137137
138+
.. function:: load_animation
139+
140+
| :sl:`load an animation (GIF/WEBP) from a file (or file-like object)`
141+
| :sg:`load_animation(file, namehint="") -> list[tuple[Surface, int]]`
142+
143+
Load an animation (GIF/WEBP) from a file source. You can pass either a
144+
filename, a Python file-like object, or a pathlib.Path. If you pass a raw
145+
file-like object, you may also want to pass the original filename as the
146+
namehint argument so that the file extension can be used to infer the file
147+
format.
148+
149+
This returns a list of tuples (corresponding to every frame of the animation),
150+
where each tuple is a (surface, delay) pair for that frame.
151+
152+
This function requires SDL_image 2.6.0 or above. If pygame was compiled with
153+
an older version, ``pygame.error`` will be raised when this function is
154+
called.
155+
156+
.. versionadded:: 2.5.4
157+
158+
.. ## pygame.image.load_animation ##
159+
138160
.. function:: save
139161

140162
| :sl:`save an image to file (or file-like object)`

examples/data/animated_sample.gif

39.4 KB
Loading

src_c/doc/image_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define DOC_IMAGE "pygame module for image transfer"
33
#define DOC_IMAGE_LOAD "load(file) -> Surface\nload(file, namehint="") -> Surface\nload new image from a file (or file-like object)"
44
#define DOC_IMAGE_LOADSIZEDSVG "load_sized_svg(file, size) -> Surface\nload an SVG image from a file (or file-like object) with the given size"
5+
#define DOC_IMAGE_LOADANIMATION "load_animation(file, namehint="") -> list[tuple[Surface, int]]\nload an animation (GIF/WEBP) from a file (or file-like object)"
56
#define DOC_IMAGE_SAVE "save(Surface, file) -> None\nsave(Surface, file, namehint="") -> None\nsave an image to file (or file-like object)"
67
#define DOC_IMAGE_GETSDLIMAGEVERSION "get_sdl_image_version(linked=True) -> None\nget_sdl_image_version(linked=True) -> (major, minor, patch)\nget version number of the SDL_Image library being used"
78
#define DOC_IMAGE_GETEXTENDED "get_extended() -> bool\ntest if extended image formats can be loaded"

src_c/image.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ static PyObject *extloadobj = NULL;
4949
static PyObject *extsaveobj = NULL;
5050
static PyObject *extverobj = NULL;
5151
static PyObject *ext_load_sized_svg = NULL;
52+
static PyObject *ext_load_animation = NULL;
5253

5354
static inline void
5455
pad(char **data, int padding)
@@ -1885,6 +1886,17 @@ image_load_sized_svg(PyObject *self, PyObject *args, PyObject *kwargs)
18851886
"Support for sized svg image loading was not compiled in.");
18861887
}
18871888

1889+
static PyObject *
1890+
image_load_animation(PyObject *self, PyObject *args, PyObject *kwargs)
1891+
{
1892+
if (ext_load_animation) {
1893+
return PyObject_Call(ext_load_animation, args, kwargs);
1894+
}
1895+
1896+
return RAISE(PyExc_NotImplementedError,
1897+
"Support for animation loading was not compiled in.");
1898+
}
1899+
18881900
static PyMethodDef _image_methods[] = {
18891901
{"load_basic", (PyCFunction)image_load_basic, METH_O, DOC_IMAGE_LOADBASIC},
18901902
{"load_extended", (PyCFunction)image_load_extended,
@@ -1893,6 +1905,8 @@ static PyMethodDef _image_methods[] = {
18931905
DOC_IMAGE_LOAD},
18941906
{"load_sized_svg", (PyCFunction)image_load_sized_svg,
18951907
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADSIZEDSVG},
1908+
{"load_animation", (PyCFunction)image_load_animation,
1909+
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADANIMATION},
18961910

18971911
{"save_extended", (PyCFunction)image_save_extended,
18981912
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_SAVEEXTENDED},
@@ -1973,6 +1987,11 @@ MODINIT_DEFINE(image)
19731987
if (!ext_load_sized_svg) {
19741988
goto error;
19751989
}
1990+
ext_load_animation =
1991+
PyObject_GetAttrString(extmodule, "_load_animation");
1992+
if (!ext_load_animation) {
1993+
goto error;
1994+
}
19761995
Py_DECREF(extmodule);
19771996
}
19781997
else {
@@ -1986,6 +2005,7 @@ MODINIT_DEFINE(image)
19862005
Py_XDECREF(extsaveobj);
19872006
Py_XDECREF(extverobj);
19882007
Py_XDECREF(ext_load_sized_svg);
2008+
Py_XDECREF(ext_load_animation);
19892009
Py_DECREF(extmodule);
19902010
Py_DECREF(module);
19912011
return NULL;

src_c/imageext.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,84 @@ imageext_load_sized_svg(PyObject *self, PyObject *arg, PyObject *kwargs)
206206
#endif /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
207207
}
208208

209+
static PyObject *
210+
imageext_load_animation(PyObject *self, PyObject *arg, PyObject *kwargs)
211+
{
212+
#if SDL_IMAGE_VERSION_ATLEAST(2, 6, 0)
213+
PyObject *obj, *ret = NULL;
214+
char *name = NULL, *ext = NULL, *type = NULL;
215+
IMG_Animation *surfs = NULL;
216+
SDL_RWops *rw = NULL;
217+
static char *kwds[] = {"file", "namehint", NULL};
218+
219+
if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O|s", kwds, &obj, &name)) {
220+
return NULL;
221+
}
222+
223+
rw = pgRWops_FromObject(obj, &ext);
224+
if (rw == NULL) { /* stop on NULL, error already set */
225+
return NULL;
226+
}
227+
228+
if (name) { /* override extension with namehint if given */
229+
type = (strlen(name) != 0) ? iext_find_extension(name) : NULL;
230+
}
231+
else { /* Otherwise type should be whatever ext is, even if ext is NULL */
232+
type = ext;
233+
}
234+
235+
Py_BEGIN_ALLOW_THREADS;
236+
#if SDL_VERSION_ATLEAST(3, 0, 0)
237+
surfs = IMG_LoadAnimationTyped_IO(rw, 1, type);
238+
#else
239+
surfs = IMG_LoadAnimationTyped_RW(rw, 1, type);
240+
#endif
241+
Py_END_ALLOW_THREADS;
242+
243+
if (ext) {
244+
free(ext);
245+
}
246+
247+
if (surfs == NULL) {
248+
return RAISE(pgExc_SDLError, IMG_GetError());
249+
}
250+
251+
ret = PyList_New(surfs->count);
252+
if (!ret) {
253+
goto error;
254+
}
255+
256+
for (int i = 0; i < surfs->count; i++) {
257+
PyObject *frame = (PyObject *)pgSurface_New(surfs->frames[i]);
258+
if (!frame) {
259+
/* IMG_FreeAnimation takes care of freeing of member SDL surfaces
260+
*/
261+
goto error;
262+
}
263+
/* The python surface object now "owns" the sdl surface, so set it
264+
* to null in the animation to prevent double free */
265+
surfs->frames[i] = NULL;
266+
267+
PyObject *listentry = Py_BuildValue("(Oi)", frame, surfs->delays[i]);
268+
Py_DECREF(frame);
269+
if (!listentry) {
270+
goto error;
271+
}
272+
PyList_SET_ITEM(ret, i, listentry);
273+
}
274+
IMG_FreeAnimation(surfs);
275+
return ret;
276+
error:
277+
Py_XDECREF(ret);
278+
IMG_FreeAnimation(surfs);
279+
return NULL;
280+
#else /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
281+
return RAISE(
282+
pgExc_SDLError,
283+
"pygame must be compiled with SDL_image 2.6.0+ to use this function");
284+
#endif /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
285+
}
286+
209287
static PyObject *
210288
image_save_ext(PyObject *self, PyObject *arg, PyObject *kwarg)
211289
{
@@ -352,6 +430,8 @@ static PyMethodDef _imageext_methods[] = {
352430
"Note: Should not be used directly."},
353431
{"_load_sized_svg", (PyCFunction)imageext_load_sized_svg,
354432
METH_VARARGS | METH_KEYWORDS, "Note: Should not be used directly."},
433+
{"_load_animation", (PyCFunction)imageext_load_animation,
434+
METH_VARARGS | METH_KEYWORDS, "Note: Should not be used directly."},
355435
{NULL, NULL, 0, NULL}};
356436

357437
/*DOC*/ static char _imageext_doc[] =

test/image_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,37 @@ def test_load_sized_svg_erroring(self):
13601360
value_error_size,
13611361
)
13621362

1363+
@unittest.skipIf(
1364+
pygame.image.get_sdl_image_version() < (2, 6, 0),
1365+
"load_animation requires SDL_image 2.6.0+",
1366+
)
1367+
def test_load_animation(self):
1368+
# test loading from a file
1369+
SAMPLE_FRAMES = 10
1370+
SAMPLE_DELAY = 150
1371+
SAMPLE_SIZE = (312, 312)
1372+
gif_path = pathlib.Path(example_path("data/animated_sample.gif"))
1373+
for inp in (
1374+
(str(gif_path),), # string path, no namehint
1375+
(gif_path,), # pathlib.Path path, no namehint
1376+
(io.BytesIO(gif_path.read_bytes()),), # file-like object, no namehint
1377+
(
1378+
io.BytesIO(gif_path.read_bytes()),
1379+
gif_path.name,
1380+
), # file-like object, with namehint
1381+
):
1382+
with self.subTest(f"Test load_animation", inp=inp):
1383+
s = pygame.image.load_animation(*inp)
1384+
self.assertIsInstance(s, list)
1385+
self.assertEqual(len(s), SAMPLE_FRAMES)
1386+
for val in s:
1387+
self.assertIsInstance(val, tuple)
1388+
frame, delay = val
1389+
self.assertIsInstance(frame, pygame.Surface)
1390+
self.assertEqual(frame.size, SAMPLE_SIZE)
1391+
self.assertIsInstance(delay, int)
1392+
self.assertEqual(delay, SAMPLE_DELAY)
1393+
13631394
def test_load_pathlib(self):
13641395
"""works loading using a Path argument."""
13651396
path = pathlib.Path(example_path("data/asprite.bmp"))

0 commit comments

Comments
 (0)