openpencil/packages/docs/programmable/cli/exporting.md
Danila Poyarkov 8d10b9ca27
fix: export layers and pages that are not on screen in app mode (#877)
* fix(automation): export layers from a page that is not on screen

The app's raster export rendered against the page on screen unless the
caller passed a page, so MCP export_image with ids on any other page, or
with page_id naming another page, failed with "Raster export selection
must stay on a single page". Automation shares one app between clients,
so the page on screen says nothing about what a request means.

Render on the page that holds the requested layers instead. The user's
view and selection stay where they were.

* fix(cli): export the requested page from the running app

`openpencil export --page` never reached the app: `exportViaApp` only
forwarded `--document-id` and `--page-id`, and the app's `export` RPC
exported the given nodes or the selection on screen, ignoring the target
page. `--page` and `--page-id` therefore exported whatever was selected.

The CLI now resolves `--page` to a page ID through `list_documents` and
asks for a page-scoped export. The app answers a page-scoped export with
the layers of the target page, loading a `.fig` page that has not been
shown yet without switching to it.

CLI tests address the package source by `#cli/`, as Core and fig tests
already do, so the alias owner widens to the whole package.

* fix(automation): prepare fonts and layout for a page exported off screen

A page export loaded the layers of a page that had not been shown, but not
its fonts or layout, so text and auto layout could render differently from
the screen. preparePageNodes runs the same font and layout pass as a page
switch, once per page, without switching or superseding a switch.

The CLI export test now writes its own discovery file, so it no longer
replaces or removes the record of an app that is running.

* fix(automation): prepare a .fig page before running a tool on it

A `.fig` opens with only its first page populated; the others get their
layers, fonts and layout when first shown. The automation tool handler built
its FigmaAPI on the target page without loading it, so MCP tools aimed at a
page nobody had opened (`page_id`) saw an empty page: find_nodes found
nothing, export_image reported "No visible nodes to export", and create_shape
added a shape to a page that then held only that shape.

Prepare the target page first with preparePageNodes, as page exports do:
layers, fonts and layout, once per page. The page on screen does not change.

* fix(automation): render explicit export IDs on the page that holds them

Since the visual diff tools, the automation FigmaAPI passes its target page
with every raster export, so export_image with IDs from another page asked to
render them on the target page and failed with "Raster export selection must
stay on a single page". The page now names which layers to export only when no
IDs are given; an ID list is rendered on its own page.

* fix(core): share one off-screen page preparation between concurrent callers

Two concurrent preparePageNodes calls for the same page both populated it
and resolved its fonts, and the font manager's blocked-node set has no
reference count, so the first to finish unblocked text the second was still
resolving. Callers now share the in-flight preparation, which is kept once
it succeeds and retried after a failure.

preparePageNodes also reports whether the page is ready, so a caller can
refuse to run on a page whose document was closed or replaced mid-way
instead of acting on a page with no layers. Its unused options are gone:
one caller's signal cannot cancel a shared preparation.

* fix(automation): prepare the target page once for every command

Preparing an unshown .fig page lived in the page export handler, so explicit
export IDs, export_jsx, eval, tools, and the RPC fallback still saw such a
page as empty. The request dispatcher now prepares the resolved target page
before any page-targeted command, and stops with an error when the page's
document closed while it loaded.

* docs(changelog): fold the off-screen page fixes into one entry

* refactor(automation): rely on the dispatcher to prepare a tool's target page

The request dispatcher now prepares the target page before every
page-targeted command, so the tool handler no longer does it itself. The
tests run tools through the dispatcher, which is where that guarantee lives.

* docs(changelog): drop the tool entry now covered by the off-screen page fix

---------

Co-authored-by: Jason Woltje <1139190+jetrich@users.noreply.github.com>
2026-10-04 13:55:23 +00:00

133 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Exporting
description: Export document content to images, PDF, PowerPoint, `.fig`, JSX, HTML, or Storybook stories, with font-substitution policies for raster and PDF output.
---
# Exporting
Export designs from the terminal — raster images, vectors, PDF, editable PowerPoint, `.fig` subsets, JSX code, HTML, or Storybook stories.
## Image Export
```sh
openpencil export design.fig # PNG (default)
openpencil export design.fig -f jpg -s 2 -q 90 # JPG at 2×, quality 90
openpencil export design.fig -f webp -s 3 # WEBP at 3×
openpencil export design.fig -f svg # SVG vector
openpencil export design.fig -f pdf # PDF
openpencil export design.fig -f pptx # editable PowerPoint
openpencil export design.fig -f fig --page "Page 1" # export one page as .fig
openpencil export design.fig -f fig --node 1:23 # export one node as .fig
openpencil export design.fig -f html --css tailwind # export an HTML fragment with Tailwind classes
```
Options:
- `-f` — format: `png`, `jpg`, `webp`, `svg`, `pdf`, `pptx`, `jsx`, `html`, `fig`, `storybook`
- `-s` — export scale (default: `1`)
- `-q` — quality: `0`–`100` (JPG/WEBP only)
- `-o` — output path
- `--page` — page name
- `--node` — specific node ID
## Font Substitution Policy
For file-backed PNG, JPG, WEBP, and PDF exports, choose how missing or substituted fonts are handled:
```sh
openpencil export design.fig -f pdf --font-policy strict
openpencil export design.fig -f png --font-policy warn
openpencil export design.fig -f webp --font-policy allow
```
- `warn` (default) — report substitutions and continue exporting.
- `strict` — stop with a nonzero exit status if font preparation cannot faithfully resolve the requested faces.
- `allow` — export without the additional font-fidelity check.
Run [`openpencil fonts`](./inspecting#font-diagnostics) first to inspect affected faces. Export preparation may use configured font providers, so its result can differ from the offline file diagnostic.
This policy does not apply to exports from the running app, or to SVG, PowerPoint, JSX, HTML, and `.fig` output. It checks font resolution, not complete visual equivalence with Figma.
## JSX Export
Export as JSX with Tailwind utility classes:
```sh
openpencil export design.fig -f tailwind-jsx # or: -f jsx --style tailwind
```
Output:
```html
<div className="flex flex-col gap-4 p-6 bg-white rounded-xl">
<p className="text-2xl font-bold text-[#1D1B20]">Card Title</p>
<p className="text-sm text-[#49454F]">Description text</p>
</div>
```
Also supports `--style openpencil` for the native JSX format (see [JSX Renderer](../jsx-renderer)).
## HTML Export
Export as an HTML fragment with inline styles by default, or Tailwind utility classes:
```sh
openpencil export design.fig -f html
openpencil export design.fig -f html --css tailwind
```
Use `--html standalone` for a browser-openable HTML document with reset styles and a page wrapper. Standalone HTML is intended as a useful visual/code handoff, not a pixel-perfect renderer replacement:
```sh
openpencil export design.fig -f html --html standalone --css inline
openpencil export design.fig -f html --html standalone --css tailwind
openpencil export design.fig -f html --html standalone --css tailwind --assets external
```
Standalone Tailwind output is compiled during export; it does not depend on the Tailwind browser runtime. Use `--assets external` to write CSS and extracted image assets next to the HTML file. Use `--fonts assets` with external assets to resolve detected SceneGraph text fonts through OpenPencil's configured web-font providers and emit local `@font-face` files.
HTML export is available in file mode.
## Storybook Export
Generate one CSF3 `.stories.ts` file per component set or component:
```sh
openpencil export design.fig -f storybook # React stories in ./design-stories/
openpencil export design.fig -f storybook --framework vue -o src/stories
openpencil export design.fig -f storybook --framework html --page "Components"
openpencil export design.pen -f storybook -o src/stories --watch # re-export on every save
openpencil export 'src/**/*.pen' -f storybook --beside --watch # stories next to each design
```
### Designs next to their stories
Keep each component's design file in the component's folder and export with `--beside`: each document's stories, design images, and `.openpencil-stories.json` manifest go into that document's own folder, next to the component's code. A Storybook `stories` glob such as `../src/**/*.stories.ts` in `.storybook/main.ts` then picks them up without further configuration.
Pass several documents, or a quoted glob such as `'src/**/*.pen'` that OpenPencil expands itself (Node.js 22 or later). Several documents need `--beside` or `--output`; `--page` works with one document only. Documents are exported one after another, and when one fails the rest are still exported before the command exits with an error. `--watch` watches every matched document; a document created after the watch started needs another run.
Each variant of a component set becomes a story, and its variant properties become `select` controls, so switching a control shows the matching variant. Standalone components named with slashes, such as `Button/Primary` and `Button/Secondary`, are grouped into one `Button` file with a `Variant` control. A combination the design has no variant for throws a named error in Storybook rather than showing a different variant.
Stories render the component as HTML with inline styles, like `-f html`, so they need no OpenPencil runtime; `--framework` (`react`, `vue`, or `html`) only changes the wrapper and the `Meta`/`StoryObj` import from `@storybook/react-vite`, `@storybook/vue3-vite`, or `@storybook/html-vite`. Text uses the document's font families, which Storybook has to load itself. Text, boolean, and instance-swap properties are not exported yet.
Stories carry `parameters.design` entries for [`@storybook/addon-designs`](https://github.com/storybookjs/addon-designs):
- **OpenPencil** — when the document path is inside the current directory, an [`openpencil://` link](../index#url-scheme) that opens the document in the desktop app and selects the variant, or its component set when another layer shares the variant's name. The scheme addresses layers by name, so a story whose variant and component names are both shared by other layers gets no link.
- **Design** — a 2× PNG of the variant, written to `<Name>.design/` next to the story and referenced with `new URL(…, import.meta.url)`, so Vite bundles it. Copy the parameter onto the story of your own component to compare the implementation with the design. `--no-design-images` skips rendering; font substitution follows `--font-policy` as in raster export.
`-o` names the output directory. A `.openpencil-stories.json` manifest there records which document, as a path relative to the output directory, and which page generated each story and design image; commit it with the stories. An export replaces the files that a previous export of the same document generated there, including those of components since deleted or renamed; with `--page`, only that page's. It refuses, before changing anything, to overwrite any other file — a hand-written story, another document's, or a stray image. A one-page export whose file names shifted onto another page's stories asks for a full export instead. Without the manifest, existing stories count as someone else's, so remove them before exporting again. `--watch` keeps the command running and re-exports whenever the document is saved, so Storybook's hot reload follows the design; a save that cannot be read is reported and the watch continues. A missing `--page` or a `--font-policy strict` substitution still ends the command.
## Live App Mode
Omit the file to export from the running app:
```sh
openpencil export -f png # export the selection in the active document
openpencil export --page "Components" -f png # export every layer of a page
openpencil export --node 1:23 -f png # export one layer, on any page
```
`--page` takes a page name and `--page-id` a page ID from `openpencil documents`; either exports that page without switching the app to it. Add `--document-id` to export from a document other than the active one.
Live app mode supports PNG, JPG, WEBP, SVG, and PDF. PowerPoint, JSX, HTML, Storybook, and `.fig` exports require a file argument. File-mode thumbnail export is not currently supported.