Skip to content

Use namedtuple for pygame.image.load_animation #3442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 9 additions & 5 deletions buildconfig/stubs/pygame/image.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ following formats.
.. versionaddedold:: 1.8 Saving PNG and JPEG files.
"""

from typing import Literal, Optional, Union
from typing import Literal, NamedTuple, Optional

from pygame.bufferproxy import BufferProxy
from pygame.surface import Surface
from pygame.typing import FileLike, IntPoint, Point
from typing_extensions import (
Expand All @@ -78,6 +77,10 @@ _to_bytes_format = Literal[
]
_from_bytes_format = Literal["P", "RGB", "RGBX", "RGBA", "ARGB", "BGRA", "ABGR"]

class _AnimationFrame(NamedTuple):
surface: Surface
delay_ms: float

def load(file: FileLike, namehint: str = "") -> Surface:
"""Load new image from a file (or file-like object).

Expand Down Expand Up @@ -136,7 +139,7 @@ def load_sized_svg(file: FileLike, size: Point) -> Surface:
.. versionadded:: 2.4.0
"""

def load_animation(file: FileLike, namehint: str = "") -> list[tuple[Surface, float]]:
def load_animation(file: FileLike, namehint: str = "") -> list[_AnimationFrame]:
"""Load an animation (GIF/WEBP) from a file (or file-like object) as a list of frames.

Load an animation (GIF/WEBP) from a file source. You can pass either a
Expand All @@ -145,8 +148,9 @@ def load_animation(file: FileLike, namehint: str = "") -> list[tuple[Surface, fl
namehint argument so that the file extension can be used to infer the file
format.

This returns a list of tuples (corresponding to every frame of the animation),
where each tuple is a (surface, delay in milliseconds) pair for that frame.
This returns a list of named tuples (corresponding to every frame of the animation),
where each tuple is a (surface, delay in milliseconds) pair for that frame. Being
named tuples, the items can be accessed as frame.surface and frame.delay_ms.

This function requires SDL_image 2.6.0 or above. If pygame was compiled with
an older version, ``pygame.error`` will be raised when this function is
Expand Down
2 changes: 1 addition & 1 deletion src_c/doc/image_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#define DOC_IMAGE "Pygame module for image transfer."
#define DOC_IMAGE_LOAD "load(file, namehint='') -> Surface\nLoad new image from a file (or file-like object)."
#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."
#define DOC_IMAGE_LOADANIMATION "load_animation(file, namehint='') -> list[tuple[Surface, float]]\nLoad an animation (GIF/WEBP) from a file (or file-like object) as a list of frames."
#define DOC_IMAGE_LOADANIMATION "load_animation(file, namehint='') -> list[_AnimationFrame]\nLoad an animation (GIF/WEBP) from a file (or file-like object) as a list of frames."
#define DOC_IMAGE_SAVE "save(surface, file, namehint='') -> None\nSave an image to file (or file-like object)."
#define DOC_IMAGE_GETSDLIMAGEVERSION "get_sdl_image_version(linked=True) -> Optional[tuple[int, int, int]]\nGet version number of the SDL_Image library being used."
#define DOC_IMAGE_GETEXTENDED "get_extended() -> bool\nTest if extended image formats can be loaded."
Expand Down
32 changes: 27 additions & 5 deletions src_c/imageext.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
static SDL_mutex *_pg_img_mutex = 0;
#endif
*/
static PyTypeObject *animation_frame_type = NULL;

static char *
iext_find_extension(char *fullname)
Expand Down Expand Up @@ -254,22 +255,29 @@ imageext_load_animation(PyObject *self, PyObject *arg, PyObject *kwargs)
}

for (int i = 0; i < surfs->count; i++) {
PyObject *delay_obj = PyFloat_FromDouble((double)surfs->delays[i]);
if (!delay_obj) {
goto error;
}
PyObject *frame = (PyObject *)pgSurface_New(surfs->frames[i]);
if (!frame) {
/* IMG_FreeAnimation takes care of freeing of member SDL surfaces
*/
Py_DECREF(delay_obj);
goto error;
}
/* The python surface object now "owns" the sdl surface, so set it
* to null in the animation to prevent double free */
surfs->frames[i] = NULL;

PyObject *listentry =
Py_BuildValue("(Od)", frame, (double)surfs->delays[i]);
Py_DECREF(frame);
PyObject *listentry = PyStructSequence_New(animation_frame_type);
if (!listentry) {
Py_DECREF(frame);
Py_DECREF(delay_obj);
goto error;
}

PyStructSequence_SetItem(listentry, 0, frame);
PyStructSequence_SetItem(listentry, 1, delay_obj);
PyList_SET_ITEM(ret, i, listentry);
}
IMG_FreeAnimation(surfs);
Expand Down Expand Up @@ -438,6 +446,16 @@ static PyMethodDef _imageext_methods[] = {
/*DOC*/ static char _imageext_doc[] =
/*DOC*/ "additional image loaders";

static PyStructSequence_Field _namedtuple_fields[] = {
{"surface", NULL}, {"delay_ms", NULL}, {NULL, NULL}};

static struct PyStructSequence_Desc _namedtuple_descr = {
.name = "_AnimationFrame",
.doc = NULL,
.fields = _namedtuple_fields,
.n_in_sequence = 2,
};

MODINIT_DEFINE(imageext)
{
static struct PyModuleDef _module = {PyModuleDef_HEAD_INIT,
Expand All @@ -462,7 +480,6 @@ MODINIT_DEFINE(imageext)
return NULL;
}
import_pygame_rwobject();

if (PyErr_Occurred()) {
return NULL;
}
Expand All @@ -477,6 +494,11 @@ MODINIT_DEFINE(imageext)
#endif
*/

animation_frame_type = PyStructSequence_NewType(&_namedtuple_descr);
if (animation_frame_type == NULL) {
return NULL; // exception set
}

/* create the module */
return PyModule_Create(&_module);
}
3 changes: 3 additions & 0 deletions test/image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,9 @@ def test_load_animation(self):
for val in s:
self.assertIsInstance(val, tuple)
frame, delay = val
frameattr, delayattr = val.surface, val.delay_ms
self.assertEqual(frame, frameattr)
self.assertEqual(delay, delayattr)
self.assertIsInstance(frame, pygame.Surface)
self.assertEqual(frame.size, SAMPLE_SIZE)
self.assertIsInstance(delay, float)
Expand Down
Loading