pdf-next by machine-made.
A small, fast PDF, image and markdown viewer that reloads the moment the file changes — built for the LaTeX and Typst compile loop, where you rebuild and want to see the result without touching anything.
It is the desktop sibling of vscode-pdf Next
and shares its rendering approach: Mozilla PDF.js (pdfjs-dist@6.3.289) parsing in a
real worker thread, dark reading modes that recolour pages instead of filtering them, and reload
that survives a build deleting and recreating the file mid-compile.
-
Sharp rendering on high-density displays. Pages refresh when display density changes. At high zoom, detail rendering skips off-screen drawing operations to reduce scrolling work.
-
Watches your file, on by default. Rebuild the PDF and the view updates within a second, keeping your page, scroll position and zoom. The interval is in the toolbar — 1s, 2s, 3s, or off. If the build deletes the file first, the last render stays on screen and a red dot appears until the new file lands.
-
Never shows a half-written file. A build rewrites a PDF over hundreds of milliseconds and the file's metadata changes the instant it starts, so a naive watcher reloads garbage. Before reloading, pdf-next checks the file is complete: on Windows the writer normally holds an exclusive lock, so simply opening it fails; on macOS and Linux nothing stops you reading a partial file, so PDFs are also checked for their
%%EOFtrailer. If it is not ready, the reload waits for the next tick. -
Leaves the PDF available to build tools. The viewer keeps PDF bytes in memory and closes the source file after reading. You can overwrite, replace, or delete it while it is displayed. Release smoke tests check those operations in the running viewer.
-
Fits the window to the document. Open a paper and the window becomes the size of the page itself, centred; open a wide figure and the window is wide. It only happens when you open a file — rebuilds never move your window. With no file open, it stays a small drop target.
-
Trims the window to the page when you open a file. A PDF or figure comes up at 100% and the window hugs it — no band of empty desk on a big screen. A figure larger than the screen, such as a plot saved at 400 dpi, opens whole: it is scaled down until it fits. A PDF page taller than the monitor clamps to the screen and scrolls. Rebuilds and tab switches leave the window where it is.
Ctrl+Shift+Fkeeps the window following the content after that. -
Dock to any half of the screen — five toolbar buttons, or
Ctrl+Shift+←/→/↑/↓, fill the left, right, top or bottom half, so the document takes one half and your editor keeps the other. Any dock switches to fit-width, because a half-screen window is for reading in, and fitting a whole page into one just shrinks the text. The center button (orCtrl+Shift+Enter, or pressing the same edge again) undocks: the window hugs the page at 100%, centred. Your place in the document survives every move. -
Fit the window to the content with
Ctrl+Shift+For the toolbar button — the other direction from everything above. Set a figure to the size you want it and the window comes to the picture: no padding, no border of background, the frame exactly on the edges. It stays on, so zooming out brings the window in with it and zooming in pushes it back out, up to the size of your screen. Choosing a fit preset from the zoom menu turns it off again, because fitting the content to the window is the opposite instruction. -
Tabs, without the memory. Open several files and they line up in a strip;
←/→,Ctrl+Tab,Ctrl+1…9or the mouse wheel over the strip move between them,Ctrl+Wcloses one. A tab is a path, not a loaded document — only the file you are looking at is in memory, so six open papers cost what one costs, and a background tab is read fresh from disk when you come back to it rather than going stale. Switching costs a re-parse, against a worker that is already warm. -
Walk a folder of figures. With a single file open,
←/→(or the toolbar arrows) step through every image next to it, in reading order —fig2beforefig10, not after it. The toolbar shows your position, and the window stays where it is instead of resizing on every press; each figure fills it whole (Fit page), larger or smaller than 100%. A rotate button next to the arrows turns the picture a quarter turn clockwise per press; the next file comes up upright again. -
Delete the file you are looking at.
Delete(⌘⌫on macOS) asks first, then moves the file to the Recycle Bin, the Trash, or your desktop's trash, where it can be restored. A folder with no Recycle Bin — a network share, or WSL's\\wsl.localhost— asks whether to delete the file permanently instead. In a folder of figures the next one opens; any other file closes its tab. -
PDF, images and markdown.
.pdf, plus.png,.jpg,.webpand.avif, plus.md. Images zoom too, with the same control and keys as a PDF. -
Markdown, rendered or raw. A
.mdopens as a typeset reading column — GitHub-style tables, task lists, footnotes and strikethrough included — parsed by pulldown-cmark in Rust and sanitized by ammonia before the HTML ever reaches the window, so a hostile file cannot script anything.Ctrl+U(or the toolbar button) flips to the raw text and back; zoom reflows the text rather than scaling a bitmap. Edit the file and the view reloads within a second, keeping your scroll position — the same watch loop as PDFs. -
Equations, typeset.
$…$,$$…$$and a fenced block markedmathare TeX, turned into MathML by math-core in the same Rust pass and set in New Computer Modern Math — no JavaScript math engine, nothing fetched, and the MathML meets the same sanitizer as the prose. Numbered environments,\labeland\eqref,\newcommandacross a file, andsplitwork. An equation that fails to convert, calls a macro in a loop, or exceeds the rendering limits is shown as escaped TeX source; an unknown command is marked in red and the rest of the equation still renders. A\eqrefto an equation further down the file shows(??). -
Markdown typography: Rendered text is set in Latin Modern Roman at 17 px on a 38 em measure, hyphenated with
text-wrap: prettyfor natural word breaks. Toolbar menus change the font (Latin Modern, the machine-made faces Weft, Sans and Heddle, Serif, Sans, Mono), the text size (14 px to 48 px) and the column width (Narrow, Normal, Wide, Full); each choice is remembered. Zoom reflows the text rather than scaling it. -
Coloured code: Fenced code blocks tagged python, c, cpp, rust, fortran, julia, bash, shell, toml, yaml, json, latex, matlab, makefile, cmake, diff or javascript (and their usual short tags, such as
py,rs,f90,sh,tex,yml) are coloured by highlight.js in all four colour modes (clear, night, invert, sepia); the raw Markdown view stays plain. An untagged or unknown block stays plain, and a block over 64 KiB, or code past 512 KiB in one file, is left uncoloured. -
Page view: The page-view menu in the toolbar shows a PDF as Single page or Two pages (the cover alone, then pairs, as in a printed book), and Markdown as Continuous, Single page or Two pages. Markdown pages are A4 boxes with 25 mm margins that take whole blocks: a heading moves to the next page with the block after it, and a block taller than a page keeps a page of its own, grown to fit, because paragraphs, tables, code and equations are not split. Reviews, find and line links work the same in every view, and each page box prints on its own sheet. The choice is remembered per kind.
-
Ask and the Review panel. With the Review panel closed, selected text is for copying (
Ctrl+Cor the right-click menu). Open the panel and a selection becomes the review tool: create a request for the agent. Improve carries an instruction; Polish (wording only, same meaning), Shorten, Expand, Check (verify a fact, number or equation), Make bold, Make italic and Delete say what to do by themselves, with an optional note. A review keeps the original quote, PDF page and context, stable ID, action, status and resolution in{stem}_review.json(paper.pdf→paper_review.json). Delete means “remove this passage from the source”; saving a request never changes a PDF or source file. Improve carries its instruction incomment. The Review panel shows each request and tracks it through open, applied and resolved. An agent records a successful source edit and build as applied; you verify the recompiled PDF before resolving it.Ctrl+Shift+Rtoggles the panel. − and + in the panel head change the comment size. Esc, the × or Cancel closes the review box without saving, and so does a click outside it while it is still empty. Remove review takes effect at once and shows Undo for ten seconds;Ctrl+Zundoes the last removal too, and the review comes back with the same ID. When the file changes under a review, as it does while an agent works through the list, each open review is found again by its quote and the text around it, even after nearby edits; pdf-next then rewrites that review's lines and context in the sidecar, so the record follows the text. A quote that was itself reworded is marked, with a dashed line, between its old neighbours. Reattach pins a review to other text: select the text and click Reattach, or click Reattach and then select it (Esc cancels). Not available for images. -
Copy the path or the name from the two buttons after zoom: the full path as a person would type it, or just the file name.
-
Reduce a PDF with the toolbar button next to Print. A pure-Rust pass (no Ghostscript) writes
{stem}_reduced.pdfbeside the original when it can shrink the file; the original stays untouched and the smaller copy opens as a tab. -
Changed blocks light up on reload: When a Markdown file rewrites, changed and new paragraphs, headings, list items, tables and equations fade from a highlight over a few seconds. The comparison is by content: adding a paragraph marks only that one. Scroll position is kept.
-
Links go where you would expect. A
#headingor footnote scrolls; a web link, in a markdown file or a PDF, opens in your browser; a relative link —[notes](other.md),[fig](fig1.png), the paper it cites — opens as a new tab, if it is a kind pdf-next shows. Nothing ever navigates the viewer itself. -
Local images in Markdown.
shows an image file on your disk, by a relative or absolute path, up to 64 MiB, and the view reloads when the image changes. Web,data:,file:and network (UNC) sources are not loaded, and neither is a link that resolves to a Windows network share. A network file system mounted as an ordinary folder looks local and is read like one. A refused image shows its alt text in a marked box. -
Follows your system dark mode, and can recolour PDF pages themselves. The mode button cycles Clear → Night → Invert → Sepia, so plain white pages are always one press away; Shift+click it to jump straight back to Clear from anywhere.
-
Text search with match counts, powered by PDF.js.
-
Opens at a page, or at a figure.
--page 12,--find "Figure 3"and--dest resultssay where to land, and the same thing can be written on the path the way a PDF link is —paper.pdf#page=7&search=wake. For Markdown,--line 42or#line=42aim at the first block at or after that line;--findand#search=also work on Markdown. A file already open is aimed rather than opened twice, so a running window jumps to the page you asked for;--no-focushands a file over without raising the window. Theopenedline prints the fragment that was understood (includingline=Nfor Markdown), which is what lets a script — or an agent — check rather than assume. -
Print with
Ctrl+P, to the system's own dialog — the real one, with your printer list, page range, copies, duplex, paper size and scaling. pdf-next adds no print settings of its own: it lays the document out for paper, then hands the window to macOS's print panel, the GTK dialog, or WebView2's print preview, whichever the machine has. A PDF is re-rendered at 150 dpi on white, so what comes out is the page and not the screen — no toolbar, no dark mode, no window chrome — and an A4 paper prints at true size on A4. Markdown reflows across sheets as text, and an image gets a sheet to itself. A document whose pages are not all the same size follows the first one, as it does in every other viewer. If the machine has nothing to print to — no printer, or a stopped print service — it says so rather than opening nothing. -
The title says which build you are running —
paper.pdf — pdf-next 0.16.0— so a bug report can name a version without hunting for an about box. -
Tells you when there is a newer version. A few seconds after launch the app asks GitHub for the latest release, once; if it is newer, the last toolbar button lights up and a press fetches the installer for your system —
.exe,.dmgor.AppImage— in your browser, from the repo's own Releases page. Pressing the button checks again on demand. Nothing polls: that one request at launch is the only network access the app makes on its own, downloads only ever happen on a press, and GitHub is the only host it can reach. -
Figures in Markdown. An image alone on its line is a figure: its alt text becomes the caption, numbered Figure 1, 2, … down the file. Size and place one in the Markdown itself with the attribute syntax Pandoc and Quarto use:
{width=60%},{width=8cm}(alsomm,in,pt,px),{height=300}, and.left,.rightor.center(inside a line of text,.leftand.rightfloat the image). Settings sets the defaults for figures that give no size: natural size, the column width, 75% or 50% of it; no height limit, the window or half of it; centred or left; captions numbered, plain or off. Click a figure to open it full size in a tab of its own, with zoom and the folder walk. -
Settings (the sliders button, or
Ctrl+,) gathers the preferences in one place: the Markdown font, size and column width, how figures are sized, placed and captioned, editing the Markdown file in place, the interface font (System, machine-made Sans or machine-made Weft), how often to check for changes, and About, with the version and the update check. -
Edit Markdown in place. In a rendered Markdown file, select text and press
Ctrl+B(bold) orCtrl+I(italic), or right-click for Underline, Strikethrough, Code and Edit text… (Ctrl+E), which opens the source of the selection, or of its paragraph when the selection crosses a link or formatting, in a small editor. pdf-next writes the.mdfile itself, only if it is still the version on screen, and every edit shows Undo;Ctrl+Zundoes it too. Applying a format to text that already has it removes it. On by default; Settings turns it off. A PDF's source is LaTeX, so for a PDF the same wishes are review requests (Make bold, Make italic). -
Keyboard first:
j/kscroll,n/ppages,g/Gfirst and last,+/-zoom,←/→tabs (or folder, with one file open),Deletemove to trash,Ctrl+Wclose,Ctrl+Ffind,Ctrl+Rreload,Ctrl+Oopen,Ctrl+Pprint,Ctrl+Uraw markdown,Ctrl+Shift+Aask, Enter (with a selection, Review panel open) add a review,Ctrl+Shift+Nadd a review,Escclose the review box without saving,Ctrl+B/Ctrl+I/Ctrl+Ebold, italic or edit a Markdown selection,Ctrl+Zundo a removed review or an edit,Ctrl+Shift+RReview panel,Ctrl+Shift+Ffit window to content,Ctrl+Shift+←/→/↑/↓dock to a screen half,Ctrl+Shift+Enterundock.
| System | Command |
|---|---|
| Windows | winget install RicardoFrantz.pdf-next, or download the .exe installer |
| macOS | brew install --cask machine-made/tap/pdf-next |
| Debian, Ubuntu | add the repository below, then sudo apt install pdf-next |
The cask lives in a personal tap rather than Homebrew's own repository, so the tap is named once. Writing it in full does that for you:
brew install --cask machine-made/tap/pdf-nextOr add the tap first and use the short name from then on:
brew tap machine-made/tap
brew install --cask pdf-nextAfterwards:
brew upgrade --cask pdf-next # to the newest release
brew uninstall --cask pdf-next # and `brew untap machine-made/tap` to forget the tapA workflow in the tap reads this repository's releases once a day, so a new version arrives there within a day of its tag rather than the moment it lands.
pdf-next is in the winget catalog as RicardoFrantz.pdf-next. Each release
reaches winget after Microsoft merges its manifest pull request, so winget can
lag a few days behind the .exe on
Releases.
winget install RicardoFrantz.pdf-next
winget upgrade RicardoFrantz.pdf-next
winget uninstall RicardoFrantz.pdf-nextOne line:
curl -fsSL https://raw.githubusercontent.com/machine-made/pdf-next/main/tools/install-apt.sh | sudo shOr add the signed repository yourself:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://machine-made.github.io/pdf-next/pdf-next.asc \
| sudo tee /etc/apt/keyrings/pdf-next.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/pdf-next.asc] https://machine-made.github.io/pdf-next stable main" \
| sudo tee /etc/apt/sources.list.d/pdf-next.list > /dev/null
sudo apt update && sudo apt install pdf-nextThen sudo apt upgrade pdf-next for a new version and sudo apt remove pdf-next to take it off. Every version from 0.14.3 on stays in the
repository, so sudo apt install pdf-next=0.14.3 still installs that one
after a newer release.
Added the repository from the old address, ricardofrantz.github.io? That
address now returns 404, so apt update reports an error and apt upgrade
finds no new version. Replace the source file once, or run the one-line
installer above again, which rewrites it. This command edits the address in
place:
sudo sed -i 's#ricardofrantz\.github\.io#machine-made.github.io#' /etc/apt/sources.list.d/pdf-next.list && sudo apt updateOr take it from Releases:
.exe or .msi for Windows, a universal .dmg for macOS (Intel and Apple
Silicon in one file), .deb, .rpm or .AppImage (chmod +x and run) for
Linux.
The Windows .exe is one click after SmartScreen: it copies into
%LOCALAPPDATA%\pdf-next (no administrator prompt), shows the progress, and
opens. There is no Next / I Agree / Finish. /W on the command line brings
the old wizard back if you need it; /S stays silent for the Store.
The builds are not code signed yet, so Windows SmartScreen warns once — More
info → Run anyway — and macOS quarantines the app and refuses its first
launch, whether it came from Homebrew or the .dmg. Let that copy through
once:
xattr -dr com.apple.quarantine /Applications/pdf-next.appSigning, and how each of these channels is published, are described in docs/distribution.md; what the app does with the network, in PRIVACY.md.
Then open a file by double-clicking it, dragging it onto the window, pressing Ctrl+O, or
passing a path:
pdf-next paper.pdfpdf-next paper.pdf --night --left # dark pages, filling the left half
pdf-next figure.png --invert # inverted, for a white-background plot
pdf-next thesis.pdf --sepia --poll 3 # warm paper, check for rebuilds every 3s
pdf-next NOTES.md --sepia # markdown as warm paper, reloading as you write
pdf-next paper.pdf supp.pdf fig1.png # three tabs, the first one showing
pdf-next paper.pdf --page 12 # open at page 12
pdf-next paper.pdf --find "Figure 3" # open at the first match, highlighted
pdf-next 'paper.pdf#page=7&search=wake' # the same, written as a link| Flag | Effect |
|---|---|
--left |
Dock to the left half of the screen (implies fit-width). |
--right |
Dock to the right half (implies fit-width). |
--top / --bottom |
Dock to the top or bottom half (implies fit-width). |
--night / --dark |
Dark pages, light text. |
--sepia / --reader |
Warm paper. |
--invert |
Invert the page, for scans and white-background figures. |
--plain / --light |
Original page colors. |
--mode <name> |
Same as the above, by name. |
--page <n> |
Open at that page. |
--find <text> |
Open at the first match, highlighted. |
--dest <name> |
Open at a named destination. |
--no-focus |
Hand the file over without raising the window. |
--poll <seconds> |
Watch interval; 0 turns watching off. |
--wait |
Stay attached to the terminal until the window closes (macOS and Linux — see below). |
--help, --version |
Print and exit. |
Appearance flags style that window only — they do not change your saved default.
Run it again with another file while it is open and the file joins the running window as a new tab — the second command returns at once instead of opening a second viewer.
pdf-next behaves like a command-line tool, so a script — or a coding agent — can call it without guessing:
pdf-next report.pdf # prints "opened /abs/path/report.pdf" and returns at once
pdf-next --help # usage, exit 0
pdf-next missing.pdf # "pdf-next: no such file: missing.pdf", exit 1
pdf-next --bogus # "unknown flag --bogus (try --help)", exit 2Pointing at a place in a document. --page 12, --find "Figure 3" and --dest intro
say where to land; the same thing can be written on the path as a fragment, in the form PDF
links have always used — paper.pdf#page=7, #nameddest=results, #search=Figure%203,
joined with &. Quote it: a shell reads # as the start of a comment. The opened line
prints the fragment that was understood, so a caller can check rather than assume:
pdf-next 'paper.pdf#page=7&search=wake' --no-focus
# opened /abs/path/paper.pdf#page=7&search=wakeA page past the end lands on the last page and says so. A --find with no match leaves the
document where it was, with the find bar showing the query and no results. The three flags
speak about the file named before them, or about the first file when they come first, so
several documents can be aimed at in one command.
If the file is already open in a tab, it is aimed rather than opened a second time — a
running window jumps to the page you asked for. --no-focus hands the file over without
raising the window, which is what you want when a script opens six figures in a row.
Exit status is 0 when the file was launched or handed to the running window, 1 for a
file that does not exist or is not a kind pdf-next can show, 2 for a command line it
could not understand. Relative paths
resolve against the caller's directory, including when they are forwarded to a running
instance. On macOS and Linux the launched process detaches by default (own process group,
so a shell tool's timeout cannot take the window down); pass --wait to hold the terminal,
as a compile loop might want. On Windows a GUI executable never holds the console.
macOS: a .app is not on your PATH. Either use the standard idiom — open -a pdf-next report.pdf — which delivers the file to the app (or the running window) the way Finder does,
or make a one-line shim once:
printf '#!/bin/sh\nexec /Applications/pdf-next.app/Contents/MacOS/pdf-next "$@"\n' \
> /usr/local/bin/pdf-next && chmod +x /usr/local/bin/pdf-nextThen which pdf-next, pdf-next --help and pdf-next report.pdf all work as above. If your
agent has a project instructions file, one line — "open PDFs with pdf-next <file>; use
open -a pdf-next on macOS if the shim is missing" — saves it rediscovering this each time.
The review sidecar is a task list for editing the source. An agent needs three things from it, and each has a short form:
-
Read the open items from
{stem}_review.agent.txt, which pdf-next rewrites after every change to the store, or print the same text withpdf-next reviews brief notes.md. One header line, then one line per open item, in reading order:# notes.md review, revision 12: 3 open, 0 applied, 0 resolved. Close an item: pdf-next reviews done notes.md <id> "<what you changed>", or append "<id> <what you changed>" to notes_review.done r3 L7 improve: move this before the table | "the table below" r1 L12 improve: say this is an ideal value | "the exit speed v ∗ of the gas" r2 L40-42 delete: (no comment) | "the slab burns from both faces at once, so the burning area stays flat until the two fronts meet near the middle of the…"L12andL40-42are source lines of the.md(p7is a PDF page), kept current: while pdf-next shows the file it finds each review again after edits and records where it now is. The quote is shortened and only helps find the place; read the exact text from the file. -
Close an item with
pdf-next reviews done notes.md r1 "stated the ideal-gas assumption", which marks itappliedwith that text as its resolution. For a PDF, add the build command that succeeded:--built "latexmk -pdf paper.tex". An agent that can only write files appends the same line,r1 stated the ideal-gas assumption, to{stem}_review.done; pdf-next applies it while it has the file open, and everyreviewscommand applies it first. The inbox is only read, never rewritten; applying it twice changes nothing, the last line for an ID wins, and an item you reopen stays open until a new line for it arrives. -
Read one whole item from the store itself.
{stem}_review.jsonholds one item per line, sogrep '"id":"r12"' notes_review.jsonprints all of it, and every item has every key (commentandresolutionare""when empty).
Agents never write the JSON store directly: a full rewrite races with the reader adding items in
the app. done, the inbox and update take the store's lock.
The applied items wait for the reader, who checks the result (the recompiled PDF, or the
Markdown) and marks them resolved, or reopens them.
For a PDF, find the quote in the LaTeX entry point or its included chapter files. Use its quote,
surrounding context and PDF position together; an optional SyncTeX source hint is a candidate
to check against the actual source. Edit the .tex, compile, and close the item only after the
build succeeds. Leave failed builds open.
For other fields, pdf-next reviews update patches one item.
For example, save this object in patch.json after checking the current revision:
{
"status": "applied",
"resolution": "Reworded the paragraph and compiled paper.tex successfully.",
"source": { "file": "chapters/method.tex" },
"build": { "success": true, "command": "latexmk -pdf paper.tex" }
}Then update only that record:
pdf-next reviews list paper.pdf
pdf-next reviews update paper.pdf --id r12 --patch patch.json --expected-revision 8A record removed by mistake goes back with restore, under its own ID, from the record exactly
as list printed it: pdf-next reviews restore paper.pdf --record r12.json --expected-revision 9.
Use the revision printed by list for --expected-revision. A concurrent update causes a
conflict; reread the store and prepare the patch again instead of overwriting it. --dry-run
checks a patch and prints the proposed JSON without saving it. The source field uses the
key file for the path actually edited. A SyncTeX hint in that field carries
method: "synctex" and verified: false; replace it with the source you verified.
Updates preserve unrelated fields and records. Each supplied field replaces its
previous value; include the complete object when replacing anchor, source, or
build so its other keys survive. The sidecar format and a complete record example are in
docs/_review.json.
Use the CLI mutation command instead of editing the JSON directly. A raw writer must preserve IDs and unknown fields, write atomically, and check the sidecar revision; writers that skip that protocol can overwrite concurrent changes.
The action field says what the reader wants: improve (the instruction is in comment),
polish (better wording, same meaning), shorten, expand, check (verify the fact, number or
equation and fix it only if wrong), bold, italic (format the passage in the source) and
delete (remove the passage). For the actions other than improve, comment is an optional note.
The PDF quote is a rendered-text target, not a .tex line number. LaTeX commands, macros,
hyphenation, equations and included files can change how text appears. Check the surrounding
source before editing; an equation symbol needs its equation context and position, so do not
copy a symbol in isolation. Record the source file that actually changed. A disappeared quote
does not prove that a Delete request succeeded, and completing a task never removes its record.
Do not delete records from the sidecar to mark them done: set applied. While pdf-next shows the
file, it keeps the open reviews pinned to their text as you edit, and list then prints their
current lines.
The threat model is the obvious one: you open a PDF someone sent you, and the attacker controls every byte of it.
- The window cannot navigate away from the app. A link annotation in a malicious PDF used to
be able to replace the entire viewer with an attacker-controlled page — in a window with no
address bar — and a link pointing back at the asset protocol would be served as HTML at a
local origin, which Tauri trusts with IPC. A navigation guard now rejects anything that is
not the app's own origin. A web link in a PDF (
http,https) opens in your system browser, never in the window;mailtoand other schemes do nothing; internal ones, like a table of contents, scroll. - The webview can read the files you opened, and the local images they show. Document
bytes are served by a purpose-built
doc://protocol whose handler checks every request against the set of files you opened this session and the local image files that an opened Markdown file shows — an allowlist in our own Rust code, populated only by the open path and the Markdown renderer. Review commands also access the adjacent review sidecar and query an optional local SyncTeX mapping; source hints must point to.texfiles inside the PDF folder. - The one file the app writes is a Markdown file you opened, and only when you edit it. Direct
edits go through one Rust command that accepts only a
.mdfrom the set you opened, refuses the write unless the file's digest is still the one on screen and the text at the edit is still the text you selected, and replaces the file atomically with its permissions kept. A PDF is never written. - Markdown is sanitized before it exists as HTML. A
.mdis parsed and scrubbed by ammonia on the Rust side; scripts, event handlers andjavascript:URLs never cross into the window, only local image files are loaded (never web or network paths), and the CSP forbids inline script besides. - One network host, on request only. The CSP names
api.github.comand nothing else, so the update check can ask for the latest release when you press the button — and an injected script has nowhere else to talk to. The download itself is opened in your browser through a Rust command that accepts only URLs under this project's Releases; the webview cannot ask the OS to open anything else. - Two permissions total — listen and unlisten for events. Everything else, including path resolution and the file dialog, is driven from Rust where the frontend cannot reach it.
- No
eval, no WASM execution, no plugins, no framing, and PDF scripting is off, so a document cannot execute anything on its own. - Generic PDF stream decoding is capped at 64 MiB per stream and 256 MiB of cumulative allocations per document, including intermediate filter layers and decompression copies. Software decoders enforce these limits before allocating output; the native decompression fast path is disabled because it cannot enforce a byte quota. Image filters must be the final decoding layer. A document that exceeds either limit fails decoding; opening it again starts a fresh budget.
Numbers from Windows 11, WebView2 151, whole process tree, the 15-page Attention Is All You Need paper:
| Working set | Private bytes | |
|---|---|---|
| Window open, no document | 376 MB | 189 MB |
| 15-page PDF open | 431 MB | 257 MB |
Be clear about what this means. The document costs about 55–70 MB; everything else is the WebView2 (Chromium) floor, which is the same floor every Chromium-based app pays and is partly shared memory counted against every process using it. The binary is ~5 MB and starts instantly, but "low memory" here means lower than Electron, not lower than a native viewer — Electron would ship its own copy of Chromium on top of this. A native renderer such as pdfium would be dramatically lighter and is the honest alternative if resident memory is the only thing you care about.
Linux (WebKitGTK) and macOS (WKWebView) use different engines with different, usually smaller, floors — those are not measured yet.
Reloading used to leak badly. Each reload dropped the previous PDF.js document without destroying it, which kept its worker thread, font and image caches and rendered canvases alive: 77 MB per reload, unbounded, so an afternoon of LaTeX would reach several gigabytes. Now the document is torn down, one worker is shared for the life of the process instead of one spawned per reload, page views free their canvases instead of waiting for the collector, and reloads are skipped entirely while the window is hidden.
What remains scales with the document, not with the number of reloads. The ~10 MB-per-reload
growth that used to point at the webview caching each cache-busted URL is gone: documents are
served from a purpose-built doc:// protocol with Cache-Control: no-store, so no revision
ever enters the HTTP cache, and the asset protocol has been dropped entirely. On top of that,
hiding the window now also releases the PDF.js font and image caches (they rebuild lazily when
you come back), and a background tab holds neither a decoded image nor a rendered markdown DOM.
Needs Rust and, on Windows, the MSVC build tools:
bun install
bun run dev # dev window with reload
bun run build # installers into src-tauri/target/release/bundleOn Ubuntu, install the webview dependencies first:
sudo apt-get install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-devcargo test --manifest-path src-tauri/Cargo.toml # the Rust side
node tools/check_frontend.mjs # the frontend contracts
bun run build && node tools/smoke.mjs # open every fixture, for realcheck_frontend.mjs enforces the invariants that are easy to break silently: a real PDF.js
worker, streaming instead of whole-file reads, the canvas budget, the one-second poll, and the
mid-build gap behaviour.
tools/smoke.mjs launches the binary that was just built, once per file in tests/fixtures,
with PDF_NEXT_SMOKE=1 in the environment. In that mode the app answers for itself: it waits
for a page to be drawn with pixels in it, then reports through a Tauri command and exits on
that answer, so a window that opens and stays empty fails the run instead of passing it.
PDF_NEXT_SMOKE_TIMEOUT bounds a launch that never reports; PDF_NEXT_BIN names a binary
somewhere else.
The fixtures are built by python3 tests/make_fixtures.py, which writes the same bytes on
every machine, so a change to them is a change someone made.
All three run on Windows, macOS and Linux for every push, and a release stays a draft until each platform's bundle has opened a document.
Every part pdf-next is made of, what it does here, and its licence. Versions are the ones this
release ships; the frontend libraries are vendored under src/vendor/, each with a VERSION
file that records its source and checksum.
The app shell (Rust)
| Part | Version | What it does in pdf-next | Licence |
|---|---|---|---|
| Rust | 1.96 or newer | The native side: files, watching, reviews, Markdown, PDF reduction, printing. | MIT / Apache-2.0 |
| Tauri | 2.11.5 | The window, the bridge between Rust and the page, bundles and installers. | MIT / Apache-2.0 |
| wry and tao | 0.55 / 0.35 | Tauri's webview and window layers: WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux. | MIT / Apache-2.0 |
| tauri-plugin-dialog | 2.7.2 | The system Open dialog. | MIT / Apache-2.0 |
| tauri-plugin-single-instance | 2.4.3 | A second pdf-next file.pdf opens a tab in the running window. |
MIT / Apache-2.0 |
| serde, serde_json | 1.0 | Reads and writes the review sidecar and every message to the page. | MIT / Apache-2.0 |
| sha2 | 0.10.9 | The sha256: digests that tell one version of a file from another. |
MIT / Apache-2.0 |
| tempfile | 3.27 | Atomic writes: the sidecar, a reduced PDF, a Markdown edit. | MIT / Apache-2.0 |
| cap-std | 3.4.6 | Keeps review writes inside the folder of the document that was opened. | Apache-2.0 / MIT |
| open | 5.4.1 | Hands a checked web link to the default browser. | MIT |
| percent-encoding | 2.3.2 | Encodes local paths for the document protocol. | MIT / Apache-2.0 |
| gio (Linux), objc2 (macOS), windows-sys (Windows) | 0.18 / 0.6 / 0.61 | Move to trash, the macOS drag pasteboard, the Windows console and print checks. | MIT (/ Apache-2.0) |
Markdown and TeX
| Part | Version | What it does in pdf-next | Licence |
|---|---|---|---|
| pulldown-cmark | 0.13.4 | Parses Markdown: CommonMark plus tables, task lists, footnotes, strikethrough, smart punctuation and $…$ math. |
MIT |
| ammonia | 4.1.4 | Sanitizes the HTML before it reaches the page: no scripts, no event handlers. | MIT / Apache-2.0 |
| math-core | 0.9.0 (pinned) | Turns TeX equations into MathML Core, drawn by the webview itself. | MIT |
| New Computer Modern Math | in vendor/math-core |
The equation font. | GUST Font License |
| Latin Modern Roman | in vendor/latin-modern |
The default Markdown text font, the TeX look. | GUST Font License |
| highlight.js | 11.12.0 | Colours code blocks: Bash, C, C++, CMake, diff, Fortran, INI, JavaScript, JSON, Julia, LaTeX, Makefile, MATLAB, Python, Rust, shell, YAML. | BSD 3-Clause |
| machine-made type | 0.1.0 | Weft, Sans and Heddle: optional Markdown and interface fonts. | SIL OFL 1.1 |
| Part | Version | What it does in pdf-next | Licence |
|---|---|---|---|
PDF.js (pdfjs-dist) |
6.3.289 | Parses and renders PDFs in a worker, with find and the text layer for reviews. Patched (see vendor/PDFJS_VERSION): smaller page cache, sharper zoomed crops, bounded stream decoding. |
Apache-2.0 |
| lopdf | 0.44 | Reads PDF structure on the Rust side, with limits on stream decompression. | MIT |
| picamatl | 0.4.4 | Reduce PDF: writes a smaller {name}_reduced.pdf in pure Rust, no Ghostscript. |
MIT / Apache-2.0 |
| SyncTeX | from your TeX install | When a .synctex.gz sits next to a PDF and the synctex program from your TeX distribution is on the PATH, a review can carry a hint of the .tex line. pdf-next ships no SyncTeX of its own; without it the hint is simply left out. |
(TeX distribution) |
Building and checking
| Part | What it does | Licence |
|---|---|---|
| Tauri CLI 2.11.4, run with Bun | Builds the app and its installers. | MIT / Apache-2.0 |
| NSIS (Windows), DMG (macOS), deb, rpm and AppImage (Linux) | The installers Tauri makes. | zlib / various |
| Node.js | Runs tools/check_frontend.mjs and the smoke test. |
MIT |
The page itself is plain JavaScript modules, HTML and CSS: no framework and no build step.
A viewer first. No annotations, no forms, no cloud. It opens a file, keeps it current, and gets out of the way. The one exception is small, deliberate edits to a Markdown file you are reading (bold, italic, a reworded sentence), each one checked against the file on disk and undoable.
pdf-next is made by machine-made, a lab for software built with AI agents. MIT licensed. Built on Mozilla PDF.js and Tauri.
Windows builds are to be signed through the SignPath Foundation: free code signing provided by SignPath.io, certificate by SignPath Foundation. Roles, the build that is signed and the privacy statement are in docs/code-signing.md. Until the signing is in place, releases are unsigned and every Windows installer is scanned with Microsoft Defender before it is published.
This software is provided "as is", without warranty of any kind. To the extent permitted by law, the authors and contributors are not liable for any damage, loss or claim arising from its use or misuse. You are responsible for how you use it and for following the laws and rules that apply to you. The full terms are in LICENSE.
pdf-next is not affiliated with or endorsed by Mozilla. PDF.js is © Mozilla and contributors, under the Apache License 2.0.
The machine-made Weft, Sans and Heddle fonts come from machine-made/type under the SIL Open Font License 1.1; their licences are in src/vendor/machine-made-type/.
Markdown code colours come from highlight.js 11.12.0 (BSD 3-Clause, © Ivan Sagalaev and contributors); its licence is in src/vendor/highlightjs/LICENSE.