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
1515Optional 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"""
1921from __future__ import annotations
2022
2527import sys
2628from dataclasses import dataclass
2729from 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