| name | pdfjs-agents-project-scaffolder |
| description | Use when generating a complete PDF.js project from scratch, scaffolding a new PDF viewer application, or setting up the full rendering pipeline with all layers. Prevents incomplete project setup by ensuring worker, canvas, text layer, and annotation layer are all properly configured. Covers bundler-specific worker config, HTML/CSS templates, TypeScript setup, package.json with correct pdfjs-dist version, and full rendering pipeline. Keywords: scaffold, project setup, boilerplate, PDF viewer template, TypeScript, pdfjs-dist, new PDF viewer, create PDF app, start PDF.js project, build PDF reader.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdfjs-dist 5.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
pdfjs-agents-project-scaffolder
Quick Reference
Scaffolding Pipeline
| Step | Action | Output |
|---|
| 1. Choose setup | Select vanilla CDN / webpack / Vite / Next.js | Build strategy |
| 2. Create package.json | Add pdfjs-dist 5.x dependency | package.json |
| 3. Configure worker | Bundler-specific worker setup | Worker config |
| 4. Create HTML template | Page container, canvas, text layer, annotation layer | index.html |
| 5. Create CSS | Layer stacking, responsive sizing | styles.css |
| 6. Create entry point | Worker setup, rendering, navigation, zoom | main.ts / main.js |
| 7. Configure bundler | webpack.config.js / vite.config.ts | Bundler config |
| 8. Add TypeScript (optional) | tsconfig.json with proper types | tsconfig.json |
Critical Warnings
ALWAYS configure GlobalWorkerOptions.workerSrc BEFORE calling getDocument() -- the worker MUST be initialized first or document loading will fail silently or throw.
ALWAYS copy or reference the worker file correctly for your bundler -- the worker version MUST match the pdfjs-dist version exactly. A version mismatch causes cryptic deserialization errors.
NEVER import pdf.worker.min.mjs directly into your main bundle -- the worker MUST run in a separate thread. Bundling it into the main bundle defeats the purpose and blocks the UI thread during PDF parsing.
ALWAYS include the pdfjs-dist/web/pdf_viewer.css stylesheet -- without it, the text layer and annotation layer will be incorrectly positioned and invisible or misaligned.
NEVER use pdfjs-dist/build/pdf.js (CommonJS) in ESM bundler projects -- ALWAYS use pdfjs-dist/build/pdf.mjs for webpack 5+ and Vite.
ALWAYS handle devicePixelRatio in canvas setup -- without it, PDFs render blurry on Retina and 4K displays.
Decision Tree: Which Setup?
Starting a new PDF.js project?
├── Quick prototype / no bundler?
│ └── Vanilla CDN → See references/examples.md §1
│ - Zero build step
│ - Script tags from CDN (cdnjs or unpkg)
│ - Good for demos and learning
│
├── Production app with legacy support?
│ └── Webpack → See references/examples.md §2
│ - Full control over worker bundling
│ - Copy-webpack-plugin for worker file
│ - Tree-shaking with webpack 5
│
├── Modern production app?
│ └── Vite → See references/examples.md §3
│ - Fastest dev server
│ - Native ESM, uses import.meta.url for worker
│ - Simplest worker configuration
│
└── SSR / React framework?
└── Next.js → See references/examples.md §4
- Dynamic import with ssr: false
- Worker loaded client-side only
- Special handling for server components
Essential Patterns
package.json (All Setups)
{
"dependencies": {
"pdfjs-dist": "^5.0.0"
}
}
ALWAYS pin to major version 5.x. NEVER mix pdfjs-dist versions between the main library and the worker file.
Worker Configuration by Setup
| Setup | Worker Strategy | Worker Path |
|---|
| Vanilla CDN | CDN URL string | https://cdnjs.cloudflare.com/ajax/libs/pdf.js/5.x.x/pdf.worker.min.mjs |
| Webpack | copy-webpack-plugin copies worker to output | new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url) |
| Vite | Native ESM, no plugin needed | new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url) |
| Next.js | CDN or public folder | CDN URL or /pdf.worker.min.mjs from public/ |
Minimal Entry Point (TypeScript)
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
import { TextLayer, AnnotationLayer } from "pdfjs-dist";
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
let pdfDoc: PDFDocumentProxy | null = null;
let currentPage = 1;
let currentScale = 1.5;
let currentRenderTask: RenderTask | null = null;
const container = document.getElementById("page-container") as HTMLDivElement;
const pageNum = .() ;
pageCount = .() ;
(): <> {
pdfDoc = ({ url }).;
pageCount. = (pdfDoc.);
(currentPage);
}
(): <> {
(!pdfDoc) ;
(currentRenderTask) {
currentRenderTask.();
currentRenderTask = ;
}
page = pdfDoc.(num);
viewport = page.({ : currentScale });
dpr = . || ;
container. = ;
container.. = ;
container.. = ;
container.. = ;
canvas = .();
canvas. = .(viewport. * dpr);
canvas. = .(viewport. * dpr);
canvas.. = ;
canvas.. = ;
canvas.. = ;
canvas.. = ;
canvas.. = ;
container.(canvas);
ctx = canvas.()!;
ctx.(dpr, dpr);
currentRenderTask = page.({ : ctx, viewport });
{
currentRenderTask.;
} (: ) {
(err && err. === ) ;
err;
} {
currentRenderTask = ;
}
textDiv = .();
textDiv. = ;
textDiv.. = ;
textDiv.. = ;
textDiv.. = ;
container.(textDiv);
textContent = page.();
textLayer = ({
: textDiv,
: textContent,
viewport,
});
textLayer.();
annotDiv = .();
annotDiv. = ;
annotDiv.. = ;
annotDiv.. = ;
annotDiv.. = ;
container.(annotDiv);
annotations = page.();
annotationLayer = ({
: annotDiv,
annotations,
page,
viewport,
});
annotationLayer.({ viewport, annotations });
pageNum. = (num);
}
(): {
(currentPage <= ) ;
currentPage--;
(currentPage);
}
(): {
(!pdfDoc || currentPage >= pdfDoc.) ;
currentPage++;
(currentPage);
}
(): {
(!pdfDoc || num < || num > pdfDoc.) ;
currentPage = num;
(currentPage);
}
(): {
currentScale *= ;
(currentPage);
}
(): {
currentScale /= ;
(currentPage);
}
HTML Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF.js Viewer</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="toolbar">
<button id="prev-btn">Previous</button>
<span>Page <span id="page-num">1</span> / <span id="page-count">-</span></span>
<button id="next-btn">Next</button>
Go
Zoom In
Zoom Out
CSS Template
@import "pdfjs-dist/web/pdf_viewer.css";
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #525659;
}
#toolbar {
position: sticky;
top: 0;
z-index: 10;
background: #323639;
color: #fff;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
}
#toolbar button {
padding: 4px 12px;
cursor: pointer;
}
#toolbar input {
width: 60px;
padding: 4px;
}
#viewer-container {
display: flex;
justify-content: center;
padding: 20px 0;
overflow: auto;
}
#page-container {
position: relative;
: (, , , );
: ;
}
{
: ;
}
{
: ;
}
File Tree by Setup
Vanilla CDN
project/
├── index.html
├── styles.css
└── main.js
Webpack (TypeScript)
project/
├── package.json
├── tsconfig.json
├── webpack.config.js
├── src/
│ ├── index.html
│ ├── styles.css
│ └── main.ts
└── dist/ (generated)
Vite (TypeScript)
project/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── index.html
├── src/
│ ├── styles.css
│ └── main.ts
└── dist/ (generated)
Next.js
project/
├── package.json
├── tsconfig.json
├── next.config.js
├── public/
│ └── pdf.worker.min.mjs (copied from node_modules)
├── app/
│ ├── layout.tsx
│ └── page.tsx
└── components/
└── PdfViewer.tsx (client component with "use client")
Reference Links
Official Sources