Skip to content

Commit 5e8da4b

Browse files
tools: lazy-load icon generation dependencies
1 parent 0f00bc4 commit 5e8da4b

1 file changed

Lines changed: 37 additions & 23 deletions

File tree

art/generate-icons.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
- generate-tv-banner.sh
1010
- copy-to-other-apps.sh
1111
12-
Required dependencies:
12+
Required runtime dependencies for image generation:
1313
python -m pip install cairosvg pillow
1414
1515
Optional optimization:
1616
Install an oxipng Python package or make the oxipng CLI available on PATH.
1717
If neither is available, Pillow's built-in PNG optimizer is used.
18+
19+
Commands like --help do not require image-generation dependencies.
1820
"""
1921
from __future__ import annotations
2022

@@ -25,22 +27,7 @@
2527
import sys
2628
from dataclasses import dataclass
2729
from pathlib import Path
28-
from typing import Iterable, Optional
29-
30-
try:
31-
import cairosvg
32-
from PIL import Image
33-
except ImportError as exc:
34-
sys.exit(
35-
f"Missing dependency: {exc}.\n"
36-
"Install required dependencies with:\n"
37-
" python -m pip install cairosvg pillow"
38-
)
39-
40-
try:
41-
import oxipng as oxipng_module # type: ignore[import-not-found]
42-
except ImportError:
43-
oxipng_module = None
30+
from typing import Any, Iterable, Optional
4431

4532

4633
@dataclass(frozen=True)
@@ -79,8 +66,32 @@ def require_dir(path: Path, label: str = "directory") -> Path:
7966
return path
8067

8168

82-
def load_png_from_bytes(png_bytes: bytes) -> Image.Image:
69+
def load_image_dependencies() -> tuple[Any, Any]:
70+
"""Load CairoSVG and Pillow only when image generation is requested."""
71+
try:
72+
import cairosvg
73+
from PIL import Image
74+
except ImportError as exc:
75+
raise RuntimeError(
76+
f"Missing image-generation dependency: {exc}.\n"
77+
"Install required dependencies with:\n"
78+
" python -m pip install cairosvg pillow"
79+
) from exc
80+
81+
return cairosvg, Image
82+
83+
84+
def load_optional_oxipng() -> Any:
85+
try:
86+
import oxipng
87+
except ImportError:
88+
return None
89+
return oxipng
90+
91+
92+
def load_png_from_bytes(png_bytes: bytes) -> Any:
8393
"""Load image eagerly so it does not keep a closed BytesIO handle."""
94+
_, Image = load_image_dependencies()
8495
with io.BytesIO(png_bytes) as buffer:
8596
image = Image.open(buffer)
8697
image.load()
@@ -92,7 +103,7 @@ def render_svg_file(
92103
width: int,
93104
height: int,
94105
background_color: Optional[str] = None,
95-
) -> Image.Image:
106+
) -> Any:
96107
svg_path = require_file(svg_path, "SVG source")
97108
return render_svg_bytes(svg_path.read_bytes(), width, height, background_color)
98109

@@ -102,7 +113,7 @@ def render_svg_text(
102113
width: int,
103114
height: int,
104115
background_color: Optional[str] = None,
105-
) -> Image.Image:
116+
) -> Any:
106117
return render_svg_bytes(svg_text.encode("utf-8"), width, height, background_color)
107118

108119

@@ -111,7 +122,8 @@ def render_svg_bytes(
111122
width: int,
112123
height: int,
113124
background_color: Optional[str] = None,
114-
) -> Image.Image:
125+
) -> Any:
126+
cairosvg, _ = load_image_dependencies()
115127
png_bytes = cairosvg.svg2png(
116128
bytestring=svg_bytes,
117129
output_width=width,
@@ -121,7 +133,7 @@ def render_svg_bytes(
121133
return load_png_from_bytes(png_bytes)
122134

123135

124-
def save_png(image: Image.Image, png_path: Path, optimize: bool = False) -> None:
136+
def save_png(image: Any, png_path: Path, optimize: bool = False) -> None:
125137
png_path = png_path.expanduser()
126138
png_path.parent.mkdir(parents=True, exist_ok=True)
127139
image.save(png_path, "PNG")
@@ -133,9 +145,10 @@ def optimize_png(png_path: Path, level: int = 4) -> None:
133145
"""Losslessly optimize a PNG, preferring oxipng and falling back to Pillow."""
134146
png_path = require_file(png_path, "PNG output")
135147

148+
oxipng_module = load_optional_oxipng()
136149
if oxipng_module is not None and hasattr(oxipng_module, "optimize"):
137150
try:
138-
oxipng_module.optimize(str(png_path), level=level) # type: ignore[attr-defined]
151+
oxipng_module.optimize(str(png_path), level=level)
139152
return
140153
except Exception as exc: # pragma: no cover - optional dependency variance
141154
print(f"warning: oxipng module failed ({exc}); falling back", file=sys.stderr)
@@ -152,6 +165,7 @@ def optimize_png(png_path: Path, level: int = 4) -> None:
152165
except subprocess.CalledProcessError as exc:
153166
print(f"warning: oxipng CLI failed ({exc}); falling back", file=sys.stderr)
154167

168+
_, Image = load_image_dependencies()
155169
with Image.open(png_path) as image:
156170
image.save(png_path, "PNG", optimize=True)
157171

0 commit comments

Comments
 (0)