Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
51b071a
checkpoint(CR-0001): CR authored for exporting slides as PNG from the…
desek May 30, 2026
0194cd5
checkpoint(CR-0001): drop html-to-image; switch to hand-rolled foreig…
desek May 30, 2026
e9b6deb
checkpoint(CR-0001): CR reviewed: drift reconciled, contradictions re…
desek May 30, 2026
0937115
checkpoint(CR-0001): phase 1 — locale keys + export-png skeleton
desek May 30, 2026
2d86266
checkpoint(CR-0001): phase 2 — single-page PNG rasterisation
desek May 30, 2026
73f8a4f
checkpoint(CR-0001): phase 3 — full-deck PNG ZIP export
desek May 30, 2026
642e31d
checkpoint(CR-0001): phase 4 — PNG progress toast and dropdown entries
desek May 30, 2026
14496d0
checkpoint(CR-0001): phase 5 — tests, changeset, polish
desek May 30, 2026
e3a9a95
checkpoint(CR-0001): CR finalized
desek May 30, 2026
ba413f8
checkpoint(CR-0001): validation report completed
desek May 30, 2026
87a65dd
checkpoint(CR-0001): gaps fixed per validation report
desek May 30, 2026
9e99e1a
checkpoint(CR-0001): documentation updated for implemented feature
desek May 30, 2026
e68c064
fix(core): make PNG export render faithfully at 1920x1080
desek May 30, 2026
0920a2b
test(core): use explicit .ts import extensions in PNG/download tests
desek May 30, 2026
4c23214
checkpoint(CR-0002): CR authored for programmatic CLI PNG export via …
desek May 30, 2026
f358411
checkpoint(CR-0002): resolve four open questions in draft CR
desek May 30, 2026
3ebe121
checkpoint(CR-0002): CR reviewed: drift reconciled, contradictions re…
desek May 30, 2026
3e2c991
checkpoint(CR-0002): resolve enumeration mechanism via new GET /__sli…
desek May 30, 2026
ad40747
checkpoint(CR-0002): phase 1 — CLI subcommand skeleton + Playwright d…
desek May 30, 2026
4a5b131
checkpoint(CR-0002): phase 2 — viewer-side readiness signal
desek May 30, 2026
e7e6342
checkpoint(CR-0002): phase 3: Headless render loop
desek May 30, 2026
c52135b
checkpoint(CR-0002): phase 4 — polish, tests, changeset
desek May 30, 2026
6294553
fix(core): mark playwright-chromium external in tsdown build
desek May 30, 2026
02001d5
checkpoint(CR-0002): CR finalized
desek May 30, 2026
121d0f9
checkpoint(CR-0002): gaps fixed per validation report
desek May 30, 2026
7326fdf
checkpoint(CR-0002): documentation updated for implemented feature
desek May 30, 2026
3228ed3
fix(core): render bare full-bleed slide for headless PNG export
desek May 30, 2026
c15eacf
fix(core): invoke @playwright/test cli directly in test:e2e
desek Jul 28, 2026
e062070
fix(core): freeze intro animations before rasterising PNG exports
desek Jul 28, 2026
5e07293
docs(core): document PNG export as the agent visual-verification loop
desek Jul 28, 2026
0a94961
docs(cr): reconcile CR-0001 and CR-0002 with the shipped implementation
desek Jul 28, 2026
97ada08
docs(core): make code docstrings self-sufficient
desek Jul 28, 2026
d10841c
refactor(core): reuse shared helpers in the image PPTX exporter
desek Jul 28, 2026
c845070
fix(core): inline pseudo-element background images in PNG export
desek Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cli-export-png.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-slide/core": minor
---

Add `open-slide export` CLI subcommand for headless PNG export. `playwright-chromium` is a devDependency only, so end-user installs are unaffected; the subcommand preflights for it and prints copy-pasteable install instructions when absent.
5 changes: 5 additions & 0 deletions .changeset/export-slides-as-png.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-slide/core": minor
---

Add "Export as PNG" entry to the viewer download menu.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a user-facing present-tense changeset description.

-Add "Export as PNG" entry to the viewer download menu.
+Adds PNG export options to the viewer download menu.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Add "Export as PNG" entry to the viewer download menu.
Adds PNG export options to the viewer download menu.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/export-slides-as-png.md at line 5, Update the changeset
description to use clear, user-facing present-tense wording while preserving the
“Export as PNG” viewer download menu behavior.

Source: Coding guidelines

5 changes: 5 additions & 0 deletions .changeset/png-visual-verification-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-slide/core": patch
---

Document PNG export as the agent's visual-verification loop; the slide-authoring skill now exports and inspects PNGs during self-review.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ Manage images, videos, and fonts per deck through a built-in assets panel. Searc

Fullscreen playback with keyboard navigation, plus a **presenter mode** with current/next slide preview, speaker notes, and a timer. Built for the stage, not just the browser tab.

### 📦 Export to static HTML & PDF
### 📦 Export to static HTML, PDF & PNG

One command exports your deck as a self-contained static HTML site or a print-ready PDF. Share without a server.
One command exports your deck as a self-contained static HTML site, a print-ready PDF, or 1920×1080 PNGs (current slide or a ZIP of every page). For CI or batch pipelines, `open-slide export` renders the same PNGs headlessly via Playwright. Share without a server.

PNG export is also how the agent writing your slides **checks its own work**: a 1920×1080 image is readable by vision-capable models, so clipping, overflow, distorted aspect ratios, and collisions get caught by looking at the slide instead of guessing at it.

### 📁 Slide manager

Expand All @@ -77,7 +79,7 @@ This repo is a pnpm + Turbo monorepo.

| Path | Description |
| --- | --- |
| [packages/core](packages/core) | `@open-slide/core` — runtime (home page, slide viewer, present mode, inspector), Vite plugin, and the `open-slide` dev/build/preview CLI. |
| [packages/core](packages/core) | `@open-slide/core` — runtime (home page, slide viewer, present mode, inspector), Vite plugin, and the `open-slide` dev/build/preview/export CLI. |
| [packages/cli](packages/cli) | `@open-slide/cli` — `npx @open-slide/cli init` scaffolder. Generates a minimal workspace where Vite/React/tsconfig stay hidden inside core. |
| [apps/demo](apps/demo) | Example workspace that consumes `@open-slide/core` via `workspace:*`. Used for local development of the framework. |

Expand Down
130 changes: 130 additions & 0 deletions apps/web/content/docs/cli/export.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
title: open-slide export
description: Render every page of every deck to PNG via headless Chromium.
---

```npm
open-slide export --all
# or
open-slide export --slide intro --page 2
```

Boots an in-process Vite dev server on `127.0.0.1`, launches headless
Chromium via [Playwright](https://playwright.dev/), navigates the real
viewer route per page, waits for the same readiness signals the in-viewer
exporter uses (fonts, `data-waitfor`, animation settle), and writes one
1920×1080 PNG per page to `--out`.

This is the pixel-perfect, scriptable counterpart to the in-viewer PNG
export ([Export](/docs/core-feature/export)): the browser's own compositor
paints, so advanced CSS (filters, blend modes, `backdrop-filter`) renders
faithfully, and there is no human in the loop.

## Why: giving agents eyes

The primary motivation is **visual verification by an agent**. A model
writing slides is otherwise working blind — it can reason about a layout
but never see it, so it predicts the vertical budget with arithmetic and
hopes. A 1920×1080 PNG closes that loop: frontier models with
high-resolution image understanding can read these files directly and
check the deck the way a human reviewer would.

That catches the defect classes static analysis cannot:

- **Clipping** — content pushed past the 1080px bottom edge, silently cropped.
- **Overflow** — text escaping its card, column, or safe area.
- **Aspect-ratio distortion** — images stretched or squashed by a bad `object-fit`.
- **Collision and overlap** — a heading landing on a figure, a caption on a chart.
- **Legibility** — type too small, or contrast too low, to read at projector distance.

The workflow is: author the deck, export the pages, look at them, fix what
the images reveal, re-export. The bundled `slide-authoring` skill builds
this into its self-review checklist, so agents following it verify their
own output instead of declaring success unseen.

Because each file is exactly 1920×1080 — the canvas as the audience sees
it — what the model inspects is what gets presented, not an approximation.

The same properties make it suitable for CI thumbnail regeneration, visual
regression diffing, and batch pipelines.

## Prerequisite: install Playwright

`playwright-chromium` is **not** a runtime dependency of `@open-slide/core`
— end-user installs stay lean. Install it on demand in the workspace that
runs the export:

```bash
pnpm add -D playwright-chromium
npx playwright install chromium
```

If Playwright is missing, `open-slide export` preflight-fails before
booting anything, exits with code `2`, and prints both commands above as a
single-paragraph, copy-pasteable hint. On Linux CI images you may also need
`npx playwright install-deps chromium` for the Chromium shared libraries.

## Flags

| Flag | Default | Description |
| --------------------- | -------------- | --------------------------------------------------------------------------- |
| `--slide <id>` | — | Restrict to a single deck (the `slideId` that appears in `/s/:slideId`). |
| `--all` | off | Export every discoverable deck. Mutually exclusive with `--slide`. |
| `--page <n>` | — | Export a single 1-based page index. Requires `--slide`. |
| `--out <dir>` | `./png-export` | Destination directory (created if missing). |
| `--port <port>` | OS-assigned | Pin the in-process dev server's port instead of an ephemeral one. |
| `--timeout <ms>` | `15000` | Per-page readiness timeout. On timeout the page is captured anyway. |

Either `--slide` or `--all` is required. `--page` without `--slide` is a
usage error. Misuse exits with code `2`.

## Output

Files are written atomically (write to `<file>.tmp`, then rename) into
`--out` as:

```text
{slideId}-p{N}.png
```

`N` is the 1-based page index, zero-padded to the total page count's width
— so a 9-page deck yields `slide-p1.png`…`slide-p9.png` (width 1) and a
100-page deck yields `slide-p001.png`…`slide-p100.png` (width 3). This
matches the in-viewer PNG export's filename convention.

Each file is exactly 1920×1080, enforced by both Playwright's viewport
size and a `clip` on `page.screenshot()`.

## Console output

One greppable line per page, plus a summary on success:

```text
intro:p1 → png-export/intro-p1.png
intro:p2 → png-export/intro-p2.png
outro:p1 → png-export/outro-p1.png
Exported 3 page(s) from 2 deck(s) to png-export
```

If a page's readiness signal does not resolve within `--timeout`, the CLI
logs a single warning naming the slide and page, captures the current
frame anyway, and continues.

## Exit codes

| Code | Meaning |
| ---- | ------------------------------------------------------------------------- |
| `0` | Success. |
| `1` | Unrecoverable runtime error (dev server failed, Chromium crashed, etc.). |
| `2` | Usage / preflight error (missing Playwright, bad flags, unknown `--slide`). |

The presenter route (`/s/:slideId/presenter`) is not captured — only the
viewer surface. JPEG, WebP, PDF, custom resolutions, and parallel
rendering are not supported.

## In-viewer vs. headless

| Path | Where | Renderer | Use when |
| ------------------------------------- | ------------------- | --------------------------------------- | ------------------------------------------------------- |
| Toolbar **Export → … as PNG** | Browser (live deck) | Client-side `<foreignObject>` rasterise | Interactive: an author wants a PNG of what's on screen. |
| `open-slide export` | Headless Chromium | `page.screenshot()` via Playwright | CI, batch, or any slide whose CSS strains the rasterizer. |
2 changes: 1 addition & 1 deletion apps/web/content/docs/cli/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "CLI",
"pages": ["overview", "init", "dev", "build", "preview", "sync-skills"]
"pages": ["overview", "init", "dev", "build", "preview", "export", "sync-skills"]
}
5 changes: 3 additions & 2 deletions apps/web/content/docs/cli/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ open-slide ships two CLIs:

- **`@open-slide/cli`** — the scaffolder. One command (`init`) to bootstrap
a new workspace.
- **`@open-slide/core`** — the runtime CLI. `dev`, `build`, `preview`, and
`sync:skills` for an existing workspace.
- **`@open-slide/core`** — the runtime CLI. `dev`, `build`, `preview`,
`export`, and `sync:skills` for an existing workspace.

After `init`, `package.json` exposes the runtime CLI under standard scripts:

Expand All @@ -18,6 +18,7 @@ After `init`, `package.json` exposes the runtime CLI under standard scripts:
"dev": "open-slide dev",
"build": "open-slide build",
"preview": "open-slide preview",
"export": "open-slide export",
"sync:skills": "open-slide sync:skills"
}
}
Expand Down
53 changes: 51 additions & 2 deletions apps/web/content/docs/core-feature/export.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Export
description: One command to a self-contained static HTML site or PDF.
description: One command to a self-contained static HTML site, PDF, or PNG.
---

`open-slide build` produces a plain static site under `dist/` — everything
Expand All @@ -24,9 +24,58 @@ The **Export** menu in the slide toolbar exports the active deck directly:
assets download as a zip (HTML file + assets folder).
- **PDF** — one 1920 × 1080 landscape PDF page per deck page. Not supported
in Safari.
- **PNG** — the current page as a single 1920 × 1080 PNG, or the whole deck as
a ZIP of PNGs. See [PNG](#png) below.
- **PPTX (images)** — each page rendered as an image on a PowerPoint slide.
Native editable PPTX export is not available yet.

## PNG

PNG export exists primarily to **let an agent see the slide**. A model
authoring a deck is otherwise blind to its own output — it can compute a
layout but never look at it. A 1920×1080 PNG is readable by frontier
models with high-resolution image understanding, which turns "I think this
fits" into "I can see that it fits", and surfaces clipping, overflow,
aspect-ratio distortion, overlapping elements, and unreadable type. Humans
get the same benefit; agents get it in a loop they can act on. See
[open-slide export](/docs/cli/export) for the headless, scriptable path
and the `slide-authoring` skill's self-review step.

From the toolbar pick **Export → Export current slide as PNG** for a single
1920×1080 PNG of the active page, or **Export all slides as PNG** for a ZIP
containing one PNG per page (named `{slideId}-p{N}.png`). Rasterisation runs
entirely client-side — no server, no headless browser, no extra dependency.

A progress toast surfaces the `processing → rasterising → zipping → done`
phases for the full-deck export. On Safari the exporter shows a best-effort
notice before running because Safari's `<foreignObject>` pipeline has
long-standing quirks; if rasterisation fails the standard export-failed
toast appears and any offscreen state is torn down.

Both PNG entries sit alongside the other export formats in the same dropdown
and are gated by `build.allowHtmlDownload`.

### Headless CLI export

For CI thumbnail regeneration, batch pipelines, or slides whose CSS
(filters, blend modes, `backdrop-filter`) strains the client rasterizer,
use the `open-slide export` CLI instead. It boots the dev server
in-process, drives a headless Chromium via Playwright, and writes
1920×1080 PNGs straight to disk:

```bash
pnpm add -D playwright-chromium
npx playwright install chromium

open-slide export --all --out ./png-export
```

`playwright-chromium` is **not** a runtime dependency of `@open-slide/core`
— end-user installs stay lean and the CLI prompts with a copy-pasteable
install hint on first run if Playwright is missing. See
[open-slide export](/docs/cli/export) for the full flag list and exit
codes.

## What gets shipped

The build pipeline only includes:
Expand All @@ -45,7 +94,7 @@ const config: OpenSlideConfig = {
build: {
showSlideBrowser: false, // hide the deck index
showSlideUi: false, // hide on-canvas UI (toolbar etc.)
allowHtmlDownload: false, // hide the export menu (HTML / PDF / PPTX)
allowHtmlDownload: false, // hide the export menu (HTML / PDF / PNG / PPTX)
},
};

Expand Down
5 changes: 3 additions & 2 deletions apps/web/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ npx @open-slide/cli init
the integrated [svgl](https://svgl.app/) catalogue.
- **Professional present mode.** Fullscreen playback, presenter view, speaker
notes, timer.
- **Export to static HTML or PDF.** Self-contained output — share without a
server.
- **Export to static HTML, PDF, or PNG.** Self-contained output — share without a
server. PNGs double as the agent's eyes: 1920×1080 images a vision-capable
model can read back to catch clipping, overflow, and distorted aspect ratios.

## A slide is a file

Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/docs/reference/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ dev server always shows the full UI.
allowHtmlDownload: {
type: 'boolean',
default: 'true',
description: 'Show the export menu (HTML, PDF, image PPTX) in the static build.',
description: 'Show the export menu (HTML, PDF, PNG, image PPTX) in the static build.',
},
}}
/>
Expand Down
Loading