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.py DELETED
@@ -1,860 +0,0 @@
1
- """Export functionality for prezo presentations.
2
-
3
- Exports presentations to PDF and HTML formats, using Rich's console
4
- rendering for PDF and custom HTML templates for web viewing.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import io
10
- import tempfile
11
- from pathlib import Path
12
-
13
- from rich.console import Console
14
- from rich.markdown import Markdown
15
- from rich.panel import Panel
16
- from rich.style import Style
17
- from rich.text import Text
18
-
19
- from .layout import has_layout_blocks, parse_layout, render_layout
20
- from .parser import clean_marp_directives, extract_notes, parse_presentation
21
- from .themes import get_theme
22
-
23
- # Export result types
24
- EXPORT_SUCCESS = 0
25
- EXPORT_FAILED = 2
26
-
27
- # SVG template without window chrome (for printing)
28
- # Uses Rich's template format: {var} for substitution, {{ }} for literal braces
29
- SVG_FORMAT_NO_CHROME = """\
30
- <svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
31
- <!-- Generated with Rich https://www.textualize.io -->
32
- <style>
33
-
34
- @font-face {{
35
- font-family: "Fira Code";
36
- src: local("FiraCode-Regular"),
37
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
38
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
39
- font-style: normal;
40
- font-weight: 400;
41
- }}
42
- @font-face {{
43
- font-family: "Fira Code";
44
- src: local("FiraCode-Bold"),
45
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
46
- url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
47
- font-style: bold;
48
- font-weight: 700;
49
- }}
50
-
51
- .{unique_id}-matrix {{
52
- font-family: Fira Code, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", monospace;
53
- font-size: {char_height}px;
54
- line-height: {line_height}px;
55
- font-variant-east-asian: full-width;
56
- }}
57
-
58
- {styles}
59
- </style>
60
-
61
- <defs>
62
- <clipPath id="{unique_id}-clip-terminal">
63
- <rect x="0" y="0" width="{width}" height="{height}" />
64
- </clipPath>
65
- {lines}
66
- </defs>
67
-
68
- <g transform="translate(0, 0)" clip-path="url(#{unique_id}-clip-terminal)">
69
- {backgrounds}
70
- <g class="{unique_id}-matrix">
71
- {matrix}
72
- </g>
73
- </g>
74
- </svg>
75
- """
76
-
77
-
78
- def render_slide_to_svg(
79
- content: str,
80
- slide_num: int,
81
- total_slides: int,
82
- *,
83
- theme_name: str = "dark",
84
- width: int = 80,
85
- height: int = 24,
86
- chrome: bool = True,
87
- ) -> str:
88
- """Render a single slide to SVG using Rich console.
89
-
90
- Args:
91
- content: The markdown content of the slide
92
- slide_num: Current slide number (0-indexed)
93
- total_slides: Total number of slides
94
- theme_name: Theme to use for rendering
95
- width: Console width in characters
96
- height: Console height in lines
97
- chrome: If True, include window decorations; if False, plain SVG for printing
98
-
99
- Returns:
100
- SVG string of the rendered slide
101
-
102
- """
103
- theme = get_theme(theme_name)
104
-
105
- # Create a console that records output (file=StringIO suppresses terminal output)
106
- console = Console(
107
- width=width,
108
- record=True,
109
- force_terminal=True,
110
- color_system="truecolor",
111
- file=io.StringIO(), # Suppress terminal output
112
- )
113
-
114
- # Base style for the entire slide (background color)
115
- base_style = Style(color=theme.text, bgcolor=theme.background)
116
-
117
- # Render the content (with layout support)
118
- if has_layout_blocks(content):
119
- blocks = parse_layout(content)
120
- slide_content = render_layout(blocks)
121
- else:
122
- slide_content = Markdown(content)
123
-
124
- # Create a panel with the slide content (height - 2 for status bar and padding)
125
- panel_height = height - 2
126
- panel = Panel(
127
- slide_content,
128
- title=f"[{theme.text_muted}]Slide {slide_num + 1}/{total_slides}[/]",
129
- title_align="right",
130
- border_style=Style(color=theme.primary),
131
- style=Style(color=theme.text, bgcolor=theme.surface),
132
- padding=(1, 2),
133
- expand=True,
134
- height=panel_height,
135
- )
136
-
137
- # Print to the recording console with background
138
- console.print(panel, style=base_style)
139
-
140
- # Add status bar at the bottom
141
- progress = (slide_num + 1) / total_slides
142
- bar_width = 20
143
- filled = int(progress * bar_width)
144
- bar = "█" * filled + "░" * (bar_width - filled)
145
- status_text = f" {bar} {slide_num + 1}/{total_slides} "
146
- # Pad status bar to full width
147
- status_text = status_text.ljust(width)
148
- status = Text(status_text, style=Style(bgcolor=theme.primary, color=theme.text))
149
- console.print(status, style=base_style)
150
-
151
- # Export to SVG
152
- if chrome:
153
- svg = console.export_svg(title=f"Slide {slide_num + 1}")
154
- else:
155
- svg = console.export_svg(code_format=SVG_FORMAT_NO_CHROME)
156
-
157
- # Add emoji font fallbacks to font-family declarations
158
- # Rich only specifies "Fira Code, monospace" which lacks emoji glyphs
159
- svg = svg.replace(
160
- "font-family: Fira Code, monospace",
161
- 'font-family: Fira Code, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", monospace',
162
- )
163
-
164
- # Add background color to SVG (Rich doesn't set it by default)
165
- # Insert a rect element right after the opening svg tag
166
- bg_rect = f'<rect width="100%" height="100%" fill="{theme.background}"/>'
167
- return svg.replace(
168
- 'xmlns="http://www.w3.org/2000/svg">',
169
- f'xmlns="http://www.w3.org/2000/svg">\n {bg_rect}',
170
- )
171
-
172
-
173
- def combine_svgs_to_pdf(svg_files: list[Path], output: Path) -> tuple[int, str]:
174
- """Combine multiple SVG files into a single PDF.
175
-
176
- Args:
177
- svg_files: List of paths to SVG files
178
- output: Output PDF path
179
-
180
- Returns:
181
- Tuple of (exit_code, message)
182
-
183
- """
184
- try:
185
- import cairosvg # noqa: PLC0415
186
- from pypdf import PdfReader, PdfWriter # noqa: PLC0415
187
- except ImportError:
188
- return EXPORT_FAILED, (
189
- "Required packages not installed. Install with:\n"
190
- " pip install cairosvg pypdf"
191
- )
192
-
193
- pdf_pages = []
194
-
195
- try:
196
- # Convert each SVG to a PDF page
197
- for svg_file in svg_files:
198
- pdf_bytes = cairosvg.svg2pdf(url=str(svg_file))
199
- assert pdf_bytes is not None
200
- pdf_pages.append(io.BytesIO(pdf_bytes))
201
-
202
- # Combine all pages into one PDF
203
- writer = PdfWriter()
204
- for page_io in pdf_pages:
205
- reader = PdfReader(page_io)
206
- for page in reader.pages:
207
- writer.add_page(page)
208
-
209
- # Write the combined PDF
210
- with open(output, "wb") as f:
211
- writer.write(f)
212
-
213
- return EXPORT_SUCCESS, f"Exported {len(svg_files)} slides to {output}"
214
-
215
- except Exception as e:
216
- return EXPORT_FAILED, f"PDF generation failed: {e}"
217
-
218
-
219
- def export_to_pdf(
220
- source: Path,
221
- output: Path,
222
- *,
223
- theme: str = "dark",
224
- width: int = 80,
225
- height: int = 24,
226
- chrome: bool = True,
227
- ) -> tuple[int, str]:
228
- """Export presentation to PDF matching TUI appearance.
229
-
230
- Args:
231
- source: Path to the markdown presentation
232
- output: Path for the output PDF
233
- theme: Theme to use for rendering
234
- width: Console width in characters
235
- height: Console height in lines
236
- chrome: If True, include window decorations; if False, plain output for printing
237
-
238
- Returns:
239
- Tuple of (exit_code, message)
240
-
241
- """
242
- if not source.exists():
243
- return EXPORT_FAILED, f"Source file not found: {source}"
244
-
245
- # Parse the presentation
246
- try:
247
- presentation = parse_presentation(source)
248
- except Exception as e:
249
- return EXPORT_FAILED, f"Failed to parse presentation: {e}"
250
-
251
- if presentation.total_slides == 0:
252
- return EXPORT_FAILED, "Presentation has no slides"
253
-
254
- # Create temporary directory for SVG files
255
- with tempfile.TemporaryDirectory() as tmpdir_str:
256
- tmpdir = Path(tmpdir_str)
257
- svg_files = []
258
-
259
- # Render each slide to SVG
260
- for i, slide in enumerate(presentation.slides):
261
- svg_content = render_slide_to_svg(
262
- slide.content,
263
- i,
264
- presentation.total_slides,
265
- theme_name=theme,
266
- width=width,
267
- height=height,
268
- chrome=chrome,
269
- )
270
-
271
- svg_file = tmpdir / f"slide_{i:04d}.svg"
272
- svg_file.write_text(svg_content)
273
- svg_files.append(svg_file)
274
-
275
- # Combine into PDF
276
- return combine_svgs_to_pdf(svg_files, output)
277
-
278
-
279
- # HTML export templates
280
- HTML_TEMPLATE = """\
281
- <!DOCTYPE html>
282
- <html lang="en">
283
- <head>
284
- <meta charset="UTF-8">
285
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
286
- <title>{title}</title>
287
- <style>
288
- * {{
289
- margin: 0;
290
- padding: 0;
291
- box-sizing: border-box;
292
- }}
293
- body {{
294
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
295
- background: {background};
296
- color: {text};
297
- min-height: 100vh;
298
- }}
299
- .slides {{
300
- max-width: 1200px;
301
- margin: 0 auto;
302
- padding: 2rem;
303
- }}
304
- .slide {{
305
- background: {surface};
306
- border: 1px solid {border};
307
- border-radius: 8px;
308
- padding: 3rem 4rem;
309
- margin-bottom: 3rem;
310
- min-height: 70vh;
311
- display: flex;
312
- flex-direction: column;
313
- page-break-after: always;
314
- }}
315
- .slide-number {{
316
- color: {text_muted};
317
- font-size: 0.9rem;
318
- margin-bottom: 1rem;
319
- padding-bottom: 0.5rem;
320
- border-bottom: 1px solid {border};
321
- }}
322
- .slide-content {{
323
- flex: 1;
324
- }}
325
- h1 {{
326
- font-size: 2.5rem;
327
- margin-bottom: 1.5rem;
328
- color: {primary};
329
- }}
330
- h2 {{
331
- font-size: 2rem;
332
- margin-bottom: 1.2rem;
333
- color: {primary};
334
- }}
335
- h3 {{
336
- font-size: 1.5rem;
337
- margin-bottom: 1rem;
338
- color: {text};
339
- }}
340
- p {{
341
- font-size: 1.2rem;
342
- line-height: 1.6;
343
- margin-bottom: 1rem;
344
- }}
345
- ul, ol {{
346
- margin: 1rem 0;
347
- padding-left: 2rem;
348
- }}
349
- li {{
350
- font-size: 1.2rem;
351
- line-height: 1.6;
352
- margin-bottom: 0.5rem;
353
- }}
354
- pre {{
355
- background: {background};
356
- border-radius: 4px;
357
- padding: 1rem;
358
- overflow-x: auto;
359
- font-family: 'Fira Code', 'Consolas', monospace;
360
- font-size: 1rem;
361
- margin: 1rem 0;
362
- }}
363
- code {{
364
- font-family: 'Fira Code', 'Consolas', monospace;
365
- background: {background};
366
- padding: 0.2rem 0.4rem;
367
- border-radius: 3px;
368
- font-size: 0.95em;
369
- }}
370
- pre code {{
371
- padding: 0;
372
- background: none;
373
- }}
374
- table {{
375
- width: 100%;
376
- border-collapse: collapse;
377
- margin: 1rem 0;
378
- }}
379
- th, td {{
380
- border: 1px solid {border};
381
- padding: 0.75rem;
382
- text-align: left;
383
- }}
384
- th {{
385
- background: {background};
386
- }}
387
- blockquote {{
388
- border-left: 4px solid {primary};
389
- padding-left: 1rem;
390
- margin: 1rem 0;
391
- font-style: italic;
392
- color: {text_muted};
393
- }}
394
- a {{
395
- color: {primary};
396
- }}
397
- img {{
398
- max-width: 100%;
399
- height: auto;
400
- }}
401
- /* Multi-column layouts */
402
- .columns {{
403
- display: flex;
404
- gap: 2rem;
405
- align-items: flex-start;
406
- }}
407
- .columns > div {{
408
- flex: 1;
409
- min-width: 0;
410
- }}
411
- .notes {{
412
- margin-top: 2rem;
413
- padding: 1rem;
414
- background: {background};
415
- border-radius: 4px;
416
- font-size: 0.9rem;
417
- color: {text_muted};
418
- }}
419
- .notes-title {{
420
- font-weight: bold;
421
- margin-bottom: 0.5rem;
422
- }}
423
- @media print {{
424
- .slide {{
425
- break-inside: avoid;
426
- page-break-inside: avoid;
427
- }}
428
- body {{
429
- background: white;
430
- color: black;
431
- }}
432
- }}
433
- </style>
434
- </head>
435
- <body>
436
- <div class="slides">
437
- {slides}
438
- </div>
439
- </body>
440
- </html>
441
- """
442
-
443
- SLIDE_TEMPLATE = """\
444
- <div class="slide" id="slide-{num}">
445
- <div class="slide-number">Slide {display_num} of {total}</div>
446
- <div class="slide-content">
447
- {content}
448
- </div>
449
- {notes}
450
- </div>
451
- """
452
-
453
- NOTES_TEMPLATE = """\
454
- <div class="notes">
455
- <div class="notes-title">Presenter Notes</div>
456
- {notes_content}
457
- </div>
458
- """
459
-
460
-
461
- def render_slide_to_html(content: str) -> str:
462
- """Convert markdown content to basic HTML.
463
-
464
- Args:
465
- content: Markdown content of the slide.
466
-
467
- Returns:
468
- HTML string for the slide content.
469
-
470
- """
471
- try:
472
- import markdown # noqa: PLC0415
473
-
474
- html = markdown.markdown(
475
- content,
476
- extensions=["tables", "fenced_code", "codehilite"],
477
- )
478
- except ImportError:
479
- # Fallback: basic markdown-to-html conversion
480
- import html as html_mod # noqa: PLC0415
481
-
482
- html = html_mod.escape(content)
483
- # Basic transformations
484
- html = html.replace("\n\n", "</p><p>")
485
- html = f"<p>{html}</p>"
486
-
487
- return html
488
-
489
-
490
- def export_to_html(
491
- source: Path,
492
- output: Path,
493
- *,
494
- theme: str = "dark",
495
- include_notes: bool = False,
496
- ) -> tuple[int, str]:
497
- """Export presentation to HTML.
498
-
499
- Args:
500
- source: Path to the markdown presentation.
501
- output: Path for the output HTML file.
502
- theme: Theme to use for styling.
503
- include_notes: Whether to include presenter notes.
504
-
505
- Returns:
506
- Tuple of (exit_code, message).
507
-
508
- """
509
- if not source.exists():
510
- return EXPORT_FAILED, f"Source file not found: {source}"
511
-
512
- try:
513
- presentation = parse_presentation(source)
514
- except Exception as e:
515
- return EXPORT_FAILED, f"Failed to parse presentation: {e}"
516
-
517
- if presentation.total_slides == 0:
518
- return EXPORT_FAILED, "Presentation has no slides"
519
-
520
- theme_obj = get_theme(theme)
521
-
522
- # Render each slide
523
- slides_html = []
524
- for i, slide in enumerate(presentation.slides):
525
- # Use raw_content and clean with keep_divs=True to preserve column layouts
526
- slide_content, _ = extract_notes(slide.raw_content)
527
- cleaned_content = clean_marp_directives(slide_content, keep_divs=True)
528
- content_html = render_slide_to_html(cleaned_content)
529
-
530
- # Handle notes
531
- notes_html = ""
532
- if include_notes and slide.notes:
533
- notes_content = render_slide_to_html(slide.notes)
534
- notes_html = NOTES_TEMPLATE.format(notes_content=notes_content)
535
-
536
- slide_html = SLIDE_TEMPLATE.format(
537
- num=i,
538
- display_num=i + 1,
539
- total=presentation.total_slides,
540
- content=content_html,
541
- notes=notes_html,
542
- )
543
- slides_html.append(slide_html)
544
-
545
- # Build final HTML
546
- title = presentation.title or source.stem
547
- html = HTML_TEMPLATE.format(
548
- title=title,
549
- background=theme_obj.background,
550
- surface=theme_obj.surface,
551
- text=theme_obj.text,
552
- text_muted=theme_obj.text_muted,
553
- primary=theme_obj.primary,
554
- border=theme_obj.text_muted,
555
- slides="\n".join(slides_html),
556
- )
557
-
558
- try:
559
- output.write_text(html)
560
- return (
561
- EXPORT_SUCCESS,
562
- f"Exported {presentation.total_slides} slides to {output}",
563
- )
564
- except Exception as e:
565
- return EXPORT_FAILED, f"Failed to write HTML: {e}"
566
-
567
-
568
- def run_export(
569
- source: str,
570
- output: str | None = None,
571
- *,
572
- theme: str = "dark",
573
- width: int = 80,
574
- height: int = 24,
575
- chrome: bool = True,
576
- ) -> int:
577
- """Run PDF export from command line.
578
-
579
- Args:
580
- source: Path to the markdown presentation (string)
581
- output: Optional path for the output PDF (string)
582
- theme: Theme to use for rendering
583
- width: Console width in characters
584
- height: Console height in lines
585
- chrome: If True, include window decorations; if False, plain output for printing
586
-
587
- Returns:
588
- Exit code (0 for success)
589
-
590
- """
591
- source_path = Path(source)
592
- output_path = Path(output) if output else source_path.with_suffix(".pdf")
593
-
594
- code, _message = export_to_pdf(
595
- source_path,
596
- output_path,
597
- theme=theme,
598
- width=width,
599
- height=height,
600
- chrome=chrome,
601
- )
602
- if code == EXPORT_SUCCESS:
603
- pass
604
- else:
605
- pass
606
- return code
607
-
608
-
609
- def run_html_export(
610
- source: str,
611
- output: str | None = None,
612
- *,
613
- theme: str = "light",
614
- include_notes: bool = False,
615
- ) -> int:
616
- """Run HTML export from command line.
617
-
618
- Args:
619
- source: Path to the markdown presentation (string).
620
- output: Optional path for the output HTML (string).
621
- theme: Theme to use for styling.
622
- include_notes: Whether to include presenter notes.
623
-
624
- Returns:
625
- Exit code (0 for success).
626
-
627
- """
628
- source_path = Path(source)
629
- output_path = Path(output) if output else source_path.with_suffix(".html")
630
-
631
- code, _message = export_to_html(
632
- source_path,
633
- output_path,
634
- theme=theme,
635
- include_notes=include_notes,
636
- )
637
- return code
638
-
639
-
640
- def export_slide_to_image(
641
- content: str,
642
- slide_num: int,
643
- total_slides: int,
644
- output_path: Path,
645
- *,
646
- output_format: str = "png",
647
- theme_name: str = "dark",
648
- width: int = 80,
649
- height: int = 24,
650
- chrome: bool = True,
651
- scale: float = 1.0,
652
- ) -> tuple[int, str]:
653
- """Export a single slide to PNG or SVG.
654
-
655
- Args:
656
- content: The markdown content of the slide.
657
- slide_num: Current slide number (0-indexed).
658
- total_slides: Total number of slides.
659
- output_path: Path to save the image.
660
- output_format: Output format ('png' or 'svg').
661
- theme_name: Theme to use for rendering.
662
- width: Console width in characters.
663
- height: Console height in lines.
664
- chrome: If True, include window decorations.
665
- scale: Scale factor for PNG output (e.g., 2.0 for 2x resolution).
666
-
667
- Returns:
668
- Tuple of (exit_code, message).
669
-
670
- """
671
- # Generate SVG
672
- svg_content = render_slide_to_svg(
673
- content,
674
- slide_num,
675
- total_slides,
676
- theme_name=theme_name,
677
- width=width,
678
- height=height,
679
- chrome=chrome,
680
- )
681
-
682
- if output_format == "svg":
683
- try:
684
- output_path.write_text(svg_content)
685
- return EXPORT_SUCCESS, f"Exported slide {slide_num + 1} to {output_path}"
686
- except Exception as e:
687
- return EXPORT_FAILED, f"Failed to write SVG: {e}"
688
-
689
- # Convert SVG to PNG
690
- try:
691
- import cairosvg # noqa: PLC0415
692
- except ImportError:
693
- return EXPORT_FAILED, (
694
- "PNG export requires cairosvg.\nInstall with: pip install prezo[export]"
695
- )
696
-
697
- try:
698
- png_data = cairosvg.svg2png(
699
- bytestring=svg_content.encode("utf-8"),
700
- scale=scale,
701
- )
702
- if png_data is None:
703
- return EXPORT_FAILED, "PNG conversion returned no data"
704
- output_path.write_bytes(png_data)
705
- return EXPORT_SUCCESS, f"Exported slide {slide_num + 1} to {output_path}"
706
- except Exception as e:
707
- return EXPORT_FAILED, f"Failed to convert to PNG: {e}"
708
-
709
-
710
- def export_to_images(
711
- source: Path,
712
- output: Path | None = None,
713
- *,
714
- output_format: str = "png",
715
- theme: str = "dark",
716
- width: int = 80,
717
- height: int = 24,
718
- chrome: bool = True,
719
- slide_num: int | None = None,
720
- scale: float = 2.0,
721
- ) -> tuple[int, str]:
722
- """Export presentation slides to images.
723
-
724
- Args:
725
- source: Path to the markdown presentation.
726
- output: Output path (file for single slide, directory for all).
727
- output_format: Output format ('png' or 'svg').
728
- theme: Theme to use for rendering.
729
- width: Console width in characters.
730
- height: Console height in lines.
731
- chrome: If True, include window decorations.
732
- slide_num: If set, export only this slide (1-indexed).
733
- scale: Scale factor for PNG output (default 2.0 for higher resolution).
734
-
735
- Returns:
736
- Tuple of (exit_code, message).
737
-
738
- """
739
- # Parse presentation
740
- try:
741
- presentation = parse_presentation(source)
742
- except Exception as e:
743
- return EXPORT_FAILED, f"Failed to read {source}: {e}"
744
-
745
- if presentation.total_slides == 0:
746
- return EXPORT_FAILED, "No slides found in presentation"
747
-
748
- # Single slide export
749
- if slide_num is not None:
750
- if slide_num < 1 or slide_num > presentation.total_slides:
751
- return EXPORT_FAILED, (
752
- f"Invalid slide number: {slide_num}. "
753
- f"Presentation has {presentation.total_slides} slides."
754
- )
755
-
756
- slide_idx = slide_num - 1
757
- slide = presentation.slides[slide_idx]
758
-
759
- out_path = Path(output) if output else source.with_suffix(f".{output_format}")
760
-
761
- return export_slide_to_image(
762
- slide.content,
763
- slide_idx,
764
- presentation.total_slides,
765
- out_path,
766
- output_format=output_format,
767
- theme_name=theme,
768
- width=width,
769
- height=height,
770
- chrome=chrome,
771
- scale=scale,
772
- )
773
-
774
- # Export all slides
775
- if output:
776
- out_dir = Path(output)
777
- if out_dir.suffix: # Has extension, treat as file prefix
778
- prefix = out_dir.stem # Get stem before reassigning
779
- out_dir = out_dir.parent
780
- else:
781
- prefix = source.stem
782
- else:
783
- out_dir = source.parent
784
- prefix = source.stem
785
-
786
- # Create output directory if needed
787
- out_dir.mkdir(parents=True, exist_ok=True)
788
-
789
- exported = 0
790
- for i, slide in enumerate(presentation.slides):
791
- out_path = out_dir / f"{prefix}_{i + 1:03d}.{output_format}"
792
- code, msg = export_slide_to_image(
793
- slide.content,
794
- i,
795
- presentation.total_slides,
796
- out_path,
797
- output_format=output_format,
798
- theme_name=theme,
799
- width=width,
800
- height=height,
801
- chrome=chrome,
802
- scale=scale,
803
- )
804
- if code != EXPORT_SUCCESS:
805
- return code, msg
806
- exported += 1
807
-
808
- return EXPORT_SUCCESS, f"Exported {exported} slides to {out_dir}/"
809
-
810
-
811
- def run_image_export(
812
- source: str,
813
- output: str | None = None,
814
- *,
815
- output_format: str = "png",
816
- theme: str = "dark",
817
- width: int = 80,
818
- height: int = 24,
819
- chrome: bool = True,
820
- slide_num: int | None = None,
821
- scale: float = 2.0,
822
- ) -> int:
823
- """Run PNG/SVG export from command line.
824
-
825
- Args:
826
- source: Path to the markdown presentation.
827
- output: Optional output path (file or directory).
828
- output_format: Output format ('png' or 'svg').
829
- theme: Theme to use for rendering.
830
- width: Console width in characters.
831
- height: Console height in lines.
832
- chrome: If True, include window decorations.
833
- slide_num: If set, export only this slide (1-indexed).
834
- scale: Scale factor for PNG output (default 2.0 for higher resolution).
835
-
836
- Returns:
837
- Exit code (0 for success).
838
-
839
- """
840
- source_path = Path(source)
841
- output_path = Path(output) if output else None
842
-
843
- code, message = export_to_images(
844
- source_path,
845
- output_path,
846
- output_format=output_format,
847
- theme=theme,
848
- width=width,
849
- height=height,
850
- chrome=chrome,
851
- slide_num=slide_num,
852
- scale=scale,
853
- )
854
-
855
- if code == EXPORT_SUCCESS:
856
- print(message)
857
- else:
858
- print(f"error: {message}", file=__import__("sys").stderr)
859
-
860
- return code