OPF
DocsWalkthrough

Get started with the OPF toolkit

Create and check a first deck, then use the toolkit sections for your integration.

Create a deck and export it

Use the published CLI for a first local run. No account or model call is required.

In a new project directory
npm init -y
npm install --save-dev @openpresentation/cli @openpresentation/opf-render @openpresentation/opf-pptx
npm install --save-dev @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2

Save this complete document as deck.opf.json.

deck.opf.json
{
  "name": "First deck",
  "slides": [
    {
      "title": "Make the next action clear",
      "text": "Keep the message in OPF. Export PowerPoint when you need an Office handoff."
    }
  ]
}
Check and export
npx opf validate deck.opf.json
npx opf convert deck.opf.json deck.pptx

Preview and export share core composition. Native Office text flow and fonts can still differ; keep the OPF source and inspect conversion diagnostics. For a website, editor or Node app, use the runnable starters.

The stack

A slide deck is a JSON file. One core package validates it and computes the geometry of every slide. The renderer, the PowerPoint exporter and the editor share that geometry. Native text flow and font availability still set fidelity limits. Four libraries and a CLI, each its own MIT-licensed repository. This site's tests run every Node snippet on this page against the published packages, and type-check the browser snippets.

How the OPF packages relateA deck.opf.json file flows into the core package, which feeds opf-render, opf-pptx and opf-editor. The CLI bundles core and loads opf-render and opf-pptx as optional peers.deck.opf.jsonplain JSON, in git@openpresentation/opfschema · catalogs · typesvalidate · lint · auditcomposeSlide · paginatepatch · diff · mergemarkdown · templates · dataopf-renderSVG → PNG · vector PDFopf-pptxtoPptx · fromPptxopf-editorsession · undo · canvasoptionalpeercli (bin: opf) bundles core;render + pptx as optional peers

The rule that holds it together: preview and export only agree when they resolve the same core. When core changes geometry, the renderer, PowerPoint exporter and editor raise their minimum core in the same release series. Core 0.12.0 did: font sizes snap to PowerPoint's 0.01 pt grid, a wrapped trailing space hangs, promoted regions compose in reading order and right-to-left decks mirror. Install the libraries as a set, pinned to the versions above.

shell
npm install @openpresentation/opf@0.20.3 @openpresentation/opf-render@0.20.2 @openpresentation/opf-pptx@0.20.3 @openpresentation/gallery@^3.0.0
# PNG and PDF export and exact text measurement use the renderer's optional peers; an SVG-only install needs none of them.
npm install @resvg/resvg-js@^2.6.2 sharp@^0.35.5 pdf-lib@^1.17.1 @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2
npm install @openpresentation/opf-editor@0.20.3
npm install --save-dev @types/node

Run npm ls @openpresentation/opf afterwards. It should list one copy of core; two copies mean two packages resolved different cores, and a preview and an export may disagree. Each package declares the Node versions it supports in engines.node, and Node 24 is the development runtime.

The document

Name files *.opf.json. Only a nonempty slides array is required; name labels the document. Other fields are optional and resolved from catalogs your host registers. The rest of this page works on this file.

deck.opf.json
{
  "name": "Quarterly Review",
  "design": {
    "theme": "minimal",
    "fontScheme": "roboto",
    "footer": { "left": { "text": "Internal" }, "right": { "text": "{{slide.number}} / {{deck.slideCount}}" } }
  },
  "slides": [
    { "id": "title", "section": "Start", "title": "Quarterly Review", "subtitle": "Q3 2026" },
    { "id": "agenda", "title": "Agenda", "items": ["Revenue", "Product", "Hiring"] },
    {
      "id": "rev",
      "title": "Revenue",
      "chart": {
        "type": "column",
        "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] }
      }
    }
  ]
}

Content shapes

A slide carries one payload at its root (text, items, bullets, quote, metric, code, timeline, chart, table, image, video), or several as blocks, nested groups, or promoted regions (left, center, right, top:left, and so on). Text can be rich runs with links, citations (cite) and inline footnotes. Images, charts, tables and video take a caption. Lists can be numbered, per level, with numbering styles such as roman-upper. See promoted regions, implicit blocks and content payloads.

Design resolution

Every visual choice resolves top-down, and the first layer that sets a value wins.

  1. The slide's design, for one slide.
  2. The deck's design, for the whole deck.
  3. The resolved theme, with its color scheme and font scheme, from a catalog.
  4. The selected layout record's design defaults.
  5. Engine defaults.

The design resolution reference has the details.

OPF v0.20.3 uses nested arrays for named grid areas, optional tracks and contentregions. Regions accept content kinds and bind compatible items in reading order; content groups retain their own nested composition. Dynamic layoutsdescribes composition and pagination.

Catalogs

Core ships no catalog records in its main entry. A document references a theme, layout, color scheme, font scheme, narrative, audience, purpose or tone by id or name:id, and the host registers the catalogs those ids come from: validate(deck, { catalogs: [defaultCatalog] }), where defaultCatalog is the catalog exported by pptx.gallery from @openpresentation/gallery. Saving embeds every record a document uses (embed, or opf embed), so the file renders the same offline and with no catalog registered. A reference that resolves nowhere is a warning. An unresolved layout composes automatically; unresolved theme, color or font references use engine defaults.

Validate in code

validate.ts
import { readFile } from "node:fs/promises"
import { assertValid, validate } from "@openpresentation/opf"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import type { Presentation } from "@openpresentation/opf/types"

const deck: unknown = JSON.parse(await readFile("deck.opf.json", "utf8"))

// Report style: never throws, returns every finding. Core ships no catalog records: register the catalogs your
// documents reference (the first is the host default), or only the records a document embeds resolve.
const catalogs = [defaultCatalog]
const { valid, findings } = validate(deck, { only: ["format", "references"], catalogs })
for (const finding of findings) console.error(finding.severity + " " + finding.path + ": " + finding.message)

// Guard style: throws OPFValidationError, then narrows the type.
assertValid(deck)
const typed: Presentation = deck

// A catalog is records keyed by id, grouped by kind.
const scheme = typeof typed.design?.fontScheme === "string" ? defaultCatalog.fontSchemes?.[typed.design.fontScheme] : undefined
console.log(valid, scheme)

Only errors make valid false. An unknown catalog id, such as a misspelled font scheme, is a warning, so check the findings for warnings (or run opf validate --fail-on warning) when a typo should fail your build.

For neutral facts about a deck (slide, word and image counts, notes, alt text, speaking time) with no judgments, call stats(deck) or run opf stats deck.opf.json; see the stats reference.

Subpath exports keep bundles small: /schemas, /validator, /types, /composition, /pagination, /data, /markdown, /yaml, /patch, /diff, /format, /convert, /font-policy, and the raw /spec/* JSON. Catalog records come from @openpresentation/gallery, registered by the host or embedded in the deck.

Validate

One checker, from "is this valid OPF" to "will people be able to read it". It never renders: layout rules run on core's own composition, and every result is a list of findings in one format.

validate

Takes a deck or its JSON text and returns valid, findings, counts and what ran. Each finding has a stable opf/<rule> id, a severity, a category, a JSON Pointer path, and a hint; text input adds source ranges, and a fix is attached where one is safe.

Categories

format and references (schema, semantic rules, catalog and asset ids) and policy (your contracts) can be errors. accessibility, layout and content are warnings and info: WCAG AA contrast against the resolved background, overflow, minimum type size, alt text, reading order, chart series told apart by color alone.

Options

only and ignore take rule ids, bare rule names or categories; severity re-ranks a rule; fonts passes the renderer's measurement. validate(deck, { only: ['format'] }) is the cheap check for a hot path.

Exit codes

CLI reports are JSON. 0 is success, possibly with warnings; 1 is an invalid document or a failed check; 2 is a usage or I/O error. --fail-on warning fails on warnings.
checks.ts
import { readFile } from "node:fs/promises"
import { validate } from "@openpresentation/opf"

const source = await readFile("deck.opf.json", "utf8")

// One checker. Text is read as strict JSON, and its findings carry source ranges (and fixes where one is safe).
const report = validate(source)
console.log(report.valid, report.counts) // { error, warning, info }

// Run only what you need: a rule id, a bare rule name or a category (format, references, policy, accessibility, layout, content).
const accessibility = validate(JSON.parse(source), { only: ["text-contrast", "missing-alt-text"] })
console.log(accessibility.counts)

Compose and paginate

Layout is a pure function in core. Every engine calls it, reducing geometry drift between SVG preview and PPTX export. Native text flow remains viewer-dependent.

compose.ts
import { readFile } from "node:fs/promises"
import { paginate, resolveSlideContext } from "@openpresentation/opf"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import { composeSlide } from "@openpresentation/opf/composition"
import { loadFonts } from "@openpresentation/opf-render/fonts-node"

const deck = JSON.parse(await readFile("deck.opf.json", "utf8"))

const fonts = await loadFonts({ pack: "base" }) // bundled, hash-pinned font files
const catalogs = [defaultCatalog] // core ships no records: the host registers the catalogs a document's references resolve in

// The slide's canvas, layout, theme and font families, resolved the one way every engine resolves them.
const { options } = resolveSlideContext(deck, 0, { fonts, catalogs })
const geometry = composeSlide(deck.slides[0], options)
const { presentation, pages } = paginate(deck, { fonts, catalogs })
console.log(geometry.width, geometry.height, presentation.slides.length, pages.length)
  • Measured, not guessed. Pass the same font options to composition, preview and export. Sizes snap to PowerPoint's 0.01 pt grid, so the preview draws exactly the size the PPTX stores.
  • Pagination never drops content. It returns ordinary OPF slides plus a mapping back to the source, keeping text, whitespace, reading order and list numbering across continuation pages.
  • Furniture. Shared headers and footers reserve bands. Body content stays between geometry.furniture.headerBottom and footerTop, and footnotes reserve their own area above the footer.
  • Right-to-left. The deck language sets direction. Columns, regions, list markers, tables and timelines mirror, and an authored left means the start edge.
  • Shared visual tables. Code syntax colors, metric trend arrows, the DrawingML pattern bitmaps and contrast-safe chart palettes live in core, so both engines draw them alike.

More in dynamic layouts and rich text measurement.

Render: SVG, PNG, PDF

@openpresentation/opf-render turns composed geometry into deterministic SVG, then rasterizes it or converts it to PDF. It makes no network request and reads no clock, and uses system fonts only if you ask.

render.ts
import { readFile, writeFile } from "node:fs/promises"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import { paginate } from "@openpresentation/opf/pagination"
import { toPdf, toPng, toSvg } from "@openpresentation/opf-render"
import { loadFonts } from "@openpresentation/opf-render/fonts-node"

const deck = JSON.parse(await readFile("deck.opf.json", "utf8"))

const fonts = await loadFonts({ pack: "office", substitutionPolicy: "visual" })
const catalogs = [defaultCatalog]
const { presentation } = paginate(deck, { fonts, catalogs })

const svgs = toSvg(presentation, { fonts, catalogs, trace: true }) // one SVG per slide, data-opf-path on every element
await writeFile("slide-1.svg", svgs[0])
await writeFile("slide-1.png", await toPng(presentation, 1, { fonts, catalogs })) // slides count from 1
await writeFile("deck.pdf", await toPdf(svgs, { fonts })) // a deck or its SVGs; vector, selectable text
console.log(fonts.registry.substitutions) // what replaced what
  • Vector PDF by default. TrueType subsets with ToUnicode, native gradients and clips, link annotations, tagged structure, and one SVG pixel per PDF point. An effect with no PDF form rasterizes that one element and reports pdf-raster-fallback; strict: true throws instead. raster: true keeps image-per-slide output.
  • Fonts. The base pack (Roboto, Roboto Mono) or the office pack (metric-compatible substitutes such as Arimo, Tinos, Carlito, and Intos for Aptos), with a metric or visual substitution policy. Script faces load on demand with scripts: "auto". Symbol fonts such as Wingdings are mapped without being bundled.
  • What to install. From 0.16 the converters and the font packages are optional peers, loaded on first use, so an SVG-only install holds none of them. /svg needs nothing; /png (or the root) needs @resvg/resvg-js, plus sharp for WebP and rotated JPEG pictures; /pdf needs sharp for pictures, and pdf-lib and @resvg/resvg-js for raster: true; loadFonts needs the @expo-google-fonts/* packages of its pack. The install command above lists the set these samples use. A missing converter rejects with converter-missing, whose message carries the install command.
  • Browser. /svg is the pure renderer, /fonts-browser loads hash-verified font bytes lazily, and /export-browser produces PDF and PNG with no Node modules.
  • Self-host the fonts. npx opf-preview-fonts public/opf-fonts --scripts Jpan,Arab copies the pinned files with SHA-256 checks. Never hotlink a font CDN. See fonts and fidelity.

Embed a presentation on a website

This HTML requires a bundler such as Vite to resolve the npm import. Serve your deck as public/deck.opf.json and copy the fonts into public/opf-fonts. Download the complete website starter for an executable project with local deck data, self-hosted fonts and PowerPoint/PDF downloads.

index.html (processed by Vite)
<script type="module">import "@openpresentation/opf-render/element/define"</script>
<opf-deck src="/deck.opf.json" fonts="/opf-fonts/" thumbnails present></opf-deck>

The element fires ready, slidechange, presentstart, presentend and error. Also exported are toHtml, for server markup with a no-JavaScript fallback, and present(), for a full-screen slideshow with a speaker view. There are no transitions or builds yet.

PowerPoint: export and import

@openpresentation/opf-pptx writes editable native PowerPoint and reads it back. It is pure, runs locally, and works in the browser too.

export-pptx.ts
import { readFile, writeFile } from "node:fs/promises"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import { paginate } from "@openpresentation/opf/pagination"
import { loadFonts } from "@openpresentation/opf-render/fonts-node"
import { fromPptx, toPptx } from "@openpresentation/opf-pptx"

const deck = JSON.parse(await readFile("deck.opf.json", "utf8"))
const fonts = await loadFonts({ pack: "office", substitutionPolicy: "visual" })
const catalogs = [defaultCatalog]
const { presentation } = paginate(deck, { fonts, catalogs })

const bytes = await toPptx(presentation, { fonts, catalogs }) // the same fonts and catalogs as the preview
await writeFile("deck.pptx", bytes)

const roundTrip = await fromPptx(bytes) // mechanical reimport; retain the original OPF source
console.log(roundTrip.name, (roundTrip.slides as unknown[]).length)

The second snippet goes the other way. It reads a deck made elsewhere, and fills the template from Markdown and templates while exporting it.

import-pptx.ts
import { readFile, writeFile } from "node:fs/promises"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import { fromPptx, toPptx } from "@openpresentation/opf-pptx"

// A deck made elsewhere: a mechanical import, plus raw per-shape facts to rebuild semantics yourself.
const { presentation, signals } = await fromPptx(await readFile("incoming.pptx"), { signals: true })
console.log(Object.keys(presentation), Object.keys(signals))

// Fill a template while exporting.
const template = JSON.parse(await readFile("quarterly.opf.json", "utf8"))
const filled = await toPptx(template, { catalogs: [defaultCatalog], variables: { client: "Globex", revenue: 1250000, wins: ["Faster onboarding"] } })
await writeFile("globex.pptx", filled)

Native, editable objects

Text boxes, tables with merged and styled cells, native charts with embedded workbooks, the PowerPoint section list, slide backgrounds, and SVG pictures with a PNG fallback.

Office 2016 chart types

Treemap, histogram, pareto, box-and-whisker, waterfall and funnel export as chartex parts by default (charts: "auto"). world stays a column fallback, because PowerPoint's map needs online geodata.

Fonts are named, never embedded

The PPTX lists only the deck's fonts, with per-run language, theme east-Asian and complex-script slots, and right-to-left runs. The viewer needs the licensed fonts installed.

Deterministic bytes

ZIP order, timestamps and workbook parts are normalized. Full provenance restores supported authored semantics when native evidence is unchanged; mechanical import can normalize fields. Keep the original OPF as canonical source.

Import from a deck made elsewhere is mechanical: text, tables, images and cached chart data map across, and a shape with no mapping becomes an editable text block with a diagnostic. There is no AI classification pass; the per-shape signals are there for you to rebuild structure yourself.

Editor primitives

@openpresentation/opf-editor is headless first. Every change is a validated JSON Patch with its inverse, so undo is a replay.

session.ts
import { readFile } from "node:fs/promises"
import { gallery as defaultCatalog } from "@openpresentation/gallery"
import { createEditorSession, saveDocument } from "@openpresentation/opf-editor"
import { switchDimension } from "@openpresentation/opf-editor/switches"

const deck = JSON.parse(await readFile("deck.opf.json", "utf8"))
// The editor imports no catalog data: the host registers the catalogs once, and pickers, the canvas and exports all use them.
const editor = createEditorSession(deck, { rejectInvalid: true, catalogs: [defaultCatalog] })

const change = editor.set("slides.0.title", "Quarterly plan")
console.log(change.patches) // [{ op: "replace", path: "/slides/0/title", value: "Quarterly plan" }]
editor.undo()
editor.redo()

// A gallery dimension is one transaction, so one undo restores the deck.
switchDimension(editor, "font-schemes", "georgia")
editor.undo()

// Saving embeds every record the deck references (core embed), so the file renders the same with no catalog registered.
const { document: saved } = saveDocument(editor)
console.log(Object.keys((saved as { catalogs?: object }).catalogs ?? {}))

In a browser, bind your own preview to the session, or take the ready-made canvas. This code needs a DOM. The live editor guide has a complete page.

preview.ts (browser)
import { createEditorSession, createSvgTraceBinding } from "@openpresentation/opf-editor"
import { createCanvasEditor } from "@openpresentation/opf-editor/canvas"
import { toSvg } from "@openpresentation/opf-render/svg"
import { loadFonts } from "@openpresentation/opf-render/fonts-browser"
import { gallery as defaultCatalog } from "@openpresentation/gallery"

// Your own preview: draw one slide with trace on, then bind the drawn elements back to the document.
const editor = createEditorSession(deck, { rejectInvalid: true, catalogs: [defaultCatalog] })
preview.innerHTML = toSvg(editor.presentation, 1, { trace: true, catalogs: editor.catalogs }) // slides count from 1

const binding = createSvgTraceBinding(preview.querySelector("svg")!, editor, {
  onSelect(selection) {
    inspector.open({ path: selection.path, commit: (value: unknown) => binding.commit(selection.element, value) })
  },
})

// Or take the ready-made canvas, with inline text editing and a fonts handle (fonts load before pixels).
const fonts = await loadFonts({ faces, lazyFontsBaseUrl: "/opf-fonts/lazy/", scriptBaseUrl: "/opf-fonts/scripts/" })
const canvas = createCanvasEditor(container, {
  editor, // the session registers the catalogs the canvas draws with
  fonts,
  onCommit: ({ editor }) => save(editor.presentation),
})
await canvas.ready

The editor exposes many subpath entries, each opt-in. The ones you will reach for first:

EntryGives you
/canvasSVG canvas with inline text editing, track-divider dragging and a font gate (fonts load before pixels).
/switchesGallery dimensions such as switchDimension(editor, "font-schemes", "georgia"), each one undoable transaction.
/slides, /outlineSlide management and an outline view.
/data-grid, /tablesA spreadsheet grid with CSV and TSV paste, transpose and cell merge.
/persistenceAutosave to IndexedDB with a restore prompt. Local only.
/exportBrowser PDF, PNG and SVG downloads with progress and cancel.
/find, /image-crop, /review-panelFind and replace, crop with a focal point, and audit findings in the interface.
/react, /svelteBindings. You pass in your own React; nothing is bundled.

Data, charts, tables

Requires @openpresentation/opf 0.13.0 or later. Shared datasets, number formats, series mapping by column name and strict chart numbers are not in earlier core releases, and a deck that uses them does not validate there. The renderer and the PowerPoint exporter need releases built on the same core to draw and export them. The editor's data grid and opf ingest --dataset need an editor and a CLI release built on core 0.13.0 or later; check the changelog before relying on them.

A dataset is declared once and referenced by name from charts and tables. The contract is in docs/chart-table-data.md.

deck.opf.json (datasets)
{
  "datasets": {
    "revenue": {
      "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, "Costs"],
      "rows": [["Q1", 12, 8], ["Q2", 18, 11], ["Q3", 24, 15]],
      "source": { "src": "./data/revenue.csv", "retrieved": "2026-10-05" }
    }
  },
  "slides": [
    {
      "title": "Revenue",
      "chart": {
        "type": "line",
        "data": { "dataset": "revenue" },
        "mapping": { "series": ["Revenue"] },
        "legend": "bottom",
        "axisTitles": { "value": "USD m" }
      }
    },
    { "title": "Detail", "table": { "dataset": "revenue", "fields": ["Quarter", "Revenue"] } }
  ]
}
  • Strict chart numbers. A chart value is a finite number or a strict decimal string such as "12", "-3.5" or "1e6". Values such as "12%", "$5" and "1,234" become gaps with a chart-value-not-numeric warning, never a guessed value. This is the one behavior change in that release.
  • Migration is a validation fix. For a column whose text cells share one style, suggestChartNumberFix returns a JSON Patch that stores numbers and adds the NumberFormat that displays the same text. validate attaches it to the warning.
  • One format language. NumberFormat (#,##0, 0.0%, $#,##0.00) is shared with number variables, and converts to and from Excel format codes (toExcelNumberFormat, fromExcelNumberFormat) for the embedded workbook.
  • Provenance, not live links. source records where data came from, and no engine fetches it. Data is always inline columns and rows or a top-level dataset.
  • Chart options axisTitles, legend and dataLabels are in core 0.12, with a chart-option-adapted warning where a chart type cannot show one. See CSV and JSON data for the CLI import commands.

A table carries a text alternative the way a chart does: table.alt says what the table shows (the point and the key numbers, not "a table"), and "" marks it decorative. The preview names the table with it, PowerPoint stores it as the table frame's description, and core adds no validation rule for it. See content payloads and tables.

shell
opf ingest revenue.csv --as chart --category Quarter --into deck.opf.json --in-place
opf ingest revenue.csv --as table --dataset revenue --into deck.opf.json --in-place

The second command stores imported rows in the revenue dataset and adds a table that references it.

Markdown and templates

Two text-first ways in: write a deck as Markdown, or fill a template from JSON or CSV.

Markdown dialect

deck.md
---
name: Q3 Business Review
language: en-US
---

<!-- slide: id=cover layout=title section=Overview -->
# Q3 Business Review
## Operations and growth

Note: Two minutes on the agenda, then straight into the numbers.

---

# Revenue grew every quarter

```chart column
Quarter,Revenue
Q1,12
Q2,18
```
markdown.ts
import { readFile, writeFile } from "node:fs/promises"
import { fromMarkdown, toMarkdown } from "@openpresentation/opf/markdown"

const { presentation, valid, findings } = fromMarkdown(await readFile("deck.md", "utf8"))
if (!valid) {
  for (const finding of findings) {
    console.error(finding.location.line + ":" + finding.location.column + " " + finding.message)
  }
}

// Any valid deck writes back out. What the dialect cannot express goes into opf-slide and opf-block YAML fences.
const { markdown, report } = toMarkdown(presentation)
await writeFile("roundtrip.md", markdown)
console.log(report.lossless)

fromMarkdown and opf convert deck.md deck.opf.json return the deck plus findings mapped back to Markdown lines. toMarkdown and opf convert deck.opf.json deck.opf.md write any valid deck; what the dialect cannot express goes into opf-slide and opf-block YAML fences, so the round trip is lossless.

Since 0.16 a file named deck.opf.md is a Markdown deck in every opf command, and core has one entry point for all three text forms. The file name decides the form (.opf.md is Markdown, .yaml and .yml are YAML, anything else is JSON), the content is never sniffed, and a plain .md file is never a deck (only opf convert reads a plain .md input as OPF Markdown). From 0.17 the entry points are parse and stringify (0.16 called them readDeck and writeDeck). parse returns the deck and throws OPFValidationError, with findings located by line and column, on a syntax, schema or reference error; validate(text) returns the full report, warnings included.

parse-deck.ts
import { readFile, writeFile } from "node:fs/promises"
import { parse, stringify } from "@openpresentation/opf"
import { gallery as defaultCatalog } from "@openpresentation/gallery"

// One entry point for every text form. The file name decides the form: .opf.md is Markdown, .yaml and .yml are YAML,
// anything else is JSON (the content is never sniffed, and a plain .md file is never a deck).
// parse returns the deck, and throws an OPFValidationError for a syntax, schema or reference error, with every finding
// located (error.report.findings, line and column) in the text that was read. validate(text) returns the full report.
const presentation = parse(await readFile("deck.md", "utf8"), {
  filename: "deck.opf.md",
  catalogs: [defaultCatalog],
})
console.log(presentation.slides.length)

await writeFile("deck.opf.md", stringify(presentation, { filename: "deck.opf.md" }))
await writeFile("deck.out.opf.json", stringify(presentation, { filename: "deck.out.opf.json" }))

Templates and variables

A template is an incomplete OPF file: "template": true, typed variables, {{id}} inside strings, and var:id where a whole typed value belongs.

quarterly.opf.json
{
  "template": true,
  "name": "Quarterly review for {{client}}",
  "variables": {
    "client": { "type": "text", "example": "Acme Corp" },
    "revenue": { "type": "number", "format": "$#,##0", "example": 1250000 },
    "wins": { "type": "list", "example": ["Faster onboarding", "Lower churn"] }
  },
  "slides": [
    { "id": "cover", "title": "Quarterly review: {{client}}" },
    { "id": "wins", "title": "Wins", "bullets": ["Revenue {{revenue}}", "var:wins"] }
  ]
}
fill.ts
import { readFile } from "node:fs/promises"
import { resolveVariables } from "@openpresentation/opf"

const template = JSON.parse(await readFile("quarterly.opf.json", "utf8"))

// Pure and deterministic. A value comes from you or the declaration, never from the library.
const result = resolveVariables(template, { client: "Globex", revenue: 1250000, wins: ["Faster onboarding"] }, { strict: true })
console.log(result)

resolveVariables is pure and never invents content. Also in core, @openpresentation/opf/convert changes a block's kind (list to table, timeline to table, text to quote and so on), splits and merges slides, and reports whether the conversion was lossless or what it lost. Guides: templates and variables.

Patch, diff, merge

Decks live in git, so core ships the tools to change and reconcile them precisely.

changes.json
[
  { "op": "test", "path": "/slides/0/id", "value": "title" },
  { "op": "replace", "path": "/slides/0/title", "value": "Approve the next milestone" },
  { "op": "add", "path": "/slides/-", "value": { "id": "next", "title": "Next steps" } }
]
patch.ts
import { readFile } from "node:fs/promises"
import { diff, merge } from "@openpresentation/opf/diff"
import { applyPatch, applyPatchWithInverse, invertPatch } from "@openpresentation/opf/patch"

const base = JSON.parse(await readFile("deck.opf.json", "utf8"))
const patch = JSON.parse(await readFile("changes.json", "utf8"))

// Apply a patch, validate the result, and keep the patch that undoes it.
const ours = applyPatchWithInverse(base, patch, { validate: true })
console.log(ours.inverse, invertPatch(base, patch))

// Compare two decks: slides match by id, then content, then similarity.
const changes = diff(base, ours.presentation)
console.log(changes.summary, changes.patch)

// Three-way merge. Collisions come back as conflicts, never as a silent choice.
const theirs = applyPatch(base, [{ op: "replace", path: "/slides/1/title", value: "Agenda and goals" }])
const { merged, conflicts, clean } = merge(base, ours.presentation, theirs)
console.log(clean, conflicts.length, merged)
  • /patch is the single RFC 6902 implementation, used by the CLI and the editor alike. applyPatchWithInverse and invertPatch give you undo.
  • diff matches slides by id, then content, then similarity, detects moves, and emits a patch from one deck to the other.
  • merge is three-way. Collisions become conflict objects with the base, ours and theirs values, and the CLI blocks the write unless you pass --prefer ours|theirs.
  • Writes are atomic and validated first, and a failed patch leaves the file untouched.

CLI and agent skills

opf bundles the schema, the default catalog snapshot and the validator, so it needs no network. The rendering commands load opf-render and opf-pptx as optional peers on first use.

shell
npm install -g @openpresentation/cli @openpresentation/opf-render @openpresentation/opf-pptx @resvg/resvg-js@^2.6.2 sharp@^0.35.5 pdf-lib@^1.17.1 @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2
opf --version

From code

Since 0.18, import * as opf from "@openpresentation/opf" is the one import for every runtime. In Node, Bun and Deno it also carries core's file API, convert, open and save, plus ingest, fill, edit, stats, paginate and embed with the CLI's defaults; in a browser or a worker the same names load, and the file functions reject with node-only. It is the simple path when an application exports a deck as PDF, PNG, SVG or PowerPoint, imports a PowerPoint file, or changes a deck file's form, without spawning opf and without wiring fonts and catalogs by hand. The extensions name the formats, nothing is written until everything is produced, and opf convert runs the same code, so the bytes match the command's. opf-render and opf-pptx stay optional peers, loaded the first time a call needs them (a missing one throws peer-not-installed with the install command). The options are the opf convert flags in camel case (slides, scale, raster, text, charts, images, fonts, name), and every error carries a code to branch on. The 0.17 @openpresentation/opf/node subpath is gone; the catalog is now exported as gallery from @openpresentation/gallery. Prefer a shorter name? npm i opf@npm:@openpresentation/opf installs the same package as opf.

convert.ts
import * as opf from "@openpresentation/opf"

// One import in every runtime; in Node it also has the file API. Files in, files out; the extensions name the formats.
// opf convert does the same from a shell.
await opf.convert("deck.opf.json", "deck.pdf")
await opf.convert("deck.opf.json", "deck.pptx")
await opf.convert("deck.opf.json", "slides/deck.png", { slides: "1-3", scale: 2 }) // slides/deck-1.png, -2, -3

// open reads .opf.md, .yaml, .yml or .json (or imports a .pptx); save writes the form the extension names.
const deck = await opf.open("deck.opf.json")
deck.slides.push({ title: "Q4" })
await opf.save(deck, "deck.opf.md")

// With no output path, convert writes nothing and returns the files: { files: [{ name, type, bytes }], findings }.
const { files } = await opf.convert(deck, { format: "pdf" })

// Import is a conversion, not a lossless round trip: what it cannot keep is in findings.
const { findings } = await opf.convert("deck.pptx", "deck.imported.opf.json")
console.log(files[0].name, deck.slides.length, findings.length)
CommandWhat it does
create, validateStart a deck, check it, and report findings with source ranges; validate --list-rules prints the rules.
edit, diff, merge, formatJSON Patch editing and version-control helpers.
statsNeutral facts about a deck: counts, words, notes, alt text and speaking time.
paginate, embedSplit overflow into slides; embed the catalog records a document uses for offline use.
fill, ingestTemplates filled from data, and CSV or JSON into tables and charts.
convert, doctorOne file into another, the formats named by the extensions: JSON, YAML and Markdown decks, PNG and SVG slides, PDF, PPTX, a zip of slides, and PPTX back to OPF. doctor reports which formats this install can write and the one command that installs the rest.
schemas, schema, catalogs, catalogLook up exact schema branches and catalog records.
skills install|status|updateInstall the six agent skills into your project.
shell
opf create deck.opf.json --title "Quarterly Review"
opf validate deck.opf.json --fail-on warning
opf validate deck.opf.json --only accessibility --format text
opf validate --list-rules
opf stats deck.opf.json --format text
shell
opf convert deck.md deck.opf.json
opf convert deck.opf.json deck.opf.md
opf fill quarterly.opf.json clients.csv "decks/qbr-{client}.opf.json"
opf fill quarterly.opf.json --examples > preview.opf.json
shell
opf edit deck.opf.json --patch changes.json --in-place --expect-sha256 "$SHA"
opf diff before.opf.json after.opf.json --format patch
opf merge base.opf.json ours.opf.json theirs.opf.json merged.opf.json
opf format decks/*.opf.json --check
shell
opf convert deck.opf.json slides/deck.png --slides 1,3-5 --scale 2
opf convert deck.opf.json deck.pdf --raster --scale 2
opf convert deck.opf.json deck.pptx --charts native
opf convert deck.pptx deck.opf.json --signals signals.json
opf doctor deck.opf.json --format text
shell
opf convert deck.opf.json deck.pdf
opf convert deck.opf.md - --to pdf > deck.pdf
opf convert deck.pptx deck.opf.yaml

opf convert validates first and is deterministic: no network, no system fonts, and no clock unless you pass --date. It reads fonts only from the bundled pack plus --fonts. Every command that writes a deck takes an optional output after its inputs (stdout without one), -i to rewrite the input, and format, embed and paginate take --check. The CLI guide has every option.

Agent skills

One command copies six skills into .agents/skills (or pass --agent claude-code, codex or cursor): opf-author, opf-layout, opf-presets, opf-edit, opf-export and opf-inspect. The installer refuses to overwrite skills you have customized, and works offline once the CLI is installed.

shell
npx @openpresentation/cli@latest skills install
npx @openpresentation/cli@latest skills install --agent claude-code

See the agent quickstart and agent skills.

Known limits

Schema support is not the same as renderer, editor or PowerPoint fidelity. These are the gaps worth knowing about before you build on the toolkit.

AreaStatus
Native PowerPoint fidelity in generalTracked in opf#87. Bounded native checks pass; arbitrary Office layout and reflow is not certified.
Linked chart data (ChartDataSource)Not planned for now (opf#240). Embed the data and record source.
Video export, external chart dataNot supported. Engines never fetch data or media.
PDF/UA and PDF/ANot claimed. The vector PDF is tagged and has selectable text.
Mermaid and general SVG diagramsPlanned. An embedded SVG is not an editable native shape.
Full WYSIWYG editing (IME, bidirectional text)Schema support is not editor coverage.

Keep going with the developer docs index, the spec, the examples and the playground.