Presentation Converter
AI-assisted project. This codebase was created with Claude Code
(Anthropic), directed and reviewed by a human author. That covers the code, the docs and the
design decisions written up below. Check it over yourself before you rely on it for a show,
as you would with any code.
Converts Keynote, PowerPoint, Google Slides, Canva and OpenDocument presentations to PDF,
and keeps the presenter notes, in a .notes.json sidecar written next to each PDF.
Exporting a deck to PDF is easy. Every tool that does it throws the speaker notes in the bin,
which is a bit of a problem if the PDF is what you're presenting from. This one exports the
PDF and rescues the notes, each mapped to the right page.
- A desktop GUI: pick files, convert a whole folder, or leave a watch folder running,
and connect Google or Canva accounts from the Settings tab.
- A CLI and library: the backend other programs drive, including
presentation-commander,
which reads the sidecars this produces.
- A Nextcloud app: keeps PDF versions of every presentation in a folder and its
subfolders up to date automatically.
A 45-second tour, driven over the app's own HTTP API. The deck it converts is six
slides with one hidden, and it comes out as a five-page PDF with five notes mapped to the
pages that survived. That's the whole reason this tool exists, happening on screen.
These are captures of the running app, not mockups, made by npm run screenshots,
which converts a demo folder and photographs the result.
Download
v0.3.0
This release contains:
All builds, checksums and release notes: github.com/stoatworks-labs/presentation-converter/releases.
The problem it actually solves
A deck's slide count and its PDF's page count usually disagree, because every exporter
silently drops hidden or skipped slides. Convert a 4-slide deck whose third slide is
hidden and you get a 3-page PDF, so a naive tool puts slide 3's notes on page 3, and
every note from there on is a page out.
This tool reads the PDF back, reconciles the two, and records what it did:
"alignment": "adjusted" // hidden slides accounted for; mapping is trustworthy
"alignment": "exact" // counts agreed
"alignment": "mismatch" // couldn't reconcile — notes emitted, but flagged as suspect
A mismatch shows up in the CLI, the GUI and the sidecar's warnings rather than
being wrong without telling anyone.
Install
Needs Node.js 20+.
git clone https://github.com/stoatworks-labs/presentation-converter.git
cd presentation-converter
npm install
npm run build
For PowerPoint and ODP files, install LibreOffice. On a
Mac, PowerPoint or Keynote can stand in for it with PowerPoint decks; ODP needs
LibreOffice. To check what your machine can do:
node packages/cli/dist/index.js doctor
Use it
Convert some files, or a whole tree:
presentation-converter convert "Q3 Review.key" "Keynote Address.pptx"
presentation-converter batch ~/Decks --out-dir ~/PDFs
Watch a folder and convert whatever lands in it:
presentation-converter watch ~/Dropbox/Incoming --out-dir ~/PDFs
Run the GUI (and the worker API) at http://127.0.0.1:4747:
presentation-converter serve
Everything takes --json for scripting, and exits non-zero if any file failed.
Options worth knowing
| Flag | Effect |
|---|
--out-dir <dir> | write PDFs here instead of beside the source; the folder tree is mirrored |
--flat | don't mirror subfolders — everything lands in one directory |
--force | reconvert even when the output is already newer than the source |
--no-sidecar | write only the PDF |
--pdf-engine <id> | force libreoffice, powerpoint, keynote, google-slides or canva |
--exclude <text...> | skip any path containing this text |
--dry-run | (batch) list what would be converted, then stop |
Re-runs are incremental: a deck whose PDF and sidecar are already newer than it gets
skipped, so a repeat batch over an unchanged folder takes milliseconds.
What it converts, and with what
I've kept notes extraction independent of PDF rendering on purpose. For Office and ODF
files the notes are read straight out of the source package's XML, so you get the
author's text word for word whichever engine drew the PDF, and it works headless, with
no application installed.
| Format | PDF rendered by | Notes read from |
|---|
.key | Keynote (macOS only) | Keynote |
.pptx | LibreOffice, or PowerPoint, or Keynote on macOS | the .pptx package (OOXML) |
.ppt | LibreOffice, or PowerPoint, or Keynote on macOS | promoted to .pptx by LibreOffice, then the package |
.odp | LibreOffice | the .odp package (ODF) |
| Google Slides | Drive export | Slides API |
| Canva | PPTX export, rendered locally | the exported .pptx (OOXML) |
Keynote can't run on Linux, so a Linux host needs a paired Mac for .key files. See
the Nextcloud notes.
On a Mac with no LibreOffice, a PowerPoint deck goes to PowerPoint if it's installed, since
it draws the deck in the author's own fonts, and to Keynote if not. Both are driven over
AppleScript, so the first time, macOS asks whether whatever is running the converter (your
terminal, say) may control them. A .ppt still needs LibreOffice for its notes; without
it, convert with --no-sidecar.
If you have both Keynote and Keynote Creator Studio installed, it drives Creator Studio.
The older Keynote opens to a dialog telling you it's out of date and waits for a click that
never comes, so it's left alone. doctor says which one it will use.
Google Slides
Pass a share URL, a file id, or a .gslides shortcut:
presentation-converter convert "https://docs.google.com/presentation/d/FILE_ID/edit" -o ~/PDFs
Connect an account first, either way round:
In the GUI: run presentation-converter serve, open the Settings tab, and either
sign in with Google or paste a service-account key. Credentials are stored in your user
config directory with 0600 permissions, and the page shows the exact path.
By environment variable: best for servers, and it takes precedence over anything saved
in the GUI:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
Either route needs the Google Drive API and Google Slides API enabled on the
project. A service account can only see presentations that have been shared with its email
address, and Drive refuses to export presentations over 10 MB.
Canva
Convert straight from a Canva link, once an account is connected in Settings → Canva:
presentation-converter convert "https://www.canva.com/design/DAFxyz123/edit" -o ~/PDFs
Or with no setup at all: export from Canva with Download → PPTX and convert that file
like any other PowerPoint deck. Canva puts the speaker notes in it.
The engine exports PPTX, never PDF, and renders that file locally. Canva's API has
no notes field, so it has to fetch a PPTX anyway, and taking the pages from the same file
halves the API cost and guarantees the notes and the pages describe the same version of the
deck. It does mean you need LibreOffice, PowerPoint or Keynote. See docs/canva.md.
Connecting accounts
Credentials are stored in your user config directory with 0600 permissions, and the page
shows the exact path. Environment variables, when they're set, take precedence over anything
saved here.
The sidecar
Q3 Review.pdf gets Q3 Review.notes.json beside it:
{
"schemaVersion": 1,
"generator": "presentation-converter 0.1.0",
"convertedAt": "2026-07-28T16:20:24.230Z",
"source": { "file": "Q3 Review.key", "format": "keynote" },
"pdf": { "file": "Q3 Review.pdf", "pageCount": 3 },
"slideCount": 4,
"engines": { "pdf": "keynote", "notes": "keynote" },
"alignment": "adjusted",
"notes": {
"1": "Thank the sponsors.\nMention the fire exits.",
"3": "Hand over to Alex."
},
"slides": [
{ "index": 3, "page": null, "title": "Hidden Backup", "notes": "…", "hidden": true }
],
"warnings": []
}
notes is the map consumers read: PDF page number to note text. slides keeps the
full picture, including hidden slides that have no page. The full details and the
compatibility rules are in docs/sidecar-format.md.
Using it from another program
import { convertFile, convertFolder, WatchFolder } from '@presentation-converter/core'
const result = await convertFile({
sourcePath: '/decks/Q3 Review.key',
outputDir: '/out'
})
console.log(result.pageCount, result.alignment)
Or drive the CLI and parse --json, which is what the Nextcloud app does.
Nextcloud app
nextcloud/presentationconverter keeps PDFs of everything in the folders your users
choose. Install and configuration are in docs/nextcloud.md.
Repository layout
packages/core conversion engines, notes extraction, sidecar, batch, watch, settings
packages/cli the presentation-converter binary
packages/server HTTP API, progress stream, GUI host, macOS worker endpoint
packages/web the browser GUI
nextcloud/ the Nextcloud app (AGPL, as Nextcloud apps are)
Development
npm run build # everything
npm test # core unit tests
npm run typecheck # all workspaces
npm run dev # server + GUI
npm run screenshots # regenerate docs/screenshots from the running app
npm run screenshots needs the server running (npm run dev) and Chrome installed. It
drives headless Chrome over the DevTools Protocol (no Puppeteer dependency) and measures
each panel so the images clip cleanly instead of relying on hard-coded heights.
Status
I've verified it end to end on macOS against real Keynote-authored fixtures: .key and .pptx
conversion, hidden-slide page mapping, folder batches with tree mirroring, incremental
re-runs, output-collision detection, the watch folder, the GUI, and the worker endpoint.
On 2026-10-20 (macOS 26.4.1) I ran the two app engines again: Keynote Creator Studio 15.3.1
converts .pptx and .key with a hidden slide mapped correctly, whether or not Keynote was
already open, and PowerPoint 16.112.3 converts .pptx and .ppt the same way. PowerPoint
has only been tried while it was already running; starting it from cold, and the Automation
prompt the first time, haven't been. The older Keynote 14.5 isn't driven at all when Creator
Studio is installed (see above), and hasn't been tried on a Mac without Creator Studio.
The settings and OAuth endpoints are verified too: credential storage and 0600
permissions, redaction (no secret is ever returned to the browser), forged-state
rejection, and the generated consent URL.
Canva is verified as far as it can be without an account: notes extraction from a real
Canva PPTX export, PKCE generation, the authorisation URL, and design-id parsing.
Not tried against a live service yet: the Nextcloud app (written but never run, see
docs/nextcloud.md), the LibreOffice engine (there's no LibreOffice on the
development machine, so .odp rendering is untried too), and full OAuth round trips for Google or Canva, as each of those needs
real credentials and a human at the consent screen.
This project is built on other people's work — see ATTRIBUTIONS.md.
Licence
MIT, see LICENSE. The Nextcloud app under nextcloud/ is AGPL-3.0, as
Nextcloud apps must be; see nextcloud/LICENSE.