Getting started
title: "Overview" description: "Host-agnostic OSS PDF viewer core for React 19. A plugin-driven canvas viewer with overlay, panel, and toolbar slots. AGPL-3.0-or-later." group: "Getting started" order: 1 slug: "overview"
LoupePDF
OSS PDF viewer core. A plugin-driven canvas viewer with overlay, panel, and toolbar slots, built around React 19. Host-agnostic: the viewer never imports a SaaS, never hardcodes a backend route, and self-hides any tool whose backing service the host hasn't wired. AGPL-3.0-or-later.
Install
LoupePDF is published to the public npm registry under the
@printwithsynergy scope.
# stable
npm install @printwithsynergy/loupe-pdf
# pre-release (current)
npm install @printwithsynergy/loupe-pdf@beta
Peer dependencies you provide in your host app:
npm install react react-dom
# Optional — only if you mount AnnotationCanvas / AnnotationThread:
npm install fabric
# Optional — only if you pass a `codex` client for Ghostscript-accurate
# separations / TAC / layers:
npm install @printwithsynergy/codex-client
Requires react@^19 and react-dom@^19. fabric@^7 is an optional peer
used by the annotation components. @printwithsynergy/codex-client@^1.8.1
is an optional peer used by the codex accuracy overlay — hosts that
never pass the codex prop don't need to install it. pdfjs-dist@^4
is a regular dependency — it comes along automatically and powers the
createBrowserViewerServices factory exposed at /browser. The package
ships ESM only.
Quick start — pick your tier
LoupePDF ships five integration levels. Start with Tier 1 and drop down only when you need more control.
Tier 1 — Drop-in production viewer (~3 lines)
<LoupePDF> is the recommended single-component entry point. One
mount, every viewer-only feature wired to pdf.js out of the box:
- Page raster with a multi-DPI tile cache so zoom never degrades the image.
- Color picker — RGB readout plus a per-ink breakdown (CMYK
- any spot inks the PDF declares).
- Densitometer — per-channel coverage and TAC limit.
- TAC heatmap — process CMYK plus every detected spot ink summed and visualised.
- Per-ink separations — toggle CMYK and any spot plates on / off (defaults to all-on like Output Preview).
- Layers — OCG list with per-layer visibility.
- Annotation toolbar / canvas / thread — pen, arrow, rect, ellipse, text, highlight, sticky note, all in-memory.
- Mobile — tools collapse into a left-anchored slide-in drawer; readouts swap to bottom sheets so they stay legible.
import { LoupePDF } from "@printwithsynergy/loupe-pdf";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
export function ProofPage() {
return <LoupePDF pdfUrl="/proofs/abc.pdf" workerSrc={pdfWorkerSrc} />;
}
Hosts with a preflight engine plug findings + dieline + box overlays in directly:
<LoupePDF
pdfUrl="/proofs/abc.pdf"
workerSrc={pdfWorkerSrc}
items={findings} // OverlayItem[] — error / warning / advisory bboxes
selectedItem={selected}
onItemSelect={setSelected}
dieline={dielineForCurrentPage}
showBoxOverlays // trim / bleed / crop popovers
cropToTrim // clip the canvas to TrimBox
tools={["color-picker", "densitometer", "annotate", "tac-heatmap"]}
onPageChange={setCurrentPage}
tokens={{ accent: "#e50c6a" }}
brand="MyApp"
brandLogoUrl="/logo.svg"
/>
No backend required for the viewer side. Server-only features (HTML /
PDF report exports, ICC-correct preflight separations, server-
persisted annotations) self-hide because their dedicated services are
intentionally markUnwired. Hosts with a backend pass services to
override the in-browser ones.
Tier 1b — Demo / showcase viewer
Same component, with an upload bar + drag-drop + URL paste — useful for marketing pages and internal sandboxes where users bring their own files.
import { LoupePDFDemo } from "@printwithsynergy/loupe-pdf";
export function DemoPage() {
return <LoupePDFDemo brand="MyApp" brandLogoUrl="/logo.svg" />;
}
Tier 2 — One-liner viewer (~5 lines)
<LoupePDFViewer> auto-discovers pages, layers, dimensions. Ships
a responsive toolbar with zoom, layers, color picker, and measure.
import { LoupePDFViewer } from "@printwithsynergy/loupe-pdf/components";
export function MyViewer() {
return <LoupePDFViewer pdfUrl="https://cdn.example.com/proof.pdf" />;
}
Slot props (header, sidebar, footer) let you replace regions
without losing the rest of the viewer:
<LoupePDFViewer
pdfUrl={url}
header={(state) => <MyToolbar zoom={state.zoom} setZoom={state.setZoom} />}
footer={<p>Custom footer</p>}
/>
Tier 3 — Hook + Provider (~20 lines)
useLoupePDF() manages all state; <LoupePDFProvider> mounts both
contexts. Build any layout you want on top.
import { useLoupePDF, LoupePDFProvider } from "@printwithsynergy/loupe-pdf/host";
import { PageCanvas } from "@printwithsynergy/loupe-pdf/components";
export function CustomViewer({ url }: { url: string }) {
const viewer = useLoupePDF(url, { tokens: { accent: "#e50c6a" } });
return (
<LoupePDFProvider value={viewer}>
<PageCanvas jobId="demo" page={viewer.currentPageInfo} zoom={viewer.zoom} items={[]} selectedItem={null} onItemClick={() => {}} />
</LoupePDFProvider>
);
}
Tier 4 — Full custom composition
Wire ViewerHostContext + ViewerServicesContext yourself. Every
component (PageCanvas, LayerPanel, MeasureTool, etc.) is
exported and unchanged.
Shareable links
Generate URLs that open the viewer with a specific PDF and settings:
import { generateShareLink, parseShareParams } from "@printwithsynergy/loupe-pdf/host";
const link = generateShareLink({
baseUrl: "https://loupepdf.com/demo",
pdfUrl: "https://cdn.example.com/proof.pdf",
fullscreen: true,
zoom: 150,
});
// On the demo page:
const params = parseShareParams(new URLSearchParams(window.location.search));
// → { pdfUrl: "https://...", fullscreen: true, zoom: 150 }
PDF validation
Client-side checks (magic bytes, MIME, size) are built in:
import { validatePdfFile, validatePdfUrl } from "@printwithsynergy/loupe-pdf/host";
const result = await validatePdfFile(file); // { valid: true } or { valid: false, error: "..." }
Browser-only services (full feature surface, no backend)
createBrowserViewerServices returns a complete ViewerServices
backed by pdf.js — every viewer-only feature works on any PDF the
browser can fetch:
import { createBrowserViewerServices } from "@printwithsynergy/loupe-pdf/browser";
import { ViewerServicesContext, ViewerHostContext } from "@printwithsynergy/loupe-pdf/host";
const services = createBrowserViewerServices({ pdfUrl: "/proof.pdf" });
<ViewerHostContext.Provider value={{ apiBase: "", jobApiBase: "", readOnly: false }}>
<ViewerServicesContext.Provider value={services}>
<PageCanvas ... />
<SeparationCanvas ... />
<TACHeatmapOverlay ... />
{/* etc. */}
</ViewerServicesContext.Provider>
</ViewerHostContext.Provider>;
services.dispose(); // free blob URLs / pdf.js doc on unmount
CMYK / TAC are RGB-derived approximations when no backend is wired.
Spot inks are detected by scanning raw PDF bytes for /Separation
and /DeviceN colour spaces; each detected spot's coverage is
estimated from its alternate-RGB direction. Good for visual
showcase and casual review, not press-grade. For ICC-correct
readings deploy the optional reference server below and pass its
services overrides — the components automatically swap from the
browser approximation to ICC-derived data with no markup change.
Demo
Want to see the hide-on-unwired contract in action without setting up your own
host? demo/ is a tiny Vite app that flips between empty / pdf.js-fallback /
fully-mocked contexts:
cd demo && npm install && npm run dev
See demo/README.md for the smoke-check checklist.
Optional reference server
For press-grade ICC-correct ink separations, densitometer readings,
and TAC heatmap (the browser services use an RGB→CMYK approximation —
fine for showcase, not for prepress sign-off), deploy the small
Node + Ghostscript service in server/. Wire its
endpoints into your ViewerServices and the corresponding components
swap from the browser approximation to ICC-derived data.
cd server && docker build -t loupe-pdf-server .
docker run -p 3000:3000 -v loupe-jobs:/var/lib/loupe-pdf/jobs loupe-pdf-server
See docs/server.md for the HTTP contract, deployment notes, and security caveats.
Documentation
| Topic | Doc |
|---|---|
The one-line <LoupePDFViewer> composition | docs/loupe-pdf-viewer.md |
| How the contexts, components, and plugins fit together | docs/architecture.md |
Wiring ViewerServices (page images, layers, separations, TAC, color, densitometer, annotations, reports) | docs/services.md |
| Capability detection, debug logging, and the in-browser PDF fallback | docs/fallback.md |
| Optional Node + Ghostscript backend for preflight-grade tools | docs/server.md |
| Per-component props and usage | docs/components.md |
Plugin slots, registration, and the replaces mechanism | docs/plugins.md |
Built-in MeasurementUnits + custom-unit Protocol | docs/measurement-units.md |
| Theme tokens, i18n, telemetry, read-only mode | docs/theming.md |
Shareable viewer links (generateShareLink, parseShareParams) | docs/share-links.md |
Client-side PDF validation (validatePdfFile, validatePdfUrl) | docs/validation.md |
| Boundary rule, provenance, contributing | docs/contributing.md |
Community
- CHANGELOG — release notes (Keep-a-Changelog format).
- CONTRIBUTING — quick-start; the full guide is in docs/contributing.md.
- CODE_OF_CONDUCT — Contributor Covenant 2.1.
- SECURITY — vulnerability disclosure process. Don't open public issues for security problems.
License
LoupePDF is licensed under the GNU Affero General Public License v3.0 or
later (AGPL-3.0-or-later). See LICENSE for the full text.
Copyright (C) 2026 Think Neverland LLC.