Skip to content

Commit cc1d5bf

Browse files
authored
Implement pygame.image.load_sized_svg (#2620)
1 parent 020f6d0 commit cc1d5bf

File tree

6 files changed

+177
-1
lines changed

6 files changed

+177
-1
lines changed

buildconfig/stubs/pygame/image.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import Optional, Tuple, Union
33
from pygame.bufferproxy import BufferProxy
44
from pygame.surface import Surface
55

6-
from ._common import FileArg, Literal, IntCoordinate
6+
from ._common import FileArg, Literal, IntCoordinate, Coordinate
77

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

1515
def load(file: FileArg, namehint: str = "") -> Surface: ...
16+
def load_sized_svg(file: FileArg, size: Coordinate) -> Surface: ...
1617
def save(surface: Surface, file: FileArg, namehint: str = "") -> None: ...
1718
def get_sdl_image_version(linked: bool = True) -> Optional[Tuple[int, int, int]]: ...
1819
def get_extended() -> bool: ...

docs/reST/ref/image.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ following formats.
109109

110110
.. ## pygame.image.load ##
111111
112+
.. function:: load_sized_svg
113+
114+
| :sl:`load an SVG image from a file (or file-like object) with the given size`
115+
| :sg:`load_sized_svg(file, size) -> Surface`
116+
117+
This function rasterizes the input SVG at the size specified by the ``size``
118+
argument. The ``file`` argument can be either a filename, a Python file-like
119+
object, or a pathlib.Path.
120+
121+
The usage of this function for handling SVGs is recommended, as calling the
122+
regular ``load`` function and then scaling the returned surface would not
123+
preserve the quality that an SVG can provide.
124+
125+
It is to be noted that this function does not return a surface whose
126+
dimensions exactly match the ``size`` argument. This function preserves
127+
aspect ratio, so the returned surface could be smaller along at most one
128+
dimension.
129+
130+
This function requires SDL_image 2.6.0 or above. If pygame was compiled with
131+
an older version, ``pygame.error`` will be raised when this function is
132+
called.
133+
134+
.. versionadded:: 2.4.0
135+
136+
.. ## pygame.image.load_sized_svg ##
137+
112138
.. function:: save
113139

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

src_c/doc/image_doc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Auto generated file: with makeref.py . Docs go in docs/reST/ref/ . */
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)"
4+
#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"
45
#define DOC_IMAGE_SAVE "save(Surface, file) -> None\nsave(Surface, file, namehint="") -> None\nsave an image to file (or file-like object)"
56
#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"
67
#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
@@ -48,6 +48,7 @@ SaveTGA_RW(SDL_Surface *surface, SDL_RWops *out, int rle);
4848
static PyObject *extloadobj = NULL;
4949
static PyObject *extsaveobj = NULL;
5050
static PyObject *extverobj = NULL;
51+
static PyObject *ext_load_sized_svg = NULL;
5152

5253
static const char *
5354
find_extension(const char *fullname)
@@ -1653,12 +1654,25 @@ SaveTGA(SDL_Surface *surface, const char *file, int rle)
16531654
return ret;
16541655
}
16551656

1657+
static PyObject *
1658+
image_load_sized_svg(PyObject *self, PyObject *args, PyObject *kwargs)
1659+
{
1660+
if (ext_load_sized_svg) {
1661+
return PyObject_Call(ext_load_sized_svg, args, kwargs);
1662+
}
1663+
1664+
return RAISE(PyExc_NotImplementedError,
1665+
"Support for sized svg image loading was not compiled in.");
1666+
}
1667+
16561668
static PyMethodDef _image_methods[] = {
16571669
{"load_basic", (PyCFunction)image_load_basic, METH_O, DOC_IMAGE_LOADBASIC},
16581670
{"load_extended", (PyCFunction)image_load_extended,
16591671
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADEXTENDED},
16601672
{"load", (PyCFunction)image_load, METH_VARARGS | METH_KEYWORDS,
16611673
DOC_IMAGE_LOAD},
1674+
{"load_sized_svg", (PyCFunction)image_load_sized_svg,
1675+
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_LOADSIZEDSVG},
16621676

16631677
{"save_extended", (PyCFunction)image_save_extended,
16641678
METH_VARARGS | METH_KEYWORDS, DOC_IMAGE_SAVEEXTENDED},
@@ -1734,6 +1748,11 @@ MODINIT_DEFINE(image)
17341748
if (!extverobj) {
17351749
goto error;
17361750
}
1751+
ext_load_sized_svg =
1752+
PyObject_GetAttrString(extmodule, "_load_sized_svg");
1753+
if (!ext_load_sized_svg) {
1754+
goto error;
1755+
}
17371756
Py_DECREF(extmodule);
17381757
}
17391758
else {
@@ -1746,6 +1765,7 @@ MODINIT_DEFINE(image)
17461765
Py_XDECREF(extloadobj);
17471766
Py_XDECREF(extsaveobj);
17481767
Py_XDECREF(extverobj);
1768+
Py_XDECREF(ext_load_sized_svg);
17491769
Py_DECREF(extmodule);
17501770
Py_DECREF(module);
17511771
return NULL;

src_c/imageext.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,53 @@ image_load_ext(PyObject *self, PyObject *arg, PyObject *kwarg)
137137
return final;
138138
}
139139

140+
static PyObject *
141+
imageext_load_sized_svg(PyObject *self, PyObject *arg, PyObject *kwargs)
142+
{
143+
#if SDL_IMAGE_VERSION_ATLEAST(2, 6, 0)
144+
PyObject *obj, *size, *final;
145+
SDL_Surface *surf;
146+
SDL_RWops *rw = NULL;
147+
int width, height;
148+
static char *kwds[] = {"file", "size", NULL};
149+
150+
if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "OO", kwds, &obj, &size)) {
151+
return NULL;
152+
}
153+
154+
if (!pg_TwoIntsFromObj(size, &width, &height)) {
155+
return RAISE(PyExc_TypeError, "size must be two numbers");
156+
}
157+
158+
if (width <= 0 || height <= 0) {
159+
return RAISE(PyExc_ValueError,
160+
"both components of size must be be positive");
161+
}
162+
163+
rw = pgRWops_FromObject(obj, NULL);
164+
if (rw == NULL) {
165+
return NULL;
166+
}
167+
168+
Py_BEGIN_ALLOW_THREADS;
169+
surf = IMG_LoadSizedSVG_RW(rw, width, height);
170+
SDL_RWclose(rw);
171+
Py_END_ALLOW_THREADS;
172+
if (surf == NULL) {
173+
return RAISE(pgExc_SDLError, IMG_GetError());
174+
}
175+
final = (PyObject *)pgSurface_New(surf);
176+
if (final == NULL) {
177+
SDL_FreeSurface(surf);
178+
}
179+
return final;
180+
#else /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
181+
return RAISE(
182+
pgExc_SDLError,
183+
"pygame must be compiled with SDL_image 2.6.0+ to use this function");
184+
#endif /* ~SDL_IMAGE_VERSION_ATLEAST(2, 6, 0) */
185+
}
186+
140187
static PyObject *
141188
image_save_ext(PyObject *self, PyObject *arg, PyObject *kwarg)
142189
{
@@ -265,6 +312,8 @@ static PyMethodDef _imageext_methods[] = {
265312
METH_VARARGS | METH_KEYWORDS,
266313
"_get_sdl_image_version() -> (major, minor, patch)\n"
267314
"Note: Should not be used directly."},
315+
{"_load_sized_svg", (PyCFunction)imageext_load_sized_svg,
316+
METH_VARARGS | METH_KEYWORDS, "Note: Should not be used directly."},
268317
{NULL, NULL, 0, NULL}};
269318

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

test/image_test.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,85 @@ def test_load_extended(self):
17861786
surf = pygame.image.load_extended(example_path("data/" + filename))
17871787
self.assertEqual(surf.get_at((0, 0)), expected_color)
17881788

1789+
@unittest.skipIf(
1790+
pygame.image.get_sdl_image_version() < (2, 6, 0),
1791+
"load_sized_svg requires SDL_image 2.6.0+",
1792+
)
1793+
def test_load_sized_svg(self):
1794+
EXPECTED_COLOR = (0, 128, 128, 255)
1795+
EXPECTED_ASPECT_RATIO = pygame.Vector2(1, 1).normalize()
1796+
for size in (
1797+
(10, 10),
1798+
[100, 100],
1799+
pygame.Vector2(1, 1),
1800+
[4, 2],
1801+
(1000, 30),
1802+
):
1803+
with self.subTest(
1804+
f"Test loading SVG with size",
1805+
size=size,
1806+
):
1807+
# test with both keywords and no keywords
1808+
surf = pygame.image.load_sized_svg(example_path("data/teal.svg"), size)
1809+
surf_kw = pygame.image.load_sized_svg(
1810+
file=example_path("data/teal.svg"), size=size
1811+
)
1812+
self.assertEqual(surf.get_at((0, 0)), EXPECTED_COLOR)
1813+
self.assertEqual(surf_kw.get_at((0, 0)), EXPECTED_COLOR)
1814+
ret_size = surf.get_size()
1815+
self.assertEqual(surf_kw.get_size(), ret_size)
1816+
1817+
# test the returned surface exactly fits the size box
1818+
self.assertTrue(
1819+
(ret_size[0] == size[0] and ret_size[1] <= size[1])
1820+
or (ret_size[1] == size[1] and ret_size[0] <= size[0]),
1821+
f"{ret_size = } must exactly fit {size = }",
1822+
)
1823+
1824+
# test that aspect ratio is maintained
1825+
self.assertEqual(
1826+
pygame.Vector2(ret_size).normalize(), EXPECTED_ASPECT_RATIO
1827+
)
1828+
1829+
@unittest.skipIf(
1830+
pygame.image.get_sdl_image_version() < (2, 6, 0),
1831+
"load_sized_svg requires SDL_image 2.6.0+",
1832+
)
1833+
def test_load_sized_svg_erroring(self):
1834+
for type_error_size in (
1835+
[100],
1836+
(0, 0, 10, 3),
1837+
"foo",
1838+
pygame.Vector3(-3, 10, 10),
1839+
):
1840+
with self.subTest(
1841+
f"Test TypeError",
1842+
type_error_size=type_error_size,
1843+
):
1844+
self.assertRaises(
1845+
TypeError,
1846+
pygame.image.load_sized_svg,
1847+
example_path("data/teal.svg"),
1848+
type_error_size,
1849+
)
1850+
1851+
for value_error_size in (
1852+
[100, 0],
1853+
(0, 0),
1854+
[-2, -1],
1855+
pygame.Vector2(-3, 10),
1856+
):
1857+
with self.subTest(
1858+
f"Test ValueError",
1859+
value_error_size=value_error_size,
1860+
):
1861+
self.assertRaises(
1862+
ValueError,
1863+
pygame.image.load_sized_svg,
1864+
example_path("data/teal.svg"),
1865+
value_error_size,
1866+
)
1867+
17891868
def test_load_pathlib(self):
17901869
"""works loading using a Path argument."""
17911870
path = pathlib.Path(example_path("data/asprite.bmp"))

0 commit comments

Comments
 (0)