EmbedPDF

Rendering

page.render.image() draws a page as an image you can show in an <img>:

const image = await page.render.image({ viewport: { kind: 'width', width: 800 } });
const { url, revoke } = await image.objectUrl();
 
img.src = url;
// When the image is no longer shown:
revoke();

Choose the size#

viewport sets how big the image is. Give a width in pixels, and the height follows the page’s shape:

await page.render.image({ viewport: { kind: 'width', width: 1200 } });

Or give a scale, in pixels per PDF point. At scale 1, a US Letter page (612 × 792 points) is 612 × 792 pixels:

await page.render.image({ viewport: { kind: 'scale', scale: 2 } });

Without viewport, the scale is 1.

Sharp images on high-density screens#

A screen with a devicePixelRatio of 2 shows two pixels for every CSS pixel. Render that many more pixels and show the image at its CSS size:

const cssWidth = 800;
const image = await page.render.image({
  viewport: { kind: 'width', width: Math.round(cssWidth * devicePixelRatio) },
});
 
img.style.width = `${cssWidth}px`;

Turn the page#

An image shows the page as it’s stored, unturned, whatever the page’s own rotation. That way one image works for every way you show the page: a viewer turns the view, not the image. To get the page the way PDF apps show it, pass the page’s own rotation from doc.pages.list():

const { pages } = await doc.pages.list();
 
await doc.page(pages[0].ref).render.image({
  viewport: { kind: 'width', width: 800 },
  rotation: pages[0].rotation,
});

rotation is 0 (the default), 90, 180 or 270 degrees clockwise, from the page as it’s stored. At 90 and 270, the image’s width and height swap, and viewport.width is the width of the turned image.

Format and quality#

format is 'png' (the default), 'webp', or 'bmp'. BMP is uncompressed: the fastest to make and the largest, for images you only keep in memory. A BMP image’s format reads 'bmp'.

In Node there is no canvas, so the engine encodes PNG and BMP itself. WebP fails with NotImplemented there unless you give the engine your own imageEncoder; see Setup.

quality goes from 0 to 1 and applies to WebP. Any other value fails with InvalidArg:

await page.render.image({ viewport: { kind: 'width', width: 800 }, format: 'webp', quality: 0.8 });

The image tells you what you got:

FieldWhat it is
format'png' or 'webp'
contentTypeThe MIME type, such as 'image/webp'
width, heightThe image’s size in pixels
objectUrl()A URL for <img>, and revoke() to free it
blob()The image’s bytes, as a Blob (below)
transformConverts between its pixels and page space (below)

The image’s bytes#

blob() gives the image as a Blob, typed with its contentType. It’s the same call on every engine, so use it to upload the image, save it or decode it:

const image = await page.render.image({ viewport: { kind: 'width', width: 800 }, format: 'png' });
const blob = await image.blob();
 
const bitmap = await createImageBitmap(blob);

Cancel it with .abort(), like a render.

The image’s source is { kind: 'bytes', bytes }: the same bytes, without making a Blob.

Background and annotations#

The page is drawn on white. For a transparent background, pass background: 'transparent' with PNG or WebP.

Annotations are drawn on the page, form fields included, each in the state it shows: the page as it is printed. To draw the page without them, when you draw annotations yourself, pass includeAnnotations: false:

await page.render.image({ includeAnnotations: false, background: 'transparent', format: 'png' });

When you draw only the form fields yourself, over the page, leave out just those with includeFormFields: false. The other annotations stay in the picture:

await page.render.image({ includeFormFields: false });

And the other way round: when you draw the annotations yourself, ask for the form fields without them:

await page.render.image({ includeAnnotations: false, includeFormFields: true });

Hidden form fields are never drawn, and fields marked not to show on screen are drawn only when printing.

The picture holds only what the user may read. Left out, includeAnnotations draws the annotations when the user may read them (doc.annotate.read), and includeFormFields the form fields when they may read the form (doc.forms.read). So someone who may fill a form but not read its comments gets a picture without the comments. Asking for annotations or fields the user may not read fails with Forbidden, naming the option.

Part of a page#

target renders one area instead of the whole page. The rect is in page space, from the page’s top-left like annotation rects and text positions, so you can render exactly the area of an annotation:

const closeUp = await page.render.image({
  target: { kind: 'rect', rect: annotation.rect },
  viewport: { kind: 'scale', scale: 4 },
});

viewport then sizes the area: a width is the width of the area’s image.

Raw pixels#

render.raw() takes the same options (except format and quality) and returns the pixels themselves, for a canvas or your own image pipeline:

const raster = await page.render.raw({ viewport: { kind: 'width', width: 800 } });
 
const imageData = new ImageData(new Uint8ClampedArray(raster.data), raster.width, raster.height);
canvas.width = raster.width;
canvas.height = raster.height;
canvas.getContext('2d')!.putImageData(imageData, 0, 0);

raster.data is RGBA, four bytes per pixel, not premultiplied, row after row (raster.stride bytes per row). It skips encoding, so it’s the fastest way to get a page onto a canvas.

From image pixels to page points#

The engine gives every position in page space, in points from the page’s top-left corner: text, search matches, annotations. The pointer and the screen work in pixels. A page transform converts between the two, and every image has the one it was rendered with:

const image = await page.render.image({ viewport: { kind: 'width', width: 800 } });
 
img.addEventListener('click', (event) => {
  const scale = image.width / img.clientWidth; // image pixels per CSS pixel
  const point = image.transform.pixelsToPage({
    x: event.offsetX * scale,
    y: event.offsetY * scale,
  });
  // point is { x, y } in page space, ready for layout.charAt() or an annotation
});
 
const box = image.transform.pageToPixels(match.segments[0].rect); // { x, y, width, height }

pageToPixels() takes a point, a box or a quad; pixelsToPage() takes a point or a box. Both take the page’s rotation, the size and the target into account, so you never do that math yourself.

A raw raster has a transform too.

Without an image#

You don’t need an image to get a transform. pageTransform() computes the same one from the page’s layout and the settings you would render with:

import { pageTransform } from '@embedpdf/engine';
 
const { pages } = await doc.pages.list();
const view = pageTransform(pages[0], { viewport: { kind: 'width', width: 800 } });
 
view.width; // 800: the size an image with these settings has
view.height; // 1035 for a US Letter page

It takes the same settings as render.image() (viewport, rotation, target) and gives the exact numbers of the image those settings render, rounding included. Use it to place boxes before the image arrives, to keep an overlay in place while you zoom, for a page drawn as several tiles (one transform for the whole page), or for a page you draw yourself.

With CSS or a canvas#

view.matrix is the conversion from page space to pixels as the six numbers of a CSS matrix() or a canvas setTransform(). Draw an overlay in page space and let the browser place it:

<svg width={view.width} height={view.height}>
  <g transform={`matrix(${view.matrix.join(' ')})`}>
    {match.segments.map(({ rect }, i) => (
      <rect
        key={i}
        x={rect.x}
        y={rect.y}
        width={rect.width}
        height={rect.height}
        fill="rgba(255, 215, 0, 0.4)"
      />
    ))}
  </g>
</svg>

Turned pages#

Rotation is one of the settings, 0 by default as for a render: at 90 the page turns clockwise, the width and height swap, and the transform turns with it. Positions in page space don’t change when a page turns, so a selection or a search result keeps the same numbers; only the transform is different.

Cancel a render#

Rendering a large page takes time. When the user scrolls away before the image arrives, cancel it:

const pending = page.render.image({ viewport: { kind: 'width', width: 1600 } });
 
// Later, when the page is no longer visible:
pending.abort();

The promise then fails with Aborted, and the engine stops drawing the page, usually within a few milliseconds, so the renders waiting behind it start right away. See Errors & cancelling.

Options#

OptionWhat it doesDefault
viewport{ kind: 'width', width } in pixels, or { kind: 'scale', scale }scale 1
rotationTurns the image clockwise: 0, 90, 180 or 2700
format'png' or 'webp' (see Format and quality)see above
qualityWebP quality, from 0 to 1encoder default
background'white' or 'transparent''white'
includeAnnotationsDraw the page’s annotationstrue when the user may read them
includeFormFieldsDraw the form fields toofalse without annotations; else true when the user may read the form
target{ kind: 'page' }, or { kind: 'rect', rect } for one areathe whole page

Permissions#

Rendering needs the doc.render permission. Without it, the call fails with Forbidden. A picture with annotations also needs doc.annotate.read, and one with form fields doc.forms.read.

Good to know#

Render policy#

doc.render.getPolicy() says which render sizes this document renders best at:

const policy = await doc.render.getPolicy();

The local engine answers { kind: 'continuous' }: it renders any size exactly as you ask.

To test a lattice policy locally, pass localEngine({ renderPolicy }).

The engine never changes the size you ask for. To pick the lattice width for the size you want, use snapFullPageViewport(). It returns the smallest width on the lattice that’s at least as large (or the largest one), and your viewport unchanged under a continuous policy:

import { snapFullPageViewport } from '@embedpdf/engine';
 
const { pages } = await doc.pages.list();
const viewport = snapFullPageViewport(
  policy,
  { kind: 'scale', scale: 2 },
  {
    pageWidth: pages[0].size.width,
  },
);
await doc.page(pages[0].ref).render.image({ viewport });

The render plugin does all of this for you, and adds tiles when the user zooms in far.

Was this page helpful?

Your feedback goes directly to the documentation team.