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/__init__.py +8 -0
- prezo/app.py +19 -27
- prezo/config.py +1 -1
- prezo/export/__init__.py +36 -0
- prezo/export/common.py +77 -0
- prezo/export/html.py +340 -0
- prezo/export/images.py +261 -0
- prezo/export/pdf.py +497 -0
- prezo/export/svg.py +170 -0
- prezo/layout.py +268 -52
- {prezo-2026.1.2.dist-info → prezo-2026.1.3.dist-info}/METADATA +1 -1
- {prezo-2026.1.2.dist-info → prezo-2026.1.3.dist-info}/RECORD +14 -9
- prezo/export.py +0 -860
- {prezo-2026.1.2.dist-info → prezo-2026.1.3.dist-info}/WHEEL +0 -0
- {prezo-2026.1.2.dist-info → prezo-2026.1.3.dist-info}/entry_points.txt +0 -0
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
|