EmbedPDF

Highlights & text markup

Text markup marks text that is already on the page: a highlight, an underline, a squiggly underline or a strikeout. You tell the engine where the text is, and it draws the mark over it.

const { matches } = await doc.search.query({ text: 'total amount' });
 
for (const match of matches) {
  await doc.page(match.page).annotations.create({
    subtype: 'highlight',
    quadPoints: match.segments.map((segment) => segment.quad),
  });
}

The four kinds#

subtypeWhat it drawsDefault color
'highlight'A marker over the textyellow
'underline'A straight line under the textblack
'squiggly'A wavy line under the textblack
'strikeout'A line through the textblack

All four take the same fields. To underline instead of highlight, change only the subtype.

Mark a search match#

A search match has one segment per line it covers, and each segment has a quad: the four corners of that piece of text. quadPoints takes exactly those quads, so a match becomes a highlight in one call, as above. See Search.

Mark a selection#

For text the user selects, get the quads from the page’s text layout:

const layout = await page.text.layout();
const range = layout.wordAt(point); // { start, count }, or null
 
if (range) {
  await page.annotations.create({
    subtype: 'underline',
    quadPoints: layout.segments(range).map((segment) => segment.quad),
  });
}

Any range works: a word, a line, or everything between two characters of a drag. See Selection & geometry for how to get one.

A markup sits on one page. When a selection runs over two pages, create one markup on each page, each from that page’s range.

Fields#

FieldWhat it isDefault
quadPointsWhere the text is: one quad per line piece. Required, at least one.
colorThe mark’s color, '#rrggbb'.yellow for a highlight, black for the others
opacityFrom 0 (invisible) to 1 (solid).1
blendModeHow the color mixes with the page under it.'multiply' for a highlight, 'normal' for others
contentsA comment, which PDF apps show with the mark.none

You don’t pass a rect: the engine works it out from the quads, as the box around all of them. In an update, a different rect moves the quads into it; see Move or resize by rect. The fields every annotation has (flags, subject, who wrote it) are on the overview.

Color#

await page.annotations.update(highlight.ref, { color: '#90ee90' });

A highlight multiplies its color with the page, like a real marker: the text under it stays dark and readable, whatever the color. Pass blendMode: 'normal' to paint the color over the text instead, and lower opacity to let the text show through.

Change what it covers#

Send new quads to move a mark to other text. They replace the old ones:

await page.annotations.update(highlight.ref, {
  quadPoints: layout.segments(newRange).map((segment) => segment.quad),
});

Keep the text it marks#

A markup stores where the text is, not which characters it covers. If you need the text later, for a list of highlights or an export, read it when you create the mark and keep it in contents:

const text = await page.text.slice(range);
 
await page.annotations.create({
  subtype: 'highlight',
  quadPoints: layout.segments(range).map((segment) => segment.quad),
  contents: text,
});

PDF apps show contents as the highlight’s comment, so the quoted text shows up there too.

Suggest an insertion or a replacement#

Reviewers mark a change to the text with a caret: subtype: 'caret' at the spot where text should go, with the new text in contents. Its box is the box the caret symbol is drawn in.

await page.annotations.create({
  subtype: 'caret',
  box: { x: 208, y: 82, width: 8, height: 12 },
  contents: 'not ',
});

To suggest replacing text, strike out the old text and put a caret right after it. Link the two, so PDF apps treat them as one suggestion:

const { annotation: caret } = await page.annotations.create({
  subtype: 'caret',
  intent: 'replace',
  box: { x: 208, y: 82, width: 8, height: 12 },
  contents: 'invoice',
});
 
await page.annotations.create({
  subtype: 'strikeout',
  intent: 'strikeout-text-edit',
  quadPoints: layout.segments(range).map((segment) => segment.quad),
  reply: { to: caret.ref, type: 'group' },
});

The caret holds the suggestion: its contents, its author and its replies. The strikeout is a part of it. A caret is red by default and takes color and opacity like a markup.

Permissions#

Creating a markup needs permission to create annotations; see Permissions. Finding the text needs less than reading it: the layout needs doc.text.select and a search needs doc.text.search. Only page.text.slice() needs doc.text.copy. So a user may highlight text they aren’t allowed to copy.

Good to know#

Quads#

A quad is four corners, { upperLeft, upperRight, lowerLeft, lowerRight }, named as the text itself stands: upper is the top of the letters, and left is the left end as it would be if the text stood upright. On text turned upside down, upperLeft is at the bottom right of the page. Right-to-left text uses the same names. Segments from a search and from the layout carry these names, for upright, rotated and vertical text alike, so pass them unchanged.

Other PDF apps store a markup’s corners in different orders. The engine sorts them out when it reads the file, so every quad it gives you uses these names.

One mark, many lines#

A highlight over three lines is one annotation with three quads, not three annotations. PDF apps select, move and delete it as one.

Replies and popups#

A markup can have replies and a popup, just like a note: see Notes & replies. For readers who know PDF: the suggestion link is /IRT with /RT /Group, and the two intents are /IT /Replace and /IT /StrikeOutTextEdit.

Was this page helpful?

Your feedback goes directly to the documentation team.