How to Work with Annotations in TypeScript

How to Work with Annotations in TypeScript

This guide shows how to add, flatten, and search PDF annotations with Aspose.PDF FOSS for TypeScript. The Page class exposes one Add* method per annotation subtype — markup (highlight, underline, strikeout, squiggly), shapes (square, circle, line, polygon, ink), text (sticky note, free text), and links — each returning a typed Annotation handle that can be read back or flattened later. It requires Node.js 22 or later.

Step-by-Step Guide

Step 1: Install the Package

asposefoss/pdf is not yet published — build from source until it ships. See the project README for build instructions.

Verify the installation by importing the Document class in a new TypeScript file — this line should resolve without error once the package is installed:

import { Document } from '@asposefoss/pdf';

Step 2: Import Required Classes

Import Document to open the file and Page for the annotation type signatures used in the steps below:

import { Document, Page } from '@asposefoss/pdf';

Step 3: Add Markup Annotations

Page.AddHighlight(), Page.AddUnderline(), Page.AddSquiggly(), and Page.AddStrikeOut() all take a quads array — one 8-number quad (x1,y1,x2,y2,x3,y3,x4,y4) per highlighted text region — plus a color and optional contents. The library generates the appearance stream (/AP) for all four subtypes automatically:

import { Document } from '@asposefoss/pdf';

const doc = Document.OpenFile('input.pdf');
const page = doc.Pages[0];

page.AddHighlight({
  quads: [72, 700, 300, 700, 72, 685, 300, 685],
  color: [1, 1, 0],
  contents: 'Yellow highlight',
});
page.AddUnderline({
  quads: [72, 660, 300, 660, 72, 645, 300, 645],
  color: [0, 0, 1],
});
page.AddStrikeOut({
  quads: [72, 620, 300, 620, 72, 605, 300, 605],
  color: [1, 0, 0],
});

doc.WriteTo('annotated.pdf');

Step 4: Add Shape and Ink Annotations

Page.AddSquare() and Page.AddCircle() take a bounding rect plus color (stroke) and optional fill. Page.AddLine() takes a 4-number line (x1,y1,x2,y2) and optional arrow endings. Page.AddInk() takes paths — an array of flat point-pair arrays, one per pen stroke:

page.AddSquare({ rect: [100, 500, 220, 560], color: [0.8, 0, 0], fill: [1, 1, 0.5], width: 2 });
page.AddCircle({ rect: [250, 500, 370, 560], color: [0, 0.5, 0], width: 2 });
page.AddLine({
  line: [100, 470, 370, 470],
  color: [0, 0, 0.7],
  width: 2,
  startEnding: 'OpenArrow',
  endEnding: 'ClosedArrow',
});
page.AddInk({
  paths: [[100, 400, 130, 430, 160, 390, 190, 420]],
  color: [0.6, 0, 0.6],
  width: 2,
});

Step 5: Add a Sticky Note and Free Text

Page.AddTextNote() places a clickable icon that opens a comment popup. Page.AddFreeText() draws text directly on the page inside its rect:

page.AddTextNote({
  rect: [400, 700, 420, 720],
  icon: 'Note',
  author: 'Reviewer',
  contents: 'This is a sticky-note annotation.',
});

page.AddFreeText({
  rect: [400, 600, 550, 660],
  contents: 'FreeText sample',
  fontSize: 10,
  align: 'center',
  fill: [1, 1, 0.8],
  width: 1,
});

Step 6: Flatten an Annotation into Static Content

Every annotation handle returned by an Add* call has a Flatten() method that bakes the annotation’s appearance into the page’s content stream and removes it from /Annots. After flattening there is nothing left to click or edit in a viewer:

const note = page.AddFreeText({
  rect: [210, 535, 470, 590],
  contents: 'Sticky note — flattened into the page.',
  fontSize: 11,
  align: 'left',
});

note.Flatten(); // -> boolean; bakes the annotation, unwires itself from /Annots

Step 7: Search Annotation Text

Page.SearchAnnotationText() and Page.SearchAnnotations() are disjoint in both directions: SearchAnnotationText() matches literal annotation metadata (/Contents, /T, /Subj), while SearchAnnotations() matches the rendered text of markup annotations such as FreeText:

const metadataHits = page.SearchAnnotationText('confidential');
for (const hit of metadataHits) {
  console.log(hit.key, hit.value); // e.g. 'Contents', 'confidential'
}

const renderedHits = page.SearchAnnotations('confidential');
console.log(renderedHits.length);

Common Issues and Fixes

Page.AddHighlight() (or another markup method) draws in the wrong place. quads is an 8-number flat array (x1,y1,x2,y2,x3,y3,x4,y4), not a rect — passing a 4-number rectangle produces a degenerate or missing quad. Build one quad per line of highlighted text.

A flattened annotation is still clickable. Flatten() must be called on the object returned by the Add* call, and the document must be saved (doc.WriteTo() / doc.Save()) after flattening — the change only exists in memory until the file is written back out.

SearchAnnotationText() returns no hits even though the text is visible on the page. It only searches annotation metadata fields (/Contents, /T, /Subj) — visible markup text rendered from an annotation’s own appearance (e.g. a FreeText body) is found by SearchAnnotations() instead, and neither searches ordinary page content (use Page.GetText() or Page.Search() for that).

Redaction marks do not show up when searching annotations. Page.AddRedact() creates a RedactAnnotation, a distinct subtype from the markup/shape/text annotations covered here — see the Redaction guide.

Frequently Asked Questions

How many annotation subtypes does Page support?

The library generates appearances for markup (highlight, underline, strikeout, squiggly), shapes (square, circle, line, polygon, ink), text (sticky note, free text), links, and stamps — plus file attachment and redaction annotations, which are covered in their own how-to guides.

Can I read annotations that already exist on a page?

Yes — Page.Annotations returns the existing annotations as an array of typed handles you can inspect, edit, or flatten, and Page.RemoveAnnotation(a) removes one.

What is the difference between flattening an annotation and applying a redaction?

Flatten() bakes an annotation’s appearance into the page and removes the annotation object, but any text or image content underneath it is untouched. ApplyRedactions() destructively rewrites the content stream so the covered content itself is gone — see the Redaction guide for details.

Are annotations preserved through a save/open round trip?

Yes — annotations added before doc.WriteTo() / doc.Save() are written into the PDF and read back correctly on the next Document.Open() / Document.OpenFile().

See Also