Skip to content

About

Converts Keynote, PowerPoint, Google Slides, Canva and ODP decks to PDF and keeps the presenter notes, mapped to the right pages even when slides are hidden. A GUI, a CLI and a Nextcloud app. Downloads: https://stoatworks-labs.com/software/presentation-converter/

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

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.

Watch it running (45 seconds)

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.

Converting a folder of presentations: the form, live per-file results showing pages, notes and which engines ran, and the engine availability grid

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

The watch folder tab: choose a folder to watch and an output folder, and new or changed presentations convert automatically

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

The Settings tab: Google Slides with sign-in or service-account options, and Canva with its client credentials and redirect URL

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.

About

Converts Keynote, PowerPoint, Google Slides, Canva and ODP decks to PDF and keeps the presenter notes, mapped to the right pages even when slides are hidden. A GUI, a CLI and a Nextcloud app. Downloads: https://stoatworks-labs.com/software/presentation-converter/

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages