Skip to content

Advanced Features

ABCrimson edited this page Mar 1, 2026 · 5 revisions

Advanced Features

modern-pdf-lib v0.15.1 — Layers, outlines, watermarks, linearization, file attachments, text extraction, incremental saves, and low-level PDF operators.


Layers (Optional Content Groups)

Optional Content Groups (OCGs), commonly called "layers", allow content to be toggled visible or hidden in a PDF viewer. Common use cases include print-only content, language overlays, CAD drawing layers, and conditional watermarks.

Imports

import {
  PdfLayerManager,
  PdfLayer,
  beginLayerContent,
  endLayerContent,
} from 'modern-pdf-lib';

PdfLayerManager

Manages the set of layers for a document and produces the /OCProperties dictionary written into the catalog.

const layers = doc.createLayerManager();   // returns PdfLayerManager

// Add layers (visible by default)
const backgroundLayer = layers.addLayer('Background', true);
const printLayer      = layers.addLayer('Print Only', true);
const draftLayer      = layers.addLayer('Draft Watermark', false); // hidden by default

// Query
layers.getLayer('Background');  // PdfLayer | undefined
layers.getLayers();             // PdfLayer[]
layers.removeLayer('Draft Watermark');
Method Returns Description
addLayer(name, visible?) PdfLayer Create and register a new layer.
getLayer(name) PdfLayer | undefined Look up a layer by name.
getLayers() PdfLayer[] All registered layers.
removeLayer(name) void Remove a layer by name.
toOCProperties(registry) PdfDict Serialize the /OCProperties catalog entry.

PdfLayer

const layer = layers.addLayer('Annotations');

layer.getName();            // 'Annotations'
layer.isVisible();          // true
layer.setVisible(false);    // hide by default
layer.getRef();             // PdfRef | undefined (after serialization)

Drawing Layer Content

import { beginLayerContent, endLayerContent } from 'modern-pdf-lib';

const textLayer = layers.addLayer('Text Content');

// Wrap page operators in layer markers
page.pushOperators(beginLayerContent(textLayer));
page.drawText('Only visible when "Text Content" layer is on', {
  x: 72, y: 700, size: 12,
});
page.pushOperators(endLayerContent());

The operators produced are:

/OC /Text_Content BDC     ← beginLayerContent(textLayer)
... drawing operators ...
EMC                        ← endLayerContent()

Outlines and Bookmarks

The PDF outline (bookmarks panel) provides hierarchical document navigation. Each item has a title, a destination, and optional visual styling.

Imports

import {
  PdfOutlineTree,
  PdfOutlineItem,
} from 'modern-pdf-lib';

import type {
  OutlineDestination,
  OutlineItemOptions,
} from 'modern-pdf-lib';

Building an Outline

const outline = doc.createOutlineTree();   // returns PdfOutlineTree

// Top-level item — navigate to page 0, fit whole page
const ch1 = outline.addItem('Chapter 1', {
  type:      'page',
  pageIndex: 0,
  fit:       'Fit',
});

// Nested item — navigate to a specific vertical position
ch1.addChild('Section 1.1', {
  type:      'page',
  pageIndex: 1,
  fit:       'FitH',
  top:       700,
});

ch1.addChild('Section 1.2', {
  type:      'page',
  pageIndex: 3,
  fit:       'XYZ',
  left:      72,
  top:       750,
  zoom:      1.5,
}, {
  bold:   true,
  italic: false,
  color:  { r: 0.2, g: 0.4, b: 0.8 },
});

// Named destination
outline.addItem('Appendix', {
  type:             'named',
  namedDestination: 'appendix-start',
});

PdfOutlineTree API

Method Returns Description
addItem(title, dest, opts?) PdfOutlineItem Add a top-level bookmark.
removeItem(item) void Remove a top-level bookmark.
items PdfOutlineItem[] Direct access to the items array.
toDict(registry, pageRefs) PdfRef Serialize to a PDF /Outlines dict.
PdfOutlineTree.fromDict(dict, resolver, pageRefMap) PdfOutlineTree Parse from an existing PDF.

PdfOutlineItem API

Property / Method Type Description
title string Displayed bookmark label.
destination OutlineDestination Navigation target.
children PdfOutlineItem[] Child items.
isOpen boolean Whether children are expanded by default.
color { r, g, b } | undefined Text colour (0–1 range).
bold boolean | undefined Bold text style.
italic boolean | undefined Italic text style.
addChild(title, dest, opts?) PdfOutlineItem Append a child bookmark.
removeChild(item) void Remove a direct child.

Destination Fit Modes

fit Parameters Viewport
'Fit' Fit page in window
'FitH' top Fit page width; scroll to top
'FitV' left Fit page height; scroll to left
'FitB' Fit bounding box of page content
'FitBH' top Fit bounding box width; scroll to top
'FitBV' left Fit bounding box height; scroll to left
'XYZ' left, top, zoom Position at (left, top) with zoom level

Watermarks

Add semi-transparent text watermarks to pages using the standard Helvetica font (no embedding required for watermarks).

Imports

import {
  addWatermark,
  addWatermarkToPage,
} from 'modern-pdf-lib';

import type { WatermarkOptions } from 'modern-pdf-lib';

addWatermark — All Pages

await addWatermark(doc, {
  text:     'CONFIDENTIAL',
  fontSize: 60,
  color:    { r: 0.8, g: 0.1, b: 0.1 },
  opacity:  0.25,
  rotation: 45,
  position: 'center',   // 'center' | 'top' | 'bottom'
});

addWatermarkToPage — Single Page

const registry = doc.getRegistry();

addWatermarkToPage(page, {
  text:     'DRAFT',
  fontSize: 80,
  color:    { r: 0.7, g: 0.7, b: 0.7 },
  opacity:  0.3,
  rotation: 45,
}, registry);

WatermarkOptions

Option Type Default Description
text string required Watermark text.
fontSize number 60 Font size in points.
color { r, g, b } Light gray RGB fill colour.
opacity number 0.3 Transparency (0 = invisible, 1 = opaque).
rotation number 45 Rotation angle in degrees.
position 'center' | 'top' | 'bottom' 'center' Vertical placement.

Linearization

Linearized (or "fast web view") PDFs are structured so that a browser can begin displaying the first page before the entire file has been downloaded.

import { linearizePdf } from 'modern-pdf-lib';

const pdfBytes     = await doc.save();
const linearBytes  = await linearizePdf(pdfBytes);

// linearBytes is a Uint8Array suitable for streaming
await writeFile('output-linear.pdf', linearBytes);

Linearization rewrites the cross-reference table and reorders objects so that first-page objects appear at the beginning of the file. The resulting PDF remains fully compatible with all PDF viewers.


File Attachments

Embed arbitrary files within a PDF document. Embedded files are stored as /EmbeddedFile streams referenced from the catalog's /Names /EmbeddedFiles name tree.

Imports

import {
  attachFile,
  getAttachments,
  removeAttachment,
} from 'modern-pdf-lib';

import type { EmbeddedFile } from 'modern-pdf-lib';

Attaching a File

const csvData = new TextEncoder().encode('name,value\nfoo,1\nbar,2');

doc.attachFile({
  name:             'data.csv',
  data:             csvData,
  mimeType:         'text/csv',
  description:      'Source data for Figure 3',
  creationDate:     new Date('2025-01-01'),
  modificationDate: new Date(),
});

Retrieving Attachments

const files = doc.getAttachments();
// files: EmbeddedFile[]

for (const file of files) {
  console.log(file.name, file.mimeType, file.data.byteLength);
}

EmbeddedFile Interface

interface EmbeddedFile {
  name:              string;
  data:              Uint8Array;
  mimeType:          string;
  description?:      string;
  creationDate?:     Date;
  modificationDate?: Date;
}

Note: PDF/A-1 and PDF/A-2 prohibit embedded file attachments. Use PDF/A-3 if you need to embed files in an archival document.


Text Extraction

Extract text content from parsed content streams, with optional position tracking.

Imports

import {
  extractText,
  extractTextWithPositions,
} from 'modern-pdf-lib';

import type { TextItem } from 'modern-pdf-lib';

Plain Text Extraction

const operators = page.parseContentStream();
const resources = page.getResources();

const text = extractText(operators, resources);
console.log(text);
// "Hello, world!\nThis is the second line."

Position-Aware Extraction

const items: TextItem[] = extractTextWithPositions(operators, resources);

for (const item of items) {
  console.log(
    `"${item.text}" at (${item.x.toFixed(1)}, ${item.y.toFixed(1)})`,
    `font: ${item.fontName}, size: ${item.fontSize}pt`,
  );
}

TextItem

interface TextItem {
  text:      string;   // Extracted string
  x:         number;   // Left edge in user-space units
  y:         number;   // Baseline in user-space units
  width:     number;   // Approximate rendered width
  height:    number;   // Approximate rendered height (based on font size)
  fontSize:  number;   // Font size in user-space units
  fontName:  string;   // Font resource name (e.g. "/F1")
}

Supported operators:

Operator Meaning
Tj Show string
TJ Show array of strings with kerning
' Move to next line and show string
" Set word/character spacing, move to next line, show string
Td, TD, Tm, T* Text positioning
Tf Font and size selection
q, Q, cm Graphics state save/restore and matrix

Supported encodings: WinAnsiEncoding (standard 8-bit) and Identity-H CID fonts with /ToUnicode CMaps.


Incremental Saves

An incremental save appends only modified or new objects to the end of the original PDF file, leaving the original bytes untouched. This is essential for:

  • Digital signature preservation — Signatures are invalidated by full rewrites; incremental saves preserve the original signed bytes.
  • Performance on large files — Saving a minor change to a 100 MB PDF takes milliseconds instead of seconds.
  • Layered history — Each incremental update is a recoverable layer.

Usage

import { saveIncremental } from 'modern-pdf-lib';

const originalBytes = await readFile('signed.pdf');
const doc = await PdfDocument.load(originalBytes);

// Make non-destructive changes (annotations, metadata)
const page = doc.getPage(0);
page.addAnnotation(/* ... */);

const result = await saveIncremental(doc, originalBytes);
// result.bytes       — complete updated PDF (original + appended section)
// result.newXrefOffset — byte offset of the new cross-reference section
interface IncrementalSaveResult {
  bytes:         Uint8Array;
  newXrefOffset: number;
}

The output PDF structure is:

[original PDF bytes — unchanged]
[new/modified objects]
[new cross-reference section with /Prev pointer]
[new trailer]
%%EOF

60+ PDF Operators

For direct content-stream manipulation, modern-pdf-lib exports every standard PDF operator as a typed function, plus the PDFOperator base class for building custom operators.

Imports

import {
  pushGraphicsState,    // alias: q
  popGraphicsState,     // alias: Q
  concatMatrix,         // cm
  moveTo,               // m
  lineTo,               // l
  curveTo,              // c
  closePath,            // h
  rectangle,            // re
  stroke,               // S
  fill,                 // f
  fillAndStroke,        // B
  setLineWidth,         // w
  setLineCap,           // J
  setLineJoin,          // j
  setDashPattern,       // d
  setMiterLimit,        // M
  setFlatness,          // i
  beginText,            // BT
  endText,              // ET
  setFont,              // Tf
  moveText,             // Td
  showText,             // Tj
  showTextAdjusted,     // TJ
  setCharSpacing,       // Tc
  setWordSpacing,       // Tw
  setTextScale,         // Tz
  setLeading,           // TL
  setTextRenderMode,    // Tr
  setTextRise,          // Ts
  setTextMatrix,        // Tm
  nextLine,             // T*
  setFillingColor,      // sc / rg / k (depending on colour space)
  setStrokingColor,     // SC / RG / K
  setFillingColorSpace, // cs
  setStrokingColorSpace,// CS
  paintXObject,         // Do
  // ... and 40+ more
} from 'modern-pdf-lib';

import { PDFOperator } from 'modern-pdf-lib';

Custom Operator Example

import { PDFOperator } from 'modern-pdf-lib';

class SetHalftone extends PDFOperator {
  constructor(private readonly halftoneRef: PdfRef) {
    super();
  }
  toString(): string {
    return `${this.halftoneRef} sh\n`;
  }
}

Low-Level Drawing

import {
  pushGraphicsState,
  popGraphicsState,
  moveTo, lineTo, closePath, stroke,
  setLineWidth, setStrokingColor,
} from 'modern-pdf-lib';

page.pushOperators([
  pushGraphicsState(),
  setStrokingColor(1, 0, 0),      // red
  setLineWidth(2),
  moveTo(72, 700),
  lineTo(500, 700),
  lineTo(500, 100),
  closePath(),
  stroke(),
  popGraphicsState(),
].join(''));

Operator Aliases

Alias Operator Function
pushGraphicsState q Save current graphics state
popGraphicsState Q Restore last saved graphics state
concatMatrix cm Modify current transformation matrix
moveTo m Start new subpath
lineTo l Append straight-line segment
curveTo c Append cubic Bézier curve
closePath h Close current subpath
rectangle re Append rectangle
stroke S Stroke path
fill f Fill path (nonzero winding)
fillAndStroke B Fill and stroke path
endPath n End path without painting
clip W Intersect current clip path
setLineWidth w Set line width
setLineCap J Set line cap style
setLineJoin j Set line join style
setDashPattern d Set dash pattern
beginText BT Begin text object
endText ET End text object
setFont Tf Set font and size
moveText Td Move text position
showText Tj Show text string
showTextAdjusted TJ Show text with individual glyph positioning
nextLine T* Move to start of next line
paintXObject Do Paint form XObject or image

Full operator reference: ISO 32000-1:2008 Annex A.

Clone this wiki locally