Drawing annotations yourself
A rendered page includes its annotations, which is all most apps need. This page is for when you draw them yourself: each annotation as its own image over the page, so you can move, select and edit them without rendering the page again. It also covers baking annotations into the page and exporting them as a PDF.
What you draw is an annotation’s appearance: the drawing stored with it.
import { shownAppearances } from '@embedpdf/engine';
const pageImage = await page.render.image({
viewport: { kind: 'scale', scale: 2 },
includeAnnotations: false,
}); // the page on its own
const { annotations } = await page.annotations.list();
const { appearances } = await page.annotations.renderAppearances({
viewport: { kind: 'scale', scale: 2 },
});
// Each annotation as the page shows it now: its look at rest, in the state it shows.
for (const appearance of shownAppearances(appearances, annotations)) {
const { url } = await appearance.image.objectUrl();
const box = pageImage.transform.pageToPixels(appearance.rect); // { x, y, width, height } in the page image
placeImage(url, box);
}The viewer’s annotation plugin does all of this for you. This page is for drawing annotations against the engine directly.
Render each annotation as an image#
renderAppearances() draws every annotation of the page that has an appearance, each look as
its own image:
| Field | What it is |
|---|---|
ref | Which annotation it is. |
rect | Where the image goes, in page space. |
mode | Which look it is: 'normal' at rest, 'rollover' under the pointer, 'down' while pressed. |
state | Which state it draws, for an annotation whose look has states (a check box’s 'Off' and its on state); else null. |
image | The image; image.objectUrl() gives a URL for an <img>, image.blob() its bytes. |
| Option | What it is | Default |
|---|---|---|
viewport | The size, the same option as for a page image. Pass the page image’s viewport, and the annotations match it. | scale 1 |
rotation | The page’s rotation as you show it: 0, 90, 180 or 270, clockwise. | 0 |
modes | Which looks to render, of those each annotation has. ['normal'] gives only the look at rest. An empty list is refused. | every look |
format | 'png' or 'webp'. | see below |
Images are PNG unless you ask for WebP.
Looks and states#
One call brings every look an annotation has, so you can show it at rest, under the pointer and pressed without asking again:
- Modes. An annotation can have a look for when the pointer is over it (
'rollover') and for when it’s pressed ('down'), besides the look at rest ('normal'). Form fields often do. You get one image for each look the annotation has, and none for a look it doesn’t have. - States. A check box or radio button has a look for each state:
'Off'and its on state ('Yes', or its export value). You get every state, whatever the check box shows now, each image labelled with itsstate. So when someone ticks the box, you already have the picture. - Which state shows is the annotation’s
appearanceState(its/AS), or for a form field, its value.
shownAppearances(appearances, annotations) picks, for each annotation, its look at rest in the
state it shows: the page as it is now. To show a look under the pointer, pick it yourself:
import { annotationKey } from '@embedpdf/engine';
const hovered = appearances.find(
(appearance) =>
annotationKey(appearance.ref) === annotationKey(checkbox.ref) &&
appearance.mode === 'rollover' &&
(appearance.state === null || appearance.state === checkbox.appearanceState),
);Turned annotations#
Some annotations can be turned; see Turned shapes.
A turned rectangle, ellipse, text box, stamp or caret whose drawing stays inside its turned box
has its image drawn unturned: the image’s rect is the annotation’s own box, and you turn the
image yourself, about its centre. appearanceTurnOf(annotation) gives the turn to apply, in
degrees clockwise, the same direction as CSS rotate(), or null:
import { appearanceTurnOf } from '@embedpdf/engine';
const rotation = appearanceTurnOf(annotation) ?? 0;
img.style.transformOrigin = 'center';
img.style.transform = `rotate(${rotation}deg)`;Every other annotation comes as it is shown, placed by rect, with nothing to turn: lines,
polygons and ink are drawn with their points turned, a callout turns only its text box, and a
drawing that reaches past its box (a cloudy border’s bumps) is drawn turned.
That split is what keeps editing cheap:
- turning an annotation never needs a new image, only a new transform;
- moving one never needs a new image, only a new position;
- resizing can stretch the image while the user drags, then render once when they let go.
Render again when the annotation’s look changed, or for a sharper image at a new zoom. An
update’s result tells you: appearance.changed is true when the annotation looks different.
Raw pixels#
renderAppearancesRaw() takes the same options and returns each annotation’s pixels instead of an
image, with nothing encoded. Use it to draw on a canvas or in WebGL.
const { appearances } = await page.annotations.renderAppearancesRaw({
viewport: { kind: 'scale', scale: 2 },
});
const { width, height, stride, data } = appearances[0].raster;data holds the pixels as RGBA, four bytes a pixel and stride bytes a row, without
premultiplied alpha.
Bake annotations into the page#
flatten() paints annotations into the page’s own content and removes them as annotations.
What’s baked in can no longer be changed or deleted as an annotation.
const result = await page.annotations.flatten([refA, refB], { usage: 'display' });
result.results; // [{ ref: refA, status: 'applied' }, { ref: refB, status: 'unchanged' }]status | What happened |
|---|---|
'applied' | Painted in and removed as an annotation. |
'unchanged' | Left as it was: hidden for that use, a popup, or with no appearance to paint. |
So you can say “2 of 3 flattened”.
usage | What’s painted |
|---|---|
'display' | The annotations shown on screen. The default. |
'print' | The annotations that print: those with print: true. |
Every ref must be on this page: a ref on another page, or an empty list, refuses the whole call
with InvalidArg and nothing changes. When something was painted, an annotations.flattened
event fires. Flattening can’t be undone, and it ends undo for every
change made before it; see Changes & undo.
To bake every annotation of some pages in one call, flatten the pages with doc.pages.flatten();
see Flatten a page’s annotations.
Export annotations as a PDF#
exportAppearance() makes a new one-page PDF of some annotations, exactly as the page shows them:
vector, in their places, on a page the size of the box around them. The document doesn’t change.
const pdf = await page.annotations.exportAppearance([refA, refB]);It’s all or nothing: a ref that isn’t on the page, is hidden, or has no appearance refuses the call
with InvalidArg. The PDF is a one-page drawing, so it can become a stamp:
await page.annotations.create({ subtype: 'stamp', box: rect }, { resources: { appearance: pdf } });Permissions#
Rendering annotations needs doc.annotate.read: seeing an annotation includes seeing how it
looks. Flattening changes the page, so it needs doc.pages.modify and doc.annotate.modify.
Exporting sends content out, so it needs doc.download.
Good to know#
Appearances, in the PDF#
An annotation’s appearance is its /AP: a small drawing stored with it, with a normal look and
optionally a rollover and a pressed one. The engine draws it for the annotations it
creates, so they look the same in every PDF app. Another app’s drawing stays as it is until you
change how that annotation looks.
Which way rotation turns#
Every rotation in the engine turns clockwise, in degrees, as you see it: a page’s rotation, the
rotation option on this page, an annotation’s own rotation and the rotation of a run of
turned text. So CSS rotate() and the engine always agree.
In the PDF file, an annotation’s angle is stored the way PDF measures angles (counterclockwise,
because y goes up). The engine converts when it reads and writes, as it does with coordinates,
so you never see the stored number.
Annotations without a number#
Annotations with only a position ref are rendered too. See Refs & identity.
What prints#
New annotations print unless you create them with print: false, so usage: 'print' includes
them. Annotations from other apps may have print off; set print: true on the ones that should
print, or flatten with 'display'.
Your feedback goes directly to the documentation team.