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#
subtype | What it draws | Default color |
|---|---|---|
'highlight' | A marker over the text | yellow |
'underline' | A straight line under the text | black |
'squiggly' | A wavy line under the text | black |
'strikeout' | A line through the text | black |
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#
| Field | What it is | Default |
|---|---|---|
quadPoints | Where the text is: one quad per line piece. Required, at least one. | |
color | The mark’s color, '#rrggbb'. | yellow for a highlight, black for the others |
opacity | From 0 (invisible) to 1 (solid). | 1 |
blendMode | How the color mixes with the page under it. | 'multiply' for a highlight, 'normal' for others |
contents | A 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.
Your feedback goes directly to the documentation team.