Skip to content

Annotations

PDF annotations are interactive or visual elements overlaid on page content. This guide explains how to read, create, modify, and remove annotations.

Annotations include:

  • Text highlights, underlines, strikethroughs
  • Comments and sticky notes
  • Links and hyperlinks
  • Form widgets
  • Stamps and signatures
  • Drawing markups

getAnnotation() returns a PDFiumAnnotation — a disposable wrapper with full read/write access:

using page = document.getPage(0);
using annotation = page.getAnnotation(0);
console.log(`Type: ${annotation.type}`);
console.log(`Bounds: ${JSON.stringify(annotation.bounds)}`);
const annotations = page.getAnnotations();
console.log(`Found ${annotations.length} annotations`);
console.log(`Page has ${page.annotationCount} annotations`);

PDFiumAnnotation provides both read-only and mutable access:

Property / MethodTypeDescription
indexnumberZero-based index on page
typeAnnotationTypeAnnotation subtype
boundsRectBounding rectangle
colourColour | nullStroke colour (cached)
flagsAnnotationFlagsAnnotation flags bitmask
objectCountnumberPage objects inside (ink/stamp)
contentsstring | undefinedText body (get/set)
authorstring | undefinedAuthor name (get/set)
subjectstring | undefinedSubject line (get/set)
interface Rect {
left: number; // Left edge in points
top: number; // Top edge in points
right: number; // Right edge in points
bottom: number; // Bottom edge in points
}
interface Colour {
r: number; // Red (0-255)
g: number; // Green (0-255)
b: number; // Blue (0-255)
a: number; // Alpha (0-255, 255 = opaque)
}

The AnnotationType enum includes 28 types:

import { AnnotationType } from '@scaryterry/pdfium';
TypeDescription
HighlightYellow highlight
UnderlineUnderline markup
SquigglyWavy underline
StrikeoutStrikethrough
TypeDescription
TextSticky note (popup comment)
FreeTextText box annotation
PopupPopup window for comments
CaretInsert text marker
TypeDescription
LineLine annotation
SquareRectangle
CircleEllipse
PolygonClosed polygon
InkFreehand drawing
TypeDescription
LinkHyperlink
FileAttachmentEmbedded file
SoundAudio annotation
ScreenScreen annotation
TypeDescription
WidgetForm field
XFAWidgetXFA form field
TypeDescription
StampStamp annotation
PrinterMarkPrinter marks
TrapNetTrapping annotation
WatermarkWatermark
ThreeD3D annotation
RichMediaRich media
RedactRedaction
UnknownUnknown type
import { AnnotationType } from '@scaryterry/pdfium';
const highlights = page.getAnnotations()
.filter(a => a.type === AnnotationType.Highlight);
console.log(`Found ${highlights.length} highlights`);
for (const h of highlights) {
console.log(`Highlight at: ${JSON.stringify(h.bounds)}`);
if (h.colour) {
console.log(`Colour: rgba(${h.colour.r}, ${h.colour.g}, ${h.colour.b}, ${h.colour.a})`);
}
}
const links = page.getAnnotations()
.filter(a => a.type === AnnotationType.Link);
console.log(`Found ${links.length} links`);
const commentTypes = [
AnnotationType.Text,
AnnotationType.FreeText,
AnnotationType.Popup,
];
const comments = page.getAnnotations()
.filter(a => commentTypes.includes(a.type));
const formFields = page.getAnnotations()
.filter(a => a.type === AnnotationType.Widget);
console.log(`Found ${formFields.length} form fields`);
import { AnnotationType } from '@scaryterry/pdfium';
interface AnnotationSummary {
pageIndex: number;
type: AnnotationType;
bounds: AnnotationBounds;
}
function getAllAnnotations(document: PDFiumDocument): AnnotationSummary[] {
const result: AnnotationSummary[] = [];
for (const page of document.pages()) {
using p = page;
for (const annot of p.getAnnotations()) {
result.push({
pageIndex: p.index,
type: annot.type,
bounds: annot.bounds,
});
}
}
return result;
}
// Usage
const allAnnotations = getAllAnnotations(document);
console.log(`Total annotations: ${allAnnotations.length}`);
function getAnnotationStats(document: PDFiumDocument) {
const stats = new Map<AnnotationType, number>();
for (const page of document.pages()) {
using p = page;
for (const annot of p.getAnnotations()) {
stats.set(annot.type, (stats.get(annot.type) || 0) + 1);
}
}
return stats;
}
// Usage
const stats = getAnnotationStats(document);
for (const [type, count] of stats) {
console.log(`${AnnotationType[type]}: ${count}`);
}

By default, render() does not include annotations. Use renderFormFields for form widgets:

// Without annotations (default)
const result1 = page.render({ scale: 2 });
// With form fields rendered
const result2 = page.render({ scale: 2, renderFormFields: true });
function boundsToPixels(
bounds: AnnotationBounds,
pageHeight: number,
scale: number
) {
return {
left: bounds.left * scale,
top: (pageHeight - bounds.top) * scale, // Flip Y axis
right: bounds.right * scale,
bottom: (pageHeight - bounds.bottom) * scale,
};
}
// Usage
const { width, height } = page.render({ scale: 2 });
const annotation = page.getAnnotation(0);
const pixelBounds = boundsToPixels(annotation.bounds, page.height, 2);
import { PDFium, AnnotationType } from '@scaryterry/pdfium';
import { promises as fs } from 'fs';
async function analyseAnnotations(filename: string) {
const data = await fs.readFile(filename);
using pdfium = await PDFium.init();
using document = await pdfium.openDocument(data);
console.log(`Analysing annotations in ${filename}`);
console.log(`Total pages: ${document.pageCount}\n`);
const typeCounts = new Map<AnnotationType, number>();
for (const page of document.pages()) {
using p = page;
if (p.annotationCount === 0) continue;
console.log(`Page ${p.index + 1}: ${p.annotationCount} annotations`);
for (const annot of p.getAnnotations()) {
typeCounts.set(annot.type, (typeCounts.get(annot.type) || 0) + 1);
const typeName = AnnotationType[annot.type] || 'Unknown';
const { left, top, right, bottom } = annot.bounds;
console.log(` [${typeName}] at (${left.toFixed(1)}, ${top.toFixed(1)}) - ` +
`(${right.toFixed(1)}, ${bottom.toFixed(1)})`);
if (annot.colour) {
const { r, g, b, a } = annot.colour;
console.log(` Colour: rgba(${r}, ${g}, ${b}, ${a})`);
}
}
}
console.log('\nSummary:');
for (const [type, count] of typeCounts) {
const typeName = AnnotationType[type] || 'Unknown';
console.log(` ${typeName}: ${count}`);
}
}
analyseAnnotations('document.pdf');

Use page.createAnnotation() to add a new annotation:

import { AnnotationType } from '@scaryterry/pdfium';
using page = document.getPage(0);
// Create a text (sticky note) annotation
using annot = page.createAnnotation(AnnotationType.Text);
if (annot) {
annot.setRect({ left: 100, bottom: 700, right: 120, top: 720 });
annot.setColour({ r: 255, g: 255, b: 0, a: 255 }); // Yellow
annot.contents = 'Review this section';
annot.author = 'Reviewer';
}
using annot = page.createAnnotation(AnnotationType.Highlight);
if (annot) {
annot.setRect({ left: 72, bottom: 700, right: 300, top: 712 });
annot.setColour({ r: 255, g: 255, b: 0, a: 128 });
// Set quad points to define the highlighted region
annot.appendAttachmentPoints({
x1: 72, y1: 712,
x2: 300, y2: 712,
x3: 72, y3: 700,
x4: 300, y4: 700,
});
}
using annot = page.createAnnotation(AnnotationType.Ink);
if (annot) {
annot.setRect({ left: 50, bottom: 400, right: 200, top: 500 });
annot.setColour({ r: 255, g: 0, b: 0, a: 255 });
// Add freehand stroke points
annot.addInkStroke([
{ x: 50, y: 450 },
{ x: 100, y: 480 },
{ x: 150, y: 420 },
{ x: 200, y: 460 },
]);
}
using annot = page.createAnnotation(AnnotationType.Link);
if (annot) {
annot.setRect({ left: 72, bottom: 650, right: 200, top: 665 });
annot.setURI('https://example.com');
}
// Check if an annotation type supports object manipulation
const supported = page.isAnnotationSubtypeSupported(AnnotationType.Stamp);

Open an existing annotation, change properties, and the changes persist when you save the document:

using annot = page.getAnnotation(0);
// Update rectangle
annot.setRect({ left: 100, bottom: 700, right: 200, top: 750 });
// Change colours
annot.setColour({ r: 0, g: 128, b: 255, a: 255 }); // stroke
annot.setColour({ r: 200, g: 200, b: 255, a: 128 }, 'interior'); // fill
// Update text content
annot.contents = 'Updated comment text';
annot.author = 'Editor';
// Update border
annot.setBorder({ horizontalRadius: 0, verticalRadius: 0, borderWidth: 2 });
// Update flags
annot.setFlags(0x04); // Print flag
// Set/clear appearance
annot.setAppearance('normal', undefined); // Clear to force re-render

Annotations store arbitrary key/value pairs in their PDF dictionary:

using annot = page.getAnnotation(0);
// Check if a key exists
if (annot.hasKey('Contents')) {
const value = annot.getStringValue('Contents');
console.log(`Contents: ${value}`);
}
// Set a custom string value
annot.setStringValue('NM', 'unique-annotation-id');
using page = document.getPage(0);
// Remove annotation at index 0
const removed = page.removeAnnotation(0);
console.log(`Removed: ${removed}`);

After creating or modifying annotations, save the document to persist changes:

const bytes = document.save();
await fs.writeFile('annotated.pdf', bytes);