Skip to content

Commit 0e24a64

Browse files
stn1slvantfuantfubot
authored
feat(export): editable PPTX export with native shapes and text (#2722)
Co-authored-by: Anthony Fu <github@antfu.me> Co-authored-by: Anthony Fu (via agent) <reg-github-bot@antfu.me>
1 parent 8ff3fb3 commit 0e24a64

20 files changed

Lines changed: 5051 additions & 8 deletions

File tree

docs/builtin/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Export slides to PDF (or other format). See <LinkInline link="guide/exporting" /
7070
Options:
7171

7272
- `--output` (`string`, default: use `exportFilename` (see https://sli.dev/custom/#frontmatter-configures) or use `[entry]-export`): path to the output.
73-
- `--format` (`'pdf', 'png', 'pptx', 'md'`, default: `'pdf'`): output format.
73+
- `--format` (`'pdf', 'png', 'pptx', 'pptx-editable', 'md'`, default: `'pdf'`): output format. `pptx` exports each slide as an image; `pptx-editable` rebuilds them as native PowerPoint shapes with selectable text.
7474
- `--timeout` (`number`, default: `30000`): timeout for rendering the print page (see https://playwright.dev/docs/api/class-page#page-goto).
7575
- `--range` (`string`): page ranges to export (example: `'1,4-5,6'`).
7676
- `--dark` (`boolean`, default: `false`): export as dark theme.

docs/guide/exporting.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ Note that all the slides in the PPTX file will be exported as images, so the tex
7272

7373
In this mode, the `--with-clicks` option is enabled by default. To disable it, pass `--with-clicks false`.
7474

75+
### Editable PPTX
76+
77+
If the recipient needs to edit the deck rather than only present it, export it with native shapes instead of pictures:
78+
79+
```bash
80+
$ slidev export --format pptx-editable
81+
```
82+
83+
The slides are measured in the browser and rebuilt as PowerPoint shapes, so text is selectable and editable, boxes can be moved and recolored, and presenter notes are carried over as usual. This does not replace `--format pptx`, which stays the most visually faithful option.
84+
85+
What stays a picture: anything PowerPoint has no equivalent for. That includes SVG (so Mermaid diagrams and icons), `<canvas>`, `<iframe>`, videos, KaTeX formulas, CSS gradients, `filter`, `backdrop-filter`, `mix-blend-mode` and `clip-path`. Only the element concerned becomes a picture, not the whole slide.
86+
87+
If a slide cannot be rebuilt safely, or ends up mostly pictures anyway, it falls back to the same image export used by `--format pptx`, for that slide alone, and the reason is printed.
88+
89+
Worth knowing before you send the file on:
90+
91+
- A `.pptx` names fonts, it does not embed them. The export prints which font families it referenced; recipients need those installed or PowerPoint will substitute.
92+
- PowerPoint does not measure text exactly as a browser does, so a long paragraph may wrap onto a different number of lines.
93+
- Decorations a theme draws with `::before` or `::after` in normal flow are left out, and the export lists them. Code block line numbers are one of these: they come from a CSS counter, which has no text and no box that a computed style can report.
94+
95+
Like `--format pptx`, this exports one slide per click step unless you pass `--with-clicks false`. `--per-slide` is not supported with it.
96+
7597
### PNGs and Markdown
7698

7799
When passing in the `--format png` option, Slidev will export PNG images for each slide instead of a PDF:

packages/slidev/node/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ function exportOptions<T>(args: Argv<T>) {
642642
})
643643
.option('format', {
644644
type: 'string',
645-
choices: ['pdf', 'png', 'pptx', 'md'],
645+
choices: ['pdf', 'png', 'pptx', 'pptx-editable', 'md'],
646646
describe: 'output format',
647647
})
648648
.option('timeout', {

packages/slidev/node/commands/export.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface ExportOptions {
2121
slides: SlideInfo[]
2222
port?: number
2323
base?: string
24-
format?: 'pdf' | 'png' | 'pptx' | 'md'
24+
format?: 'pdf' | 'png' | 'pptx' | 'pptx-editable' | 'md'
2525
output?: string
2626
timeout?: number
2727
wait?: number
@@ -221,6 +221,9 @@ export async function exportSlides({
221221
const buffers = await genPagePng(false)
222222
await genPagePptx(buffers)
223223
}
224+
else if (format === 'pptx-editable') {
225+
await genPagePptxEditable()
226+
}
224227
else {
225228
throw new Error(`[slidev] Unsupported exporting format "${format}"`)
226229
}
@@ -544,6 +547,26 @@ export async function exportSlides({
544547
await fs.writeFile(output, buffer)
545548
}
546549

550+
// Native shapes and editable text, rather than one picture per slide. The
551+
// walker, normalizer and builder live in ./pptx, so this is a delegation.
552+
async function genPagePptxEditable() {
553+
if (perSlide) {
554+
// Per-slide mode never renders the print page the measurement depends
555+
// on. Failing beats writing a deck missing every slide but the first.
556+
throw new Error('[slidev] `--per-slide` is not supported with `--format pptx-editable`')
557+
}
558+
559+
const { exportPptxEditable, reportEditableExport } = await import('./pptx')
560+
const result = await exportPptxEditable({ page, slides, width, height, pages, go }, output)
561+
// So the "exported to ..." line names the file that was actually written.
562+
output = result.output
563+
564+
// The progress bar repaints on a timer with the cursor hidden, so anything
565+
// written while it runs is interleaved with it or overwritten.
566+
progress.stop()
567+
reportEditableExport(result)
568+
}
569+
547570
// Adds metadata (title, author, keywords) to PDF document, mutating it
548571
function addPdfMetadata(pdf: PDFDocument): void {
549572
const titleSlide = slides[0]
@@ -612,7 +635,7 @@ export function getExportOptions(args: ExportArgs, options: ResolvedSlidevOption
612635
slides: options.data.slides,
613636
total: options.data.slides.length,
614637
range,
615-
format: (format || 'pdf') as 'pdf' | 'png' | 'pptx' | 'md',
638+
format: (format || 'pdf') as 'pdf' | 'png' | 'pptx' | 'pptx-editable' | 'md',
616639
timeout: timeout ?? 30000,
617640
wait: wait ?? 0,
618641
waitUntil: waitUntil === 'none' ? undefined : (waitUntil ?? 'networkidle') as 'networkidle' | 'load' | 'domcontentloaded',
@@ -621,7 +644,9 @@ export function getExportOptions(args: ExportArgs, options: ResolvedSlidevOption
621644
routerMode: options.data.config.routerMode === 'memory' ? 'history' : options.data.config.routerMode,
622645
width: options.data.config.canvasWidth,
623646
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
624-
withClicks: withClicks ?? format === 'pptx',
647+
// Both pptx formats default to one slide per click step. Testing the
648+
// exact string here silently collapsed click steps for the editable one.
649+
withClicks: withClicks ?? !!format?.startsWith('pptx'),
625650
executablePath,
626651
withToc: withToc || false,
627652
perSlide: perSlide || false,

0 commit comments

Comments
 (0)