prezo 2026.1.2__py3-none-any.whl → 2026.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prezo/export/images.py ADDED
@@ -0,0 +1,261 @@
1
+ """Image (PNG/SVG) export functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from prezo.parser import parse_presentation
9
+
10
+ from .common import (
11
+ EXIT_FAILURE,
12
+ EXIT_SUCCESS,
13
+ ExportError,
14
+ check_font_availability,
15
+ print_font_warnings,
16
+ )
17
+ from .svg import render_slide_to_svg
18
+
19
+
20
+ def export_slide_to_image(
21
+ content: str,
22
+ slide_num: int,
23
+ total_slides: int,
24
+ output_path: Path,
25
+ *,
26
+ output_format: str = "png",
27
+ theme_name: str = "dark",
28
+ width: int = 80,
29
+ height: int = 24,
30
+ chrome: bool = True,
31
+ scale: float = 1.0,
32
+ ) -> Path:
33
+ """Export a single slide to PNG or SVG.
34
+
35
+ Args:
36
+ content: The markdown content of the slide.
37
+ slide_num: Current slide number (0-indexed).
38
+ total_slides: Total number of slides.
39
+ output_path: Path to save the image.
40
+ output_format: Output format ('png' or 'svg').
41
+ theme_name: Theme to use for rendering.
42
+ width: Console width in characters.
43
+ height: Console height in lines.
44
+ chrome: If True, include window decorations.
45
+ scale: Scale factor for PNG output (e.g., 2.0 for 2x resolution).
46
+
47
+ Returns:
48
+ Path to the created image file.
49
+
50
+ Raises:
51
+ ExportError: If export fails.
52
+
53
+ """
54
+ # Generate SVG
55
+ svg_content = render_slide_to_svg(
56
+ content,
57
+ slide_num,
58
+ total_slides,
59
+ theme_name=theme_name,
60
+ width=width,
61
+ height=height,
62
+ chrome=chrome,
63
+ )
64
+
65
+ if output_format == "svg":
66
+ try:
67
+ output_path.write_text(svg_content)
68
+ return output_path
69
+ except Exception as e:
70
+ msg = f"Failed to write SVG: {e}"
71
+ raise ExportError(msg) from e
72
+
73
+ # Convert SVG to PNG
74
+ try:
75
+ import cairosvg # noqa: PLC0415
76
+ except ImportError as e:
77
+ msg = "PNG export requires cairosvg.\nInstall with: pip install prezo[export]"
78
+ raise ExportError(msg) from e
79
+
80
+ try:
81
+ png_data = cairosvg.svg2png(
82
+ bytestring=svg_content.encode("utf-8"),
83
+ scale=scale,
84
+ )
85
+ if png_data is None:
86
+ msg = "PNG conversion returned no data"
87
+ raise ExportError(msg)
88
+ output_path.write_bytes(png_data)
89
+ return output_path
90
+ except ExportError:
91
+ raise
92
+ except Exception as e:
93
+ msg = f"Failed to convert to PNG: {e}"
94
+ raise ExportError(msg) from e
95
+
96
+
97
+ def export_to_images(
98
+ source: Path,
99
+ output: Path | None = None,
100
+ *,
101
+ output_format: str = "png",
102
+ theme: str = "dark",
103
+ width: int = 80,
104
+ height: int = 24,
105
+ chrome: bool = True,
106
+ slide_num: int | None = None,
107
+ scale: float = 2.0,
108
+ ) -> list[Path]:
109
+ """Export presentation slides to images.
110
+
111
+ Args:
112
+ source: Path to the markdown presentation.
113
+ output: Output path (file for single slide, directory for all).
114
+ output_format: Output format ('png' or 'svg').
115
+ theme: Theme to use for rendering.
116
+ width: Console width in characters.
117
+ height: Console height in lines.
118
+ chrome: If True, include window decorations.
119
+ slide_num: If set, export only this slide (1-indexed).
120
+ scale: Scale factor for PNG output (default 2.0 for higher resolution).
121
+
122
+ Returns:
123
+ List of paths to the created image files.
124
+
125
+ Raises:
126
+ ExportError: If export fails.
127
+
128
+ """
129
+ # Parse presentation
130
+ try:
131
+ presentation = parse_presentation(source)
132
+ except Exception as e:
133
+ msg = f"Failed to read {source}: {e}"
134
+ raise ExportError(msg) from e
135
+
136
+ if presentation.total_slides == 0:
137
+ msg = "No slides found in presentation"
138
+ raise ExportError(msg)
139
+
140
+ # Check font availability and warn if needed (for PNG export)
141
+ if output_format == "png":
142
+ font_warnings = check_font_availability()
143
+ print_font_warnings(font_warnings)
144
+
145
+ # Single slide export
146
+ if slide_num is not None:
147
+ if slide_num < 1 or slide_num > presentation.total_slides:
148
+ msg = (
149
+ f"Invalid slide number: {slide_num}. "
150
+ f"Presentation has {presentation.total_slides} slides."
151
+ )
152
+ raise ExportError(msg)
153
+
154
+ slide_idx = slide_num - 1
155
+ slide = presentation.slides[slide_idx]
156
+
157
+ out_path = Path(output) if output else source.with_suffix(f".{output_format}")
158
+
159
+ result_path = export_slide_to_image(
160
+ slide.content,
161
+ slide_idx,
162
+ presentation.total_slides,
163
+ out_path,
164
+ output_format=output_format,
165
+ theme_name=theme,
166
+ width=width,
167
+ height=height,
168
+ chrome=chrome,
169
+ scale=scale,
170
+ )
171
+ return [result_path]
172
+
173
+ # Export all slides
174
+ if output:
175
+ out_dir = Path(output)
176
+ if out_dir.suffix: # Has extension, treat as file prefix
177
+ prefix = out_dir.stem # Get stem before reassigning
178
+ out_dir = out_dir.parent
179
+ else:
180
+ prefix = source.stem
181
+ else:
182
+ out_dir = source.parent
183
+ prefix = source.stem
184
+
185
+ # Create output directory if needed
186
+ out_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ exported_paths = []
189
+ for i, slide in enumerate(presentation.slides):
190
+ out_path = out_dir / f"{prefix}_{i + 1:03d}.{output_format}"
191
+ result_path = export_slide_to_image(
192
+ slide.content,
193
+ i,
194
+ presentation.total_slides,
195
+ out_path,
196
+ output_format=output_format,
197
+ theme_name=theme,
198
+ width=width,
199
+ height=height,
200
+ chrome=chrome,
201
+ scale=scale,
202
+ )
203
+ exported_paths.append(result_path)
204
+
205
+ return exported_paths
206
+
207
+
208
+ def run_image_export(
209
+ source: str,
210
+ output: str | None = None,
211
+ *,
212
+ output_format: str = "png",
213
+ theme: str = "dark",
214
+ width: int = 80,
215
+ height: int = 24,
216
+ chrome: bool = True,
217
+ slide_num: int | None = None,
218
+ scale: float = 2.0,
219
+ ) -> int:
220
+ """Run PNG/SVG export from command line.
221
+
222
+ Args:
223
+ source: Path to the markdown presentation.
224
+ output: Optional output path (file or directory).
225
+ output_format: Output format ('png' or 'svg').
226
+ theme: Theme to use for rendering.
227
+ width: Console width in characters.
228
+ height: Console height in lines.
229
+ chrome: If True, include window decorations.
230
+ slide_num: If set, export only this slide (1-indexed).
231
+ scale: Scale factor for PNG output (default 2.0 for higher resolution).
232
+
233
+ Returns:
234
+ Exit code (0 for success).
235
+
236
+ """
237
+ source_path = Path(source)
238
+ output_path = Path(output) if output else None
239
+
240
+ try:
241
+ exported_paths = export_to_images(
242
+ source_path,
243
+ output_path,
244
+ output_format=output_format,
245
+ theme=theme,
246
+ width=width,
247
+ height=height,
248
+ chrome=chrome,
249
+ slide_num=slide_num,
250
+ scale=scale,
251
+ )
252
+ if len(exported_paths) == 1:
253
+ print(f"Exported to {exported_paths[0]}")
254
+ else:
255
+ print(
256
+ f"Exported {len(exported_paths)} slides to {exported_paths[0].parent}/"
257
+ )
258
+ return EXIT_SUCCESS
259
+ except ExportError as e:
260
+ print(f"error: {e}", file=sys.stderr)
261
+ return EXIT_FAILURE