| name | pdfstudio-core-pdfjs-pdflib-bridge |
| description | Use when modifying PDF loading, saving, or annotation code in open-pdf-studio. Prevents the common mistake of breaking the originalBytesCache bridge between PDF.js (rendering) and pdf-lib (editing), or corrupting coordinate transforms between PDF coordinates, viewport coordinates, and app annotation coordinates. Covers dual-parse architecture, buffer management, color extraction bridge, and the three coordinate systems that interact during annotation save/load. Keywords: PDF.js, pdf-lib, originalBytesCache, annotation coordinates, CropBox, viewport, saver, loader, buffer detachment, dual parse, bridge between viewer and editor, coordinate mismatch, PDF save corrupted.
|
| license | MIT |
| compatibility | Designed for Claude Code. Specific to open-pdf-studio (PDF.js 5.4, pdf-lib 1.17). |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
PDF.js / pdf-lib Bridge
Quick Reference
| Aspect | Detail |
|---|
| PDF.js role | Read-only rendering, text extraction, annotation parsing, form field display |
| pdf-lib role | PDF creation, modification, saving with annotations, form field persistence |
| Bridge mechanism | originalBytesCache (Map<filePath, Uint8Array>) + coordinate transforms |
| PDF.js version | pdfjs-dist 5.4.624 |
| pdf-lib version | 1.17.1 |
| Loader file | js/pdf/loader.js |
| Saver file | js/pdf/saver.js |
Technology Boundary
Side A: PDF.js (pdfjs-dist v5.4): Rendering & Parsing
PDF.js is the read-only side. It handles:
- Page rendering to canvas (
js/pdf/renderer.js)
- Text layer extraction for selection
- Annotation object parsing via
page.getAnnotations()
- Form field display and interaction via
AnnotationStorage
- Link layer for clickable URLs
PDF.js renders annotations internally, but the app disables this (annotationMode: 0) and draws annotations on a separate overlay canvas instead. PDF.js NEVER modifies the PDF bytes.
Limitation: PDF.js does NOT expose all annotation properties. Interior colors (IC), appearance stream colors, and certain opacity values are inaccessible through PDF.js alone.
Side B: pdf-lib (v1.17): Creation & Saving
pdf-lib is the write side. It handles:
- Loading existing PDF bytes for modification
- Creating blank PDF documents
- Writing app annotations as PDF annotation dictionaries
- Persisting form field values from PDF.js
AnnotationStorage
- Applying page rotations
- Stripping and replacing managed annotation types
pdf-lib NEVER renders anything. It operates purely on the PDF object model.
The Bridge: originalBytesCache + Coordinate Transforms
The two libraries NEVER share parsed state. They parse the same raw bytes independently. The bridge consists of:
originalBytesCache — A Map<string, Uint8Array> holding raw PDF bytes. Loader writes bytes in; saver reads bytes out.
- Coordinate transforms — Loading converts PDF coordinates to app coordinates; saving converts back.
- Color extraction — pdf-lib fills gaps where PDF.js cannot expose color data.
- Form field persistence — PDF.js
AnnotationStorage values flow into pdf-lib form fields at save time.
Critical Rules
Buffer Management
- ALWAYS call
.slice() on the Uint8Array BEFORE passing it to pdfjsLib.getDocument(). PDF.js transfers the ArrayBuffer to its web worker via Transferable, which detaches the original buffer (makes it zero-length). Without .slice(), the cached bytes become empty and saving fails silently.
- ALWAYS store the cloned bytes in
originalBytesCache BEFORE calling pdfjsLib.getDocument().
- NEVER assume the
Uint8Array passed to PDF.js remains valid after the getDocument() call.
- ALWAYS update
originalBytesCache after a successful save with the new bytes from pdfDocLib.save().
Dual-Parse Architecture
- ALWAYS treat PDF.js and pdf-lib as fully independent parsers. They have zero shared state.
- NEVER pass PDF.js objects (pages, annotations, refs) to pdf-lib or vice versa.
- ALWAYS use
originalBytesCache as the single bridge for raw bytes between the two libraries.
- ALWAYS load pdf-lib lazily in the background via
getSharedPdfLibDoc() — it is NOT needed for first paint.
Coordinate Transforms
- ALWAYS use CropBox-based transforms when converting between app and PDF coordinates.
- NEVER assume MediaBox equals CropBox. Use
page.getCropBox() from pdf-lib.
- ALWAYS flip the Y-axis when converting: PDF uses bottom-left origin; the app uses top-left origin.
- NEVER apply viewport scale during save — app annotations are stored at scale=1.
Annotation Flow
- ALWAYS strip existing handled annotation subtypes from the page's
/Annots array before writing new ones. The app is the source of truth for managed types.
- NEVER strip annotation types the app does not manage (widgets, links, popups). These MUST be preserved.
- ALWAYS use the
handledSubtypes set to determine which annotations to strip vs. keep.
Color Extraction
- NEVER rely solely on PDF.js for annotation colors. Interior colors (
IC), appearance stream colors, and fill colors require pdf-lib extraction.
- ALWAYS handle the case where pdf-lib is not yet loaded when colors are needed — queue the page in
doc._pagesNeedingColorUpdate and reprocess later.
Form Field Persistence
- ALWAYS read form field values from PDF.js
AnnotationStorage and write them to pdf-lib form fields during save.
- NEVER skip the
fieldNameMap lookup — PDF.js annotation IDs do NOT match pdf-lib field names directly.
Coordinate System Reference
Three Coordinate Systems
1. PDF Coordinates (in the PDF file)
- Origin: bottom-left of CropBox
- Units: points (1/72 inch)
- Y increases upward
- Used by: pdf-lib when reading/writing annotation Rect arrays
2. Viewport Coordinates (PDF.js rendering)
- Origin: top-left of rendered page
- Units: pixels (scaled by viewport scale factor)
- Y increases downward
- Used by: PDF.js page.getViewport(), convertToViewportPoint()
3. App Annotation Coordinates (state.annotations)
- Origin: top-left of page
- Units: pixels at scale=1 (unscaled)
- Y increases downward
- Used by: all annotation objects in doc.annotations[]
Transform Formulas
Loading (PDF to App) — in annotation-converter.js:
const viewport = page.getViewport({ scale: 1 });
const [appX, appY] = viewport.convertToViewportPoint(pdfX, pdfY);
const [x1, y1, x2, y2] = viewport.convertToViewportRectangle(pdfRect);
Saving (App to PDF) — in saver.js:
const cropBox = page.getCropBox();
const viewLeft = cropBox.x;
const viewTop = cropBox.y + cropBox.height;
const convertX = (canvasX) => canvasX + viewLeft;
const convertY = (canvasY) => viewTop - canvasY;
Coordinate Example
For a PDF with CropBox {x: 0, y: 0, width: 612, height: 792}:
- App annotation at
(100, 200) maps to PDF point (100, 592) — because 792 - 200 = 592
- PDF annotation at
(100, 592) maps to app point (100, 200) — because 792 - 592 = 200
Data Flow: Open PDF
File on disk
|
v
readBinaryFile(filePath) <-- Tauri FS plugin
|
v
typedArray = new Uint8Array(data)
|
+---> originalBytesCache.set(filePath, typedArray.slice())
| ^^^ CRITICAL: .slice() clones before PDF.js detaches buffer
|
+---> pdfjsLib.getDocument({ data: typedArray })
| --> doc.pdfDoc (rendering, text layers, annotation parsing)
|
+---> PDFDocument.load(pdfBytes) <-- Background, non-blocking
--> doc._sharedPdfLibDoc (color extraction only)
After first paint, annotations load per-page:
page.getAnnotations() <-- PDF.js: raw annotation objects
+
extractAnnotationColors(pageNum, pdfLibDoc) <-- pdf-lib: fill color gaps
+
extractStampImagesViaPdfJs(page, viewport) <-- PDF.js: render stamp regions
|
v
convertPdfAnnotation() <-- Merge into app model
|
v
doc.annotations.push(converted)
Data Flow: Save PDF
state.annotations (source of truth)
|
v
PDFDocument.load(existingPdfBytes) <-- from originalBytesCache
|
+-- Strip existing handled annotations from each page /Annots array
| (handledSubtypes: Highlight, Square, Circle, Line, Ink, etc.)
| Keep: Widget, Link, Popup, FileAttachment, etc.
|
+-- For each app annotation: convert to PDF annotation dict
| - convertX(canvasX) = canvasX + cropBox.x
| - convertY(canvasY) = (cropBox.y + cropBox.height) - canvasY
| - Build Rect, QuadPoints, AP streams as needed
|
+-- Persist form field values
| PDF.js AnnotationStorage --> pdf-lib form.getField() --> setText/check/select
|
+-- Apply page rotations (combine with existing PDF rotation)
|
v
pdfDocLib.save() --> Uint8Array --> writeBinaryFile()
|
v
originalBytesCache.set(filePath, newBytes) <-- Update cache for next save
Essential Patterns
Pattern 1: Safe Buffer Caching
const typedArray = new Uint8Array(data);
originalBytesCache.set(filePath, typedArray.slice());
const pdfDoc = await pdfjsLib.getDocument({ data: typedArray }).promise;
const pdfDoc = await pdfjsLib.getDocument({ data: typedArray }).promise;
originalBytesCache.set(filePath, typedArray);
Pattern 2: Async Color Fallback Queue
if (pdfLibDoc) {
annotColorMap = await extractAnnotationColors(pageNum, pdfLibDoc);
} else {
doc._pagesNeedingColorUpdate.add(pageNum);
}
for (const pageNum of doc._pagesNeedingColorUpdate) {
doc.annotations = doc.annotations.filter(a => a.page !== pageNum);
}
Pattern 3: Staleness Guard for Async Operations
const loadId = ++doc._annotationLoadId;
if (loadId !== doc._annotationLoadId) return;
if (!state.documents.includes(doc)) return;
ALWAYS check both conditions after every await boundary.
Pattern 4: Form Field Value Round-Trip
const storage = getAnnotationStorage();
const fieldNameMap = getAnnotIdToFieldName();
for (const [annotId, fieldName] of fieldNameMap.entries()) {
const storedValue = storage.getRawValue(annotId);
if (storedValue === undefined) continue;
const field = form.getField(fieldName);
if (field instanceof PDFTextField) {
field.setText(String(storedValue.value));
}
}
Key Files
| File | Role |
|---|
js/pdf/loader.js | PDF loading orchestrator; manages originalBytesCache, PDF.js + pdf-lib instances |
js/pdf/loader/annotation-converter.js | Converts PDF.js annotations to app model (PDF coords to app coords) |
js/pdf/loader/color-extraction.js | Uses pdf-lib to extract colors PDF.js cannot expose |
js/pdf/loader/image-extraction.js | Extracts stamp images via PDF.js rendering fallback |
js/pdf/saver.js | Converts app annotations to PDF dicts via pdf-lib (app coords to PDF coords) |
js/pdf/renderer.js | PDF.js page rendering to canvas; manages text/link/form layers |
js/pdf/form-layer.js | PDF.js AnnotationStorage management for form field values |
js/annotations/rendering.js | Draws app annotations on overlay canvas (Canvas2D, no PDF library) |
Reference Links