English · Русский
A cross-platform PDF viewer for conference talks: the slide goes to the projector, while your laptop keeps a presenter view with the slide order, the next slide and the text of the current one. Several decks open as tabs. The interface is light and deliberately plain.
macOS · Windows · Linux (Electron + pdf.js).
Prebuilt installers are on the
Releases page: .dmg for macOS
(Apple Silicon and Intel separately), .exe for Windows (installer and
portable), .AppImage and .deb for Linux.
The builds are not code-signed, so the first launch needs a nudge:
- macOS — right-click the app → “Open”, then confirm. A plain double-click is blocked by Gatekeeper.
- Windows — SmartScreen warns: “More info” → “Run anyway”.
- Linux — make the AppImage executable:
chmod +x SlidePDF-*.AppImage.
Node.js 20 or newer.
npm install
npm start # or: npm start -- path/to/deck.pdf
npm run dev # same, plus renderer logs in the terminal
npm startgoes throughscripts/start.js. The VS Code terminal exportsELECTRON_RUN_AS_NODE=1, and with it the Electron binary starts as plain Node and the app crashes. The launcher clears that variable.
npm run check # syntax check across all sources
npm test # tests for the pure show logicTests run on Node's built-in runner (node --test), with no dependencies. They
cover the pure logic in src/main/lib/deck.js: page
bounds, tab order, screen selection. The rest of the main process is tied to
Electron windows and displays and only makes sense to verify by running it —
that is what npm run check guards against typos before startup. GitHub Actions
runs both.
npm run dist:mac # dmg + zip
npm run dist:win # nsis + portable
npm run dist:linux # AppImage + debelectron-builder only builds for the host OS: cross-building for Windows needs
wine, and Linux builds are not supported on macOS. Releases are therefore built
on GitHub Actions — one job per OS. The
release.yml workflow fires on a v* tag:
git tag v0.1.0 && git push origin v0.1.0The audience window does not appear on startup at all — only on “Present” (F5), and it leaves the screen once the show ends. With no file open the show does not start at all — the button and the menu item are disabled, otherwise the projector would get the “open a file” prompt. To see beforehand what the room will see, open it by hand: “Show / hide audience window” in the menu.
The app picks the screen for the show on its own:
- An external screen is present — the slide goes there, the presenter view stays on the built-in display.
- A single screen — the show covers the presenter window.
- A projector plugged in or unplugged mid-talk —
display-added/display-removedrebuild the layout on the fly, and a running show moves to the right screen without a break. Plugging one in does not start a show. - The automation can be overridden: the gear in the header opens settings with a list of screens, where you pick the one the slide goes to. After a manual choice the automation stops reassigning the show screen — until that monitor is disconnected.
Both windows render the PDF independently from the same bytes, while the state — current slide, black screen, active tab — lives in the main process and is broadcast to both. The audience window cannot fall behind the presenter view.
Starts and ends with F5. The audience window is created without a
frame (frame: false), so nothing but the slide is on screen; in preview mode
(single display) it gets its own title strip instead of the system frame, so it
can still be dragged.
The show appears already finished. While the window geometry changes it is kept
transparent, and it is only revealed once the renderer confirms it has redrawn
the slide at the new size. Otherwise the audience would watch the window stretch
out of its preview size while the slide caught up. On macOS the show uses
simpleFullScreen so that no separate Space is created with its transition
animation.
Freeze (F) detaches the room from your navigation: the audience screen stays on the current slide while you browse the deck on your own — find the slide behind a question, check a number, come back. The room sees none of it. Neither navigation nor switching tabs lifts the freeze; only pressing it again or ending the show does. A reminder of what the room is looking at stays in the header, and the slide is marked in the thumbnail strip.
Focus moves to the full-screen window immediately, so a presenter remote and the keyboard act on the show. The mouse cursor hides there — instantly on start, and again two seconds after the mouse stops moving.
- Several decks open as tabs: Ctrl+Tab forward, Shift+Tab back, the cross on a tab or Cmd/Ctrl+W to close. Each tab remembers its own page; the open dialog and drag-and-drop both accept several files at once.
- The current slide is large, the next one sits beside it.
- Slide order: a thumbnail strip at the bottom and a full-screen grid of every slide (G). In the grid the arrows move the selection without touching the audience screen; Enter or a click jumps there.
- The text of the current slide stands in for speaker notes.
- The slide counter and the screen indicator both appear only when they have something to say.
- Settings offer “reopen last files”: the app remembers the open tabs and brings them back on the next launch, along with the one you left off on. Off by default, and while it is off no paths are written anywhere. Files given on the command line win over saved ones, and files that moved or were deleted are skipped silently.
| Key | Action |
|---|---|
| → ↓ Space PageDown | next slide |
| ← ↑ PageUp Backspace | previous slide |
| Home / End | first / last slide |
| digits, then Enter | jump to a slide by number |
| G | grid of all slides |
| Ctrl+Tab / Shift+Tab | next / previous tab |
| B | black out the audience screen |
| F | freeze: the room stays on the slide while you browse |
| F5 | start / end the show |
| O | open a PDF |
| Esc | close the grid, clear the black screen, end the show |
Both windows listen for keys, so a presenter remote (which sends PageUp/PageDown) works no matter which window has focus. The layout also recognises the Cyrillic letters on the same physical keys.
The mark is a deck of slides: three same-sized cards fanned diagonally. The tile is coral — in a taskbar where almost everything is blue and grey, a warm colour stands out.
assets/logo.svg the mark for the interface (two-tone, on a light background)
assets/icon.svg the app icon — a white mark on a coral tile
assets/icon-small.svg a simplified mark for the small icon sizes
assets/icon.png 1024×1024, electron-builder turns it into .icns
assets/icon.ico 16…256 for Windows
assets/file-icon.svg the PDF file icon — a page with a label
assets/file-icon-small.svg a simplified file icon for 16–32 px
assets/file-icon.png 1024×1024
assets/file-icon.ico 16…256, the file icon in Windows Explorer
assets/file-icon.icns 16…1024, the file icon in Finder
npm run icon rebuilds both images from the SVGs using Chromium: a standalone
SVG converter may be missing from the system, while Electron is already
installed. The script bootstraps itself — under plain Node it re-spawns itself
in Electron.
The .ico and .icns files are built here rather than left to electron-builder:
that one squeezes a single 1024×1024 image down to every size at once, and a 64×
downscale smears the detail away. Here each size is rasterised from the vector
separately, at its natural size, and the small ones come from simplified sources.
For the app icon that means two cards instead of three with a larger offset: at
16 pixels the third card shifts by less than a pixel and merges with its
neighbour. For the file icon the “PDF” label gives way to a solid band — three
letters across eight dots turn to mud.
The installer registers the app as a PDF viewer, so files open on a double click
or through “Open with”. It works on all three systems: macOS delivers the file
through the open-file event, Windows and Linux as a command-line argument.
Launching again with a file does not spawn a second window — the file goes to the
running one as a new tab.
The app declares itself a viewer, not the owner of the format: it does not take PDFs away from the system viewer, the choice stays yours.
src/main/main.js displays, windows, show state, menu
src/main/lib/deck.js pure logic: pages, tabs, screen choice
src/main/lib/layout.js the window-to-display layout decision
src/preload/preload.js contextBridge: commands, subscriptions, drag-and-drop paths
src/renderer/
presenter.* presenter view
audience.* audience screen
lib/pdfview.js pdf.js wrapper: document, fitted slide, thumbnails
lib/keys.js shared key bindings and drag-and-drop
scripts/start.js launcher that clears ELECTRON_RUN_AS_NODE
scripts/make-icon.js builds assets/icon.png from assets/icon.svg
scripts/check.js syntax check across the sources
test/ tests for the pure logic
assets/ logo and application icon
docs/ screenshot for the README
The windows are isolated: contextIsolation: true, nodeIntegration: false,
and the CSP forbids every network request — a PDF with external links loads
nothing.
Source comments are written in Russian.
GNU General Public License v3.0 or later — full text in LICENSE.
SlidePDF is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
Third-party components: pdf.js (Apache-2.0) and Electron (MIT).
