A lightweight client-side PDF viewer and annotation tool built with React, PDF.js, pdf-lib and Lucide icons.
The whole project, except this first section of the readme, has been written by Codex ✨
PDF Annotator opens local PDFs, displays them crisply, and saves interoperable annotations back into the PDF. Editable annotations include text highlights, freehand ink, freehand highlights, text annotations, sticky notes and image stamps. Other annotation types from external tools are preserved and shown read-only where PDF.js can render them.
It also supports page add/delete/rotate/merge, blank/lined/Cornell templates, printing, Save, Save As and Download copy.
The app is client-side. This project does not upload PDFs, filenames, annotations or passwords. Browser file handles are limited to user-selected files and kept in memory for the current session. Writes are serialised across app windows, checked for external changes and verified byte-for-byte after saving. PDF scripting and XFA are disabled, the offline cache contains only static app assets, and external PDF links are confirmed before opening.
docker compose upOpen http://127.0.0.1:5173/.
The Docker dev container installs dependencies only when package.json or package-lock.json changes.
Generated files are kept under the ignored out/renderer directory.
Dependency-derived renderer assets are staged under the ignored .generated directory.
The production browser build is an installable PWA with offline app assets. Installed Chrome and Edge desktop apps can register as a PDF file handler: opening a PDF launches the app or adds it to the existing window as a new internal tab. Other browsers retain the normal Open and drag-and-drop flows. User PDF contents are never placed in the offline cache.
src/annotator: reusable single-PDF workspace component.src/tabbedapp: reusable multi-PDF tab shell.src/browserapp: browser/GitHub Pages host wiring.
The reusable layers expose capabilities upward. A button appears only when the host supplies the matching callback or target, for example printTarget, pickMergePdfFile, pickImageFile, saveAsTarget or downloadTarget.
Use this when a host app already owns document selection and wants one PDF viewer/editor.
import { PdfWorkspace, readPdfFile } from './annotator';
const bytes = await readPdfFile(file);
<PdfWorkspace
source={{ bytes, name: file.name, sourceId: file.name }}
onClose={() => setOpen(false)}
onOpenExternalLink={(url) =>
window.open(url, '_blank', 'noopener,noreferrer')
}
theme={{ accent: '#cc41bf' }}
/>;Required props:
source: PDF bytes or a loader, plusnameandsourceId.onClose: called by the workspace close button.
Useful optional props:
confirmDiscardChanges,initialSession,onSessionChangeonOpenExternalLinkpickImageFile,pickMergePdfFile,printTargetallowEditing,readOnlyMessage,allowImageAnnotations,showCloseButtontheme,className,style
Save and download capabilities live on source: saveTarget, saveAsTarget, downloadTarget.
The ref exposes save(), saveAs(), downloadCopy(), print(), snapshot() and releaseRenderResources().
Use this when a host app wants Chrome-style tabs around PdfWorkspace.
import { TabbedPdfShell } from './tabbedapp';
<TabbedPdfShell
fileAdapter={myFileAdapter}
workspaceOptions={{ onOpenExternalLink: openInHostBrowser }}
/>;Required props:
fileAdapter: host file operations and optional capabilities.
Useful optional props:
renderHome: override the built-in Open/New home tab.workspaceOptions: props passed to eachPdfWorkspace.initialDocuments,onDocumentsChangeconfirmCloseDocumentsenableCloseTabShortcut,newTabMenuActions,theme
fileAdapter can provide:
pickPdfDocumentspdfDocumentsFromDrop,pdfDocumentsFromFileInput,fileInputpickImageFile,pickMergePdfFilesaveAsTarget,downloadTarget,printTarget
The ref exposes openDocument(), openDocuments(), openSource(), focusHome(), getDocuments(), closeAllDocuments() and confirmWindowClose().
Individual PdfHostDocument values can set readOnly and readOnlyMessage without changing other tabs.