EmbedPDF

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:

FieldWhat it is
refWhich annotation it is.
rectWhere the image goes, in page space.
modeWhich look it is: 'normal' at rest, 'rollover' under the pointer, 'down' while pressed.
stateWhich state it draws, for an annotation whose look has states (a check box’s 'Off' and its on state); else null.
imageThe image; image.objectUrl() gives a URL for an <img>, image.blob() its bytes.
OptionWhat it isDefault
viewportThe size, the same option as for a page image. Pass the page image’s viewport, and the annotations match it.scale 1
rotationThe page’s rotation as you show it: 0, 90, 180 or 270, clockwise.0
modesWhich 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 its state. 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' }]
statusWhat 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”.

usageWhat’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'.

Was this page helpful?

Your feedback goes directly to the documentation team.