Skip to content

Commit 7671d87

Browse files
committed
Add svg2png image conversion function
1 parent 63bc653 commit 7671d87

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add utility function to compute ZIM Tags #164, including deduplication #156
13+
- Add svg2png image conversion function #113
1314

1415
## Changed
1516
- **BREAKING** Renamed `zimscraperlib.image.convertion` to `zimscraperlib.image.conversion` to fix typo

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies = [
2222
# promise is that they will never (or always) break the API, and the API is very
2323
# limited and we use only a very small subset of it.
2424
"regex>=2020.7.14",
25+
"CairoSVG>=2.2.0,<3.0",
2526
# youtube-dl should be updated as frequently as possible
2627
"yt-dlp"
2728
]

src/zimscraperlib/image/conversion.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
from __future__ import annotations
55

6+
import io
67
import pathlib
78
from typing import IO
89

10+
import cairosvg.svg
911
from PIL.Image import open as pilopen
1012

1113
from zimscraperlib.constants import ALPHA_NOT_SUPPORTED
@@ -40,6 +42,39 @@ def convert_image(
4042
save_image(image, dst, fmt, **params)
4143

4244

45+
def convert_svg2png(
46+
src: str | pathlib.Path | io.BytesIO,
47+
dst: pathlib.Path | IO[bytes],
48+
width: int | None = None,
49+
height: int | None = None,
50+
):
51+
"""Convert a SVG to a PNG
52+
53+
Output width and height might be specified if resize is needed.
54+
PNG background is transparent.
55+
"""
56+
kwargs = {}
57+
if isinstance(src, pathlib.Path):
58+
src = str(src)
59+
if isinstance(src, str):
60+
kwargs["url"] = src
61+
else:
62+
kwargs["bytestring"] = src.getvalue()
63+
if width:
64+
kwargs["output_width"] = width
65+
if height:
66+
kwargs["output_height"] = height
67+
if isinstance(dst, pathlib.Path):
68+
cairosvg.svg2png(write_to=str(dst), **kwargs)
69+
else:
70+
result = cairosvg.svg2png(**kwargs)
71+
if not isinstance(result, bytes):
72+
raise Exception(
73+
"Unexpected type returned by cairosvg.svg2png"
74+
) # pragma: no cover
75+
dst.write(result)
76+
77+
4378
def create_favicon(src: pathlib.Path, dst: pathlib.Path) -> None:
4479
"""generate a squared favicon from a source image"""
4580
if dst.suffix != ".ico":

tests/image/test_image.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
from resizeimage.imageexceptions import ImageSizeError
1818

1919
from zimscraperlib.image import presets
20-
from zimscraperlib.image.conversion import convert_image, create_favicon
20+
from zimscraperlib.image.conversion import (
21+
convert_image,
22+
convert_svg2png,
23+
create_favicon,
24+
)
2125
from zimscraperlib.image.optimization import (
2226
ensure_matches,
2327
get_optimization_method,
@@ -328,6 +332,42 @@ def test_convert_path_src_io_dst(png_image: pathlib.Path):
328332
assert dst_image.format == "PNG"
329333

330334

335+
def test_convert_svg_io_src_path_dst(svg_image: pathlib.Path, tmp_path: pathlib.Path):
336+
src = io.BytesIO(svg_image.read_bytes())
337+
dst = tmp_path / "test.png"
338+
convert_svg2png(src, dst)
339+
dst_image = Image.open(dst)
340+
assert dst_image.format == "PNG"
341+
342+
343+
def test_convert_svg_io_src_io_dst(svg_image: pathlib.Path):
344+
src = io.BytesIO(svg_image.read_bytes())
345+
dst = io.BytesIO()
346+
convert_svg2png(src, dst)
347+
dst_image = Image.open(dst)
348+
assert dst_image.format == "PNG"
349+
350+
351+
def test_convert_svg_path_src_path_dst(svg_image: pathlib.Path, tmp_path: pathlib.Path):
352+
src = svg_image
353+
dst = tmp_path / "test.png"
354+
convert_svg2png(src, dst, width=96, height=96)
355+
dst_image = Image.open(dst)
356+
assert dst_image.format == "PNG"
357+
assert dst_image.width == 96
358+
assert dst_image.height == 96
359+
360+
361+
def test_convert_svg_path_src_io_dst(svg_image: pathlib.Path):
362+
src = svg_image
363+
dst = io.BytesIO()
364+
convert_svg2png(src, dst, width=96, height=96)
365+
dst_image = Image.open(dst)
366+
assert dst_image.format == "PNG"
367+
assert dst_image.width == 96
368+
assert dst_image.height == 96
369+
370+
331371
@pytest.mark.parametrize(
332372
"fmt,exp_size",
333373
[("png", 128), ("jpg", 128)],

0 commit comments

Comments
 (0)