prezo 2026.1.2__py3-none-any.whl → 2026.1.4__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 +20 -1
- prezo/app.py +295 -43
- prezo/config.py +2 -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 +360 -53
- prezo/parser.py +29 -0
- prezo/widgets/status_bar.py +20 -2
- {prezo-2026.1.2.dist-info → prezo-2026.1.4.dist-info}/METADATA +1 -1
- {prezo-2026.1.2.dist-info → prezo-2026.1.4.dist-info}/RECORD +16 -11
- prezo/export.py +0 -860
- {prezo-2026.1.2.dist-info → prezo-2026.1.4.dist-info}/WHEEL +0 -0
- {prezo-2026.1.2.dist-info → prezo-2026.1.4.dist-info}/entry_points.txt +0 -0
prezo/export/html.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""HTML export functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from prezo.parser import clean_marp_directives, extract_notes, parse_presentation
|
|
8
|
+
from prezo.themes import get_theme
|
|
9
|
+
|
|
10
|
+
from .common import EXIT_FAILURE, EXIT_SUCCESS, ExportError
|
|
11
|
+
|
|
12
|
+
# HTML export templates
|
|
13
|
+
HTML_TEMPLATE = """\
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>{title}</title>
|
|
20
|
+
<style>
|
|
21
|
+
* {{
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}}
|
|
26
|
+
body {{
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
28
|
+
background: {background};
|
|
29
|
+
color: {text};
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
}}
|
|
32
|
+
.slides {{
|
|
33
|
+
max-width: 1200px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 2rem;
|
|
36
|
+
}}
|
|
37
|
+
.slide {{
|
|
38
|
+
background: {surface};
|
|
39
|
+
border: 1px solid {border};
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 3rem 4rem;
|
|
42
|
+
margin-bottom: 3rem;
|
|
43
|
+
min-height: 70vh;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
page-break-after: always;
|
|
47
|
+
}}
|
|
48
|
+
.slide-number {{
|
|
49
|
+
color: {text_muted};
|
|
50
|
+
font-size: 0.9rem;
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
|
+
padding-bottom: 0.5rem;
|
|
53
|
+
border-bottom: 1px solid {border};
|
|
54
|
+
}}
|
|
55
|
+
.slide-content {{
|
|
56
|
+
flex: 1;
|
|
57
|
+
}}
|
|
58
|
+
h1 {{
|
|
59
|
+
font-size: 2.5rem;
|
|
60
|
+
margin-bottom: 1.5rem;
|
|
61
|
+
color: {primary};
|
|
62
|
+
}}
|
|
63
|
+
h2 {{
|
|
64
|
+
font-size: 2rem;
|
|
65
|
+
margin-bottom: 1.2rem;
|
|
66
|
+
color: {primary};
|
|
67
|
+
}}
|
|
68
|
+
h3 {{
|
|
69
|
+
font-size: 1.5rem;
|
|
70
|
+
margin-bottom: 1rem;
|
|
71
|
+
color: {text};
|
|
72
|
+
}}
|
|
73
|
+
p {{
|
|
74
|
+
font-size: 1.2rem;
|
|
75
|
+
line-height: 1.6;
|
|
76
|
+
margin-bottom: 1rem;
|
|
77
|
+
}}
|
|
78
|
+
ul, ol {{
|
|
79
|
+
margin: 1rem 0;
|
|
80
|
+
padding-left: 2rem;
|
|
81
|
+
}}
|
|
82
|
+
li {{
|
|
83
|
+
font-size: 1.2rem;
|
|
84
|
+
line-height: 1.6;
|
|
85
|
+
margin-bottom: 0.5rem;
|
|
86
|
+
}}
|
|
87
|
+
pre {{
|
|
88
|
+
background: {background};
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
padding: 1rem;
|
|
91
|
+
overflow-x: auto;
|
|
92
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
93
|
+
font-size: 1rem;
|
|
94
|
+
margin: 1rem 0;
|
|
95
|
+
}}
|
|
96
|
+
code {{
|
|
97
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
98
|
+
background: {background};
|
|
99
|
+
padding: 0.2rem 0.4rem;
|
|
100
|
+
border-radius: 3px;
|
|
101
|
+
font-size: 0.95em;
|
|
102
|
+
}}
|
|
103
|
+
pre code {{
|
|
104
|
+
padding: 0;
|
|
105
|
+
background: none;
|
|
106
|
+
}}
|
|
107
|
+
table {{
|
|
108
|
+
width: 100%;
|
|
109
|
+
border-collapse: collapse;
|
|
110
|
+
margin: 1rem 0;
|
|
111
|
+
}}
|
|
112
|
+
th, td {{
|
|
113
|
+
border: 1px solid {border};
|
|
114
|
+
padding: 0.75rem;
|
|
115
|
+
text-align: left;
|
|
116
|
+
}}
|
|
117
|
+
th {{
|
|
118
|
+
background: {background};
|
|
119
|
+
}}
|
|
120
|
+
blockquote {{
|
|
121
|
+
border-left: 4px solid {primary};
|
|
122
|
+
padding-left: 1rem;
|
|
123
|
+
margin: 1rem 0;
|
|
124
|
+
font-style: italic;
|
|
125
|
+
color: {text_muted};
|
|
126
|
+
}}
|
|
127
|
+
a {{
|
|
128
|
+
color: {primary};
|
|
129
|
+
}}
|
|
130
|
+
img {{
|
|
131
|
+
max-width: 100%;
|
|
132
|
+
height: auto;
|
|
133
|
+
}}
|
|
134
|
+
/* Multi-column layouts */
|
|
135
|
+
.columns {{
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 2rem;
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
}}
|
|
140
|
+
.columns > div {{
|
|
141
|
+
flex: 1;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
}}
|
|
144
|
+
.notes {{
|
|
145
|
+
margin-top: 2rem;
|
|
146
|
+
padding: 1rem;
|
|
147
|
+
background: {background};
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
font-size: 0.9rem;
|
|
150
|
+
color: {text_muted};
|
|
151
|
+
}}
|
|
152
|
+
.notes-title {{
|
|
153
|
+
font-weight: bold;
|
|
154
|
+
margin-bottom: 0.5rem;
|
|
155
|
+
}}
|
|
156
|
+
@media print {{
|
|
157
|
+
.slide {{
|
|
158
|
+
break-inside: avoid;
|
|
159
|
+
page-break-inside: avoid;
|
|
160
|
+
}}
|
|
161
|
+
body {{
|
|
162
|
+
background: white;
|
|
163
|
+
color: black;
|
|
164
|
+
}}
|
|
165
|
+
}}
|
|
166
|
+
</style>
|
|
167
|
+
</head>
|
|
168
|
+
<body>
|
|
169
|
+
<div class="slides">
|
|
170
|
+
{slides}
|
|
171
|
+
</div>
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
SLIDE_TEMPLATE = """\
|
|
177
|
+
<div class="slide" id="slide-{num}">
|
|
178
|
+
<div class="slide-number">Slide {display_num} of {total}</div>
|
|
179
|
+
<div class="slide-content">
|
|
180
|
+
{content}
|
|
181
|
+
</div>
|
|
182
|
+
{notes}
|
|
183
|
+
</div>
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
NOTES_TEMPLATE = """\
|
|
187
|
+
<div class="notes">
|
|
188
|
+
<div class="notes-title">Presenter Notes</div>
|
|
189
|
+
{notes_content}
|
|
190
|
+
</div>
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def render_slide_to_html(content: str) -> str:
|
|
195
|
+
"""Convert markdown content to basic HTML.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
content: Markdown content of the slide.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
HTML string for the slide content.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
import markdown # noqa: PLC0415
|
|
206
|
+
|
|
207
|
+
html = markdown.markdown(
|
|
208
|
+
content,
|
|
209
|
+
extensions=["tables", "fenced_code", "codehilite"],
|
|
210
|
+
)
|
|
211
|
+
except ImportError:
|
|
212
|
+
# Fallback: basic markdown-to-html conversion
|
|
213
|
+
import html as html_mod # noqa: PLC0415
|
|
214
|
+
|
|
215
|
+
html = html_mod.escape(content)
|
|
216
|
+
# Basic transformations
|
|
217
|
+
html = html.replace("\n\n", "</p><p>")
|
|
218
|
+
html = f"<p>{html}</p>"
|
|
219
|
+
|
|
220
|
+
return html
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def export_to_html(
|
|
224
|
+
source: Path,
|
|
225
|
+
output: Path,
|
|
226
|
+
*,
|
|
227
|
+
theme: str = "dark",
|
|
228
|
+
include_notes: bool = False,
|
|
229
|
+
) -> Path:
|
|
230
|
+
"""Export presentation to HTML.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
source: Path to the markdown presentation.
|
|
234
|
+
output: Path for the output HTML file.
|
|
235
|
+
theme: Theme to use for styling.
|
|
236
|
+
include_notes: Whether to include presenter notes.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Path to the created HTML file.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ExportError: If export fails.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
if not source.exists():
|
|
246
|
+
msg = f"Source file not found: {source}"
|
|
247
|
+
raise ExportError(msg)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
presentation = parse_presentation(source)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
msg = f"Failed to parse presentation: {e}"
|
|
253
|
+
raise ExportError(msg) from e
|
|
254
|
+
|
|
255
|
+
if presentation.total_slides == 0:
|
|
256
|
+
msg = "Presentation has no slides"
|
|
257
|
+
raise ExportError(msg)
|
|
258
|
+
|
|
259
|
+
theme_obj = get_theme(theme)
|
|
260
|
+
|
|
261
|
+
# Render each slide
|
|
262
|
+
slides_html = []
|
|
263
|
+
for i, slide in enumerate(presentation.slides):
|
|
264
|
+
# Use raw_content and clean with keep_divs=True to preserve column layouts
|
|
265
|
+
slide_content, _ = extract_notes(slide.raw_content)
|
|
266
|
+
cleaned_content = clean_marp_directives(slide_content, keep_divs=True)
|
|
267
|
+
content_html = render_slide_to_html(cleaned_content)
|
|
268
|
+
|
|
269
|
+
# Handle notes
|
|
270
|
+
notes_html = ""
|
|
271
|
+
if include_notes and slide.notes:
|
|
272
|
+
notes_content = render_slide_to_html(slide.notes)
|
|
273
|
+
notes_html = NOTES_TEMPLATE.format(notes_content=notes_content)
|
|
274
|
+
|
|
275
|
+
slide_html = SLIDE_TEMPLATE.format(
|
|
276
|
+
num=i,
|
|
277
|
+
display_num=i + 1,
|
|
278
|
+
total=presentation.total_slides,
|
|
279
|
+
content=content_html,
|
|
280
|
+
notes=notes_html,
|
|
281
|
+
)
|
|
282
|
+
slides_html.append(slide_html)
|
|
283
|
+
|
|
284
|
+
# Build final HTML
|
|
285
|
+
title = presentation.title or source.stem
|
|
286
|
+
html = HTML_TEMPLATE.format(
|
|
287
|
+
title=title,
|
|
288
|
+
background=theme_obj.background,
|
|
289
|
+
surface=theme_obj.surface,
|
|
290
|
+
text=theme_obj.text,
|
|
291
|
+
text_muted=theme_obj.text_muted,
|
|
292
|
+
primary=theme_obj.primary,
|
|
293
|
+
border=theme_obj.text_muted,
|
|
294
|
+
slides="\n".join(slides_html),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
output.write_text(html)
|
|
299
|
+
return output
|
|
300
|
+
except Exception as e:
|
|
301
|
+
msg = f"Failed to write HTML: {e}"
|
|
302
|
+
raise ExportError(msg) from e
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_html_export(
|
|
306
|
+
source: str,
|
|
307
|
+
output: str | None = None,
|
|
308
|
+
*,
|
|
309
|
+
theme: str = "light",
|
|
310
|
+
include_notes: bool = False,
|
|
311
|
+
) -> int:
|
|
312
|
+
"""Run HTML export from command line.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
source: Path to the markdown presentation (string).
|
|
316
|
+
output: Optional path for the output HTML (string).
|
|
317
|
+
theme: Theme to use for styling.
|
|
318
|
+
include_notes: Whether to include presenter notes.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Exit code (0 for success).
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
import sys # noqa: PLC0415
|
|
325
|
+
|
|
326
|
+
source_path = Path(source)
|
|
327
|
+
output_path = Path(output) if output else source_path.with_suffix(".html")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
result_path = export_to_html(
|
|
331
|
+
source_path,
|
|
332
|
+
output_path,
|
|
333
|
+
theme=theme,
|
|
334
|
+
include_notes=include_notes,
|
|
335
|
+
)
|
|
336
|
+
print(f"Exported to {result_path}")
|
|
337
|
+
return EXIT_SUCCESS
|
|
338
|
+
except ExportError as e:
|
|
339
|
+
print(f"error: {e}", file=sys.stderr)
|
|
340
|
+
return EXIT_FAILURE
|
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
|