Skip to content
beleonPublic

About

Non-destructive PDF viewer/editor

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

pdfed

A desktop PDF editor that never writes to the file you opened.

Every edit — page order, rotations, notes, drawings, images, redactions — is kept in a small JSON file beside the document and replayed when you reopen it. The original stays byte-for-byte intact, and a real PDF is produced only when you export.

PDF Editor: a report page with a highlight, a circled figure, an arrow, a bold-styled note and a signature

Why pdfed

  • Non-destructive by construction, not by habit. Most editors save into the PDF and rely on you keeping a copy. pdfed never opens the original for writing: edits are a replayable file beside it, and the PDF you send is always a separate export. You can undo a decision from last week.
  • Redaction removes pixels. A viewer's "redact" is usually a black rectangle the reader can move aside. Here the tool rasterizes the page and replaces the pixels, and the app tells you where un-redacted content still lives (the original, the edits file) so you know what not to send.
  • Notes export exactly as shown. One layout engine feeds both the screen and the PDF, with the fonts embedded, so line breaks, bold runs, symbols and emoji are identical in every viewer rather than relaid by each one.
  • Exports carry no active content. Link actions, embedded scripts, form fields and file attachments from the source document are stripped on export, so a PDF you forward under your name cannot carry its sender's payload.
  • One tool for the whole job. Rearranging pages, pulling pages from other PDFs and scans, marking up, signing and redacting are usually three programs on Linux.
  • Self-contained. The deb, rpm and Flatpak bring their own Python and Qt and touch nothing your distribution ships.

What it deliberately is not: a word processor for the text already in a PDF, or a form filler. It adds to a page and rearranges pages; it does not rewrite the existing content.

Three things follow from the design:

  • The sidecar file does not travel with the PDF, so a document you email arrives unedited. Export first.
  • Cropping stays reversible while you work, so the sidecar keeps the full image behind every crop. The exported PDF does not.
  • Redaction removes content only from the file you export. Your original PDF is never modified, so it still holds whatever you painted over. For a page you made from an image, your edits file holds it too, and for a page you inserted from another PDF it stays in that PDF. Send the export, and treat every file it was built from as you would the original. SECURITY.md in the source tree lists every place un-redacted content can remain.

Linux is the supported platform. Built on PySide6 (Qt 6), PyMuPDF and pikepdf; OCR through ocrmypdf where it is installed. AGPL-3.0-or-later — see LICENSE and licenses/ for the bundled components' terms.

Where your edits live

Every edit lives in a sidecar file, <name>.pdf.pdfed.json, next to the PDF (or under ~/.local/share/pdfed/ when that folder is not writable), autosaved continuously and restored the next time you open the file.

The app deletes a sidecar only when you undo back to the original. When it has to move one aside — the PDF changed underneath it, it could not be read, or you chose "start over" — the old one stays beside it, renamed with a reason and a timestamp: .bak-20260907T101500-ab12 (set aside by the app) or .discarded-… (you were asked and said no). To restore one, rename it over the live .pdfed.json name. The app never deletes these copies, so remember them when you clean up a folder or want a document's history really gone.

Features

Reading. Continuous scroll, zoom and fit-to-width/page, an outline sidebar, clickable links, search (Ctrl+F), text selection and copy. Encrypted PDFs, printing, drag and drop. A command palette (Ctrl+Shift+P) runs any menu action by name.

Pages. Drag thumbnails to rearrange; delete, rotate and duplicate pages; insert blank pages, pages from another PDF, or images as pages (with a chosen paper size and compression) — even with no PDF open, so you can build a document from scans. Rescale pages to one size; the app warns when a document mixes sizes.

Marking up. Rectangles, ellipses, lines and arrows (plain, arrow, double-headed, dashed, dotted; click to chain a polyline), freehand drawing, and text highlights anchored to the real text. Select, move and resize with grips; arrange front-to-back; nudge with the arrow keys; full undo.

Text notes, typed on the page. You type directly on the page, in the note's own font. The editor and the exported PDF use the same embedded fonts and the same line layout, so what you see while typing is what the export contains — line breaks, positions and all. Select with the mouse or Shift+arrows; Ctrl+B and Ctrl+I make the selection, or what you type next, bold or italic in the matching Noto Sans cuts. Symbols and monochrome emoji (✓ → ∑ 😀) come from bundled Noto faces, so they look the same everywhere and stay searchable. Esc or a click elsewhere finishes a note; double-click one to edit it again; resizing sets the wrap width and the height follows.

Images. Place a signature, stamp or screenshot (Ctrl+Shift+I, or paste with Ctrl+V), with PNG transparency kept. Scale with the blue grips, crop with the orange corner brackets; cropping is reversible while you edit, but the export contains only the visible pixels.

Redaction, for real. Draw a black box (B) on an image or rasterized page and the pixels are replaced in the export — nothing under the box can be extracted. On a normal page the tool offers to rasterize first. "Draw into page pixels" (P) makes any drawing tool permanent in the same way, and thumbnail badges (IMG, IMG*, +N) show what each page is made of, so a vector box on top is never mistaken for a redaction.

Export. Any printer-style page selection (1-3,7, duplicates and reordering allowed) as a standalone PDF, with overlays written as real PDF annotations (editable in other viewers) or flattened into the page. Notes and images are always real page content, with their fonts embedded and subset, so every viewer shows exactly what you saw. Split a selection into numbered files (N pages each, optionally skipping separator sheets). The outline is remapped to the exported pages.

OCR. Create a searchable copy of the original with ocrmypdf. It reads the original file, so the copy contains none of your edits — export first if you want a searchable version of the edited document.

Install

Debian / Ubuntu — sudo apt install ./pdfed_<version>_<arch>.deb Fedora / RHEL — sudo dnf install ./pdfed-<version>-1.<arch>.rpm Flatpak — build and install it from the manifest (see Linux packages below), then run it from your application menu or with flatpak run io.github.beleon.pdfed. No prebuilt .flatpak bundle is published yet. Any distribution, nothing installed — the AppImage: chmod +x pdfed-<version>-x86_64.AppImage && ./pdfed-<version>-x86_64.AppImage. Same application, same edits file beside your PDF; it just does not add a menu entry or an "Open with" association. If your system lacks FUSE, run it with --appimage-extract-and-run.

The deb and rpm bundle their own Python and Qt, so they do not use — or disturb — the versions your distribution ships. Launch from the application menu, or run pdfed (optionally with a filename).

Upgrading from a version before 1.19.0? The rename to pdfed left the old desktop entry behind under its previous name, so some machines show two PDF Editor items, one without an icon. The app does not delete it for you. How to remove it depends on how the old version was installed:

  • deb or rpm: the stray entry belongs to the old package, which the new one does not replace. Remove that package: sudo apt remove pdf-editor or sudo dnf remove pdf-editor.
  • the install script: rm "${XDG_DATA_HOME:-$HOME/.local/share}/applications/pdf-editor.desktop"

Running from a checkout

uv sync
uv run pdfed [file.pdf]

Linux packages (deb / rpm / AppImage / Flatpak)

packaging/ builds proper Linux packages for x86_64 and aarch64; all of them bundle their own Python and Qt (PySide6 essentials), so the only system requirements are a handful of X11/GL libraries the packages declare themselves:

packaging/build-packages.sh          # dist/: deb + rpm for amd64 + arm64 (needs uv + nfpm)
packaging/build-appimage.sh          # dist/: AppImage for x86_64 + aarch64 (needs mksquashfs)
packaging/test-packages.sh           # clean-room install tests via podman
# Flatpak. Once, to get the builder and the runtime the manifest builds against
# (the builder ships AS a flatpak; `command -v flatpak-builder` finds nothing):
flatpak --user install -y flathub org.freedesktop.Platform//25.08 \
    org.freedesktop.Sdk//25.08 org.flatpak.Builder
cd packaging/flatpak
python3 gen-pip-sources.py           #   refresh pinned wheels after uv.lock changes
flatpak run org.flatpak.Builder --user --install --force-clean \
    build-dir io.github.beleon.pdfed.yml

The arm64 deb/rpm are cross-built (nothing in the tree executes at build time); the glibc floor comes from the PySide6 wheels — 2.34 on x86_64 (Debian 12+), 2.39 on arm64 (Ubuntu 24.04+ / Debian 13+) — and is declared as a package dependency. The Flatpak manifest carries pinned wheels for both arches and builds on whichever kind of host runs it.

The deb/rpm install to /opt/pdfed with a pdfed launcher on PATH, a desktop entry and PDF "Open With" registration. The Flatpak app ID is io.github.beleon.pdfed; it uses --filesystem=home so sidecars can live next to the PDFs (portal-picked paths would forbid that). The grant is wider than that phrase suggests — home plus /run/media, /media, /mnt and read-only xdg-run/gvfs — so the sandbox is a boundary against the network and other apps' data, not against your own files. OCR is the one feature the Flatpak lacks (ocrmypdf stays outside the sandbox); the deb recommends ocrmypdf as an optional dependency.

Installing from source

uv tool install git+https://github.com/beleon/pdfed          # or a release tarball
uv tool install git+https://codefloe.com/beleon/pdfed        # same code, second host

uv tool puts a pdfed command on your PATH and keeps its own Python and Qt. Update by running the same command again; remove with uv tool uninstall pdfed. The two hosts carry the same repository; neither is a mirror of the other. If you install from a tarball, compare its sha256 against docs/releases.md first: the build backend runs during install.

Where things live

sidecar fallback log
Linux $XDG_DATA_HOME/pdfed/sidecars (~/.local/share/…) …/pdfed/pdfed.log
macOS ~/Library/Application Support/pdfed/sidecars same
Windows %APPDATA%\pdfed\sidecars %LOCALAPPDATA%\pdfed\pdfed.log

Sidecars normally live next to the PDF; the fallback is used when that directory is not writable. Unhandled errors are logged to pdfed.log — Help ▸ Open the log folder gets you there, and Help ▸ Copy diagnostics puts the version and engine list on the clipboard for a bug report.

macOS and Windows have never been exercised beyond uv run pdfed from a checkout; the application is cross-platform Python and Qt and should be fine, but nothing there has been tested.

Optional: ocrmypdf for OCR (apt install ocrmypdf / brew install ocrmypdf; on Windows pip install ocrmypdf plus Tesseract and Ghostscript on PATH).

Keyboard shortcuts

Key Action
H / V Pan & links / Select & move
X / G Select text / Highlight text
T R E L D Text note, Rectangle, Ellipse, Line, Draw
B Redact pixels (black-box; edits the page image itself)
P Draw into page pixels (toggle)
Ctrl+F Find
Ctrl+Shift+P Command palette
Ctrl+E Export
Ctrl+G Go to page
Ctrl+wheel, Ctrl+=/- Zoom
Ctrl+1/2/3 Actual size / fit width / fit page
Del Delete selected annotations
Arrow keys Nudge selected objects 1 pt (Shift: 10 pt)
Shift+drag Constrain: square/circle, 45° lines, aspect-locked resize
Ctrl+D Duplicate selected objects
Ctrl+] / Ctrl+[ Bring forward / send backward (Shift: to front / to back)
Ctrl+Shift+I / Ctrl+V Insert image object / paste image
Ctrl+B / Ctrl+I Bold / italic: the selection in a note, what you type next, or a selected note
Shift+arrows, drag, double-click Select text inside a note
Space (hold) Temporarily pan with any tool
Click (line tool) Start a chain; each click adds a vertex
Esc (line chain of 2+ points) Finish the chain — it is kept
Esc (otherwise) Finish the note / cancel the draw / clear selection / back to pan tool
Pinch Zoom (trackpads)

Known limitations

  • You cannot edit the text that is already in the PDF. The app adds to a page and rearranges pages; it does not rewrite the existing content. To change wording, cover it and type a note on top, or redact it
  • Redacting turns that page into an image, so the rest of that page's text stops being selectable and searchable, and its links stop working. That is what makes the covered content actually gone rather than hidden. Only the pages you redact are affected — form fields, by contrast, are removed from every page on export whether you redact it or not
  • Text highlighting needs a text layer; on scanned pages run OCR first or use the rectangle tool
  • Internal links that point to deleted pages stop working in exports; AcroForm form fields do not survive export: they are removed from every page, in both export modes, along with the document's form dictionary. They are not editable in the app either, and a form field can carry actions that run when a file is opened
  • One freehand stroke per annotation; highlights cannot be moved or resized (they are anchored to text)
  • Pages inserted from another PDF reference that file by path — keep it around until you export. The reference is not content-checked: if that file is replaced between sessions, the new content is shown under your existing annotations, with no warning
  • Rescaled pages keep their extractable text but lose links and form fields (the page is embedded as a scaled object on export). Annotations the page already carried are baked into it, so they show but are no longer editable in other viewers
  • Untitled documents (built from images without a base PDF) have no sidecar — their edits live in memory until you export

Development

QT_QPA_PLATFORM=offscreen uv run python tests/smoke_test.py

Architecture (all under src/pdfed/): model.py holds the virtual page list + annotations over read-only PyMuPDF documents; commands.py wraps every mutation in a QUndoCommand; sidecar.py persists state; fonts.py is the one text layout both the canvas and the export use; render.py is a single background render thread (PyMuPDF is not thread-safe — all fitz access holds model.lock); export.py bakes virtual state into a new PDF (geometry stored in display space is mapped through page.derotation_matrix); ui/ contains the Qt widgets.

How it was built

pdfed was written with Claude Code, Anthropic's command-line coding agent, working from Leon's design and direction: the code, the tests, the reviews that shaped it and this README were produced in those sessions. Decisions about what the program should and should not do are Leon's.

About

Non-destructive PDF viewer/editor

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages