diff --git a/buildconfig/stubs/pygame/image.pyi b/buildconfig/stubs/pygame/image.pyi index 01591a823f..eb8c36bb17 100644 --- a/buildconfig/stubs/pygame/image.pyi +++ b/buildconfig/stubs/pygame/image.pyi @@ -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 ( @@ -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). @@ -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 @@ -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 diff --git a/src_c/doc/image_doc.h b/src_c/doc/image_doc.h index 9830b39291..db44b28223 100644 --- a/src_c/doc/image_doc.h +++ b/src_c/doc/image_doc.h @@ -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." diff --git a/src_c/imageext.c b/src_c/imageext.c index b13a6859c7..36d96eb53c 100644 --- a/src_c/imageext.c +++ b/src_c/imageext.c @@ -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) @@ -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); @@ -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, @@ -462,7 +480,6 @@ MODINIT_DEFINE(imageext) return NULL; } import_pygame_rwobject(); - if (PyErr_Occurred()) { return NULL; } @@ -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); } diff --git a/test/image_test.py b/test/image_test.py index eab5cc06fc..9d6d9ece42 100644 --- a/test/image_test.py +++ b/test/image_test.py @@ -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)