Skip to content

Commit

Permalink
Implement pygame.image.load_sized_svg (#2620)
Browse files Browse the repository at this point in the history
  • Loading branch information
ankith26 authored Dec 25, 2023
1 parent 020f6d0 commit cc1d5bf
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 1 deletion.
3 changes: 2 additions & 1 deletion buildconfig/stubs/pygame/image.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ from typing import Optional, Tuple, Union
from pygame.bufferproxy import BufferProxy
from pygame.surface import Surface

from ._common import FileArg, Literal, IntCoordinate
from ._common import FileArg, Literal, IntCoordinate, Coordinate

_BufferStyle = Union[BufferProxy, bytes, bytearray, memoryview]
_to_string_format = Literal[
Expand All @@ -13,6 +13,7 @@ _from_buffer_format = Literal["P", "RGB", "BGR", "BGRA", "RGBX", "RGBA", "ARGB"]
_from_string_format = Literal["P", "RGB", "RGBX", "RGBA", "ARGB", "BGRA"]

def load(file: FileArg, namehint: str = "") -> Surface: ...
def load_sized_svg(file: FileArg, size: Coordinate) -> Surface: ...
def save(surface: Surface, file: FileArg, namehint: str = "") -> None: ...
def get_sdl_image_version(linked: bool = True) -> Optional[Tuple[int, int, int]]: ...
def get_extended() -> bool: ...
Expand Down
26 changes: 26 additions & 0 deletions docs/reST/ref/image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ following formats.

.. ## pygame.image.load ##
.. function:: load_sized_svg

| :sl:`load an SVG image from a file (or file-like object) with the given size`
| :sg:`load_sized_svg(file, size) -> Surface`
This function rasterizes the input SVG at the size specified by the ``size``
argument. The ``file`` argument can be either a filename, a Python file-like
object, or a pathlib.Path.

The usage of this function for handling SVGs is recommended, as calling the
regular ``load`` function and then scaling the returned surface would not
preserve the quality that an SVG can provide.

It is to be noted that this function does not return a surface whose
dimensions exactly match the ``size`` argument. This function preserves
aspect ratio, so the returned surface could be smaller along at most one
dimension.

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
called.

.. versionadded:: 2.4.0

.. ## pygame.image.load_sized_svg ##
.. function:: save

| :sl:`save an image to file (or file-like object)`
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/image_doc.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* Auto generated file: with makeref.py . Docs go in docs/reST/ref/ . */
#define DOC_IMAGE "pygame module for image transfer"
#define DOC_IMAGE_LOAD "load(file) -> Surface\nload(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_SAVE "save(Surface, file) -> None\nsave(Surface, file, namehint="") -> None\nsave an image to file (or file-like object)"
#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"
#define DOC_IMAGE_GETEXTENDED "get_extended() -> bool\ntest if extended image formats can be loaded"
Expand Down
20 changes: 20 additions & 0 deletions src_c/image.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ SaveTGA_RW(SDL_Surface *surface, SDL_RWops *out, int rle);
static PyObject *extloadobj = NULL;
static PyObject *extsaveobj = NULL;
static PyObject *extverobj = NULL;
static PyObject *ext_load_sized_svg = NULL;

static const char *
find_extension(const char *fullname)
Expand Down Expand Up @@ -1653,12 +1654,25 @@ SaveTGA(SDL_Surface *surface, const char *file, int rle)
return ret;
}

static PyObject *
image_load_sized_svg(PyObject *self, PyObject *args, PyObject *kwargs)
{
if (ext_load_sized_svg) {
return PyObject_Call(ext_load_sized_svg, args, kwargs);
}

return RAISE(PyExc_NotImplementedError,
"Support for sized svg image loading was not compiled in.");
}

static PyMethodDef _image_methods[] = {
{"load_basic", (PyCFunction)image_load_basic, METH_O, DOC_IMAGE_LOADBASIC},
{"load_extended", (PyCFunction)image_load_extended,
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADEXTENDED},
{"load", (PyCFunction)image_load, METH_VARARGS | METH_KEYWORDS,
DOC_IMAGE_LOAD},
{"load_sized_svg", (PyCFunction)image_load_sized_svg,
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADSIZEDSVG},

{"save_extended", (PyCFunction)image_save_extended,
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_SAVEEXTENDED},
Expand Down Expand Up @@ -1734,6 +1748,11 @@ MODINIT_DEFINE(image)
if (!extverobj) {
goto error;
}
ext_load_sized_svg =
PyObject_GetAttrString(extmodule, "_load_sized_svg");
if (!ext_load_sized_svg) {
goto error;
}
Py_DECREF(extmodule);
}
else {
Expand All @@ -1746,6 +1765,7 @@ MODINIT_DEFINE(image)
Py_XDECREF(extloadobj);
Py_XDECREF(extsaveobj);
Py_XDECREF(extverobj);
Py_XDECREF(ext_load_sized_svg);
Py_DECREF(extmodule);
Py_DECREF(module);
return NULL;
Expand Down
49 changes: 49 additions & 0 deletions src_c/imageext.c
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,53 @@ image_load_ext(PyObject *self, PyObject *arg, PyObject *kwarg)
return final;
}

static PyObject *
imageext_load_sized_svg(PyObject *self, PyObject *arg, PyObject *kwargs)
{
#if SDL_IMAGE_VERSION_ATLEAST(2, 6, 0)
PyObject *obj, *size, *final;
SDL_Surface *surf;
SDL_RWops *rw = NULL;
int width, height;
static char *kwds[] = {"file", "size", NULL};

if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "OO", kwds, &obj, &size)) {
return NULL;
}

if (!pg_TwoIntsFromObj(size, &width, &height)) {
return RAISE(PyExc_TypeError, "size must be two numbers");
}

if (width <= 0 || height <= 0) {
return RAISE(PyExc_ValueError,
"both components of size must be be positive");
}

rw = pgRWops_FromObject(obj, NULL);
if (rw == NULL) {
return NULL;
}

Py_BEGIN_ALLOW_THREADS;
surf = IMG_LoadSizedSVG_RW(rw, width, height);
SDL_RWclose(rw);
Py_END_ALLOW_THREADS;
if (surf == NULL) {
return RAISE(pgExc_SDLError, IMG_GetError());
}
final = (PyObject *)pgSurface_New(surf);
if (final == NULL) {
SDL_FreeSurface(surf);
}
return final;
#else /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
return RAISE(
pgExc_SDLError,
"pygame must be compiled with SDL_image 2.6.0+ to use this function");
#endif /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
}

static PyObject *
image_save_ext(PyObject *self, PyObject *arg, PyObject *kwarg)
{
Expand Down Expand Up @@ -265,6 +312,8 @@ static PyMethodDef _imageext_methods[] = {
METH_VARARGS | METH_KEYWORDS,
"_get_sdl_image_version() -> (major, minor, patch)\n"
"Note: Should not be used directly."},
{"_load_sized_svg", (PyCFunction)imageext_load_sized_svg,
METH_VARARGS | METH_KEYWORDS, "Note: Should not be used directly."},
{NULL, NULL, 0, NULL}};

/*DOC*/ static char _imageext_doc[] =
Expand Down
79 changes: 79 additions & 0 deletions test/image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,85 @@ def test_load_extended(self):
surf = pygame.image.load_extended(example_path("data/" + filename))
self.assertEqual(surf.get_at((0, 0)), expected_color)

@unittest.skipIf(
pygame.image.get_sdl_image_version() < (2, 6, 0),
"load_sized_svg requires SDL_image 2.6.0+",
)
def test_load_sized_svg(self):
EXPECTED_COLOR = (0, 128, 128, 255)
EXPECTED_ASPECT_RATIO = pygame.Vector2(1, 1).normalize()
for size in (
(10, 10),
[100, 100],
pygame.Vector2(1, 1),
[4, 2],
(1000, 30),
):
with self.subTest(
f"Test loading SVG with size",
size=size,
):
# test with both keywords and no keywords
surf = pygame.image.load_sized_svg(example_path("data/teal.svg"), size)
surf_kw = pygame.image.load_sized_svg(
file=example_path("data/teal.svg"), size=size
)
self.assertEqual(surf.get_at((0, 0)), EXPECTED_COLOR)
self.assertEqual(surf_kw.get_at((0, 0)), EXPECTED_COLOR)
ret_size = surf.get_size()
self.assertEqual(surf_kw.get_size(), ret_size)

# test the returned surface exactly fits the size box
self.assertTrue(
(ret_size[0] == size[0] and ret_size[1] <= size[1])
or (ret_size[1] == size[1] and ret_size[0] <= size[0]),
f"{ret_size = } must exactly fit {size = }",
)

# test that aspect ratio is maintained
self.assertEqual(
pygame.Vector2(ret_size).normalize(), EXPECTED_ASPECT_RATIO
)

@unittest.skipIf(
pygame.image.get_sdl_image_version() < (2, 6, 0),
"load_sized_svg requires SDL_image 2.6.0+",
)
def test_load_sized_svg_erroring(self):
for type_error_size in (
[100],
(0, 0, 10, 3),
"foo",
pygame.Vector3(-3, 10, 10),
):
with self.subTest(
f"Test TypeError",
type_error_size=type_error_size,
):
self.assertRaises(
TypeError,
pygame.image.load_sized_svg,
example_path("data/teal.svg"),
type_error_size,
)

for value_error_size in (
[100, 0],
(0, 0),
[-2, -1],
pygame.Vector2(-3, 10),
):
with self.subTest(
f"Test ValueError",
value_error_size=value_error_size,
):
self.assertRaises(
ValueError,
pygame.image.load_sized_svg,
example_path("data/teal.svg"),
value_error_size,
)

def test_load_pathlib(self):
"""works loading using a Path argument."""
path = pathlib.Path(example_path("data/asprite.bmp"))
Expand Down

0 comments on commit cc1d5bf

Please sign in to comment.