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 'webp' (the default) or 'png'. The server keeps WebP renders at the widths it
shares between users (see Render policy), so WebP is usually the fastest to load.
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:
| Field | What it is |
|---|---|
format | 'png' or 'webp' |
contentType | The MIME type, such as 'image/webp' |
width, height | The image’s size in pixels |
objectUrl() | A URL for <img>, and revoke() to free it |
blob() | The image’s bytes, as a Blob (below) |
transform | Converts 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: 'url', url }, an address that needs the document’s token.
blob() fetches it with the token, and asks again when the document changed in the meantime.
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 shown on screen. 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 in a print picture.
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.
Print pictures#
A picture for paper follows the print rules instead of the screen’s. Pass usage: 'print', and
size it with a dpi viewport:
const image = await page.render.image({
usage: 'print',
viewport: { kind: 'dpi', dpi: 300 },
format: 'png',
});What changes on paper:
- Layers take their print state. A watermark set to show only when printing is drawn, and a layer set to show only on screen is not.
- Annotations and form fields are drawn by their
printflag. One that doesn’t print is left out, even when it shows on screen, such as a Print or Submit button marked “visible but doesn’t print”. One that is hidden on screen but prints (noView) is drawn. Hidden ones never are.
dpi is 150 or 300, in dots per inch of the paper the page prints on, A4 or Letter. A page
that fits on that paper is drawn at its own size at that DPI. A bigger one gets no more pixels than
it needs shrunk to fit the paper, since the printer would throw the rest away: an A0 drawing at
300 DPI is 2482 × 3508 pixels, not 9933 × 14043.
A print picture is always the whole page, upright, on white. It takes only a dpi viewport, and
no target, rotation or background; a dpi viewport is only for print pictures. Anything else
fails with InvalidArg. includeAnnotations and includeFormFields work as for any picture.
pageTransform() gives a print picture’s size and transform without rendering:
const { width, height } = pageTransform(pages[0], { viewport: { kind: 'dpi', dpi: 150 } });Printing needs doc.print, and 300 DPI also doc.print.high: the PDF’s permission to print at
full quality. Without them the call fails with Forbidden.
Each print picture has an address of its own for each DPI, so a CDN only serves the 300 DPI pictures to sessions that may print at full quality.
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.
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.
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 '@cloudpdf/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 pageIt 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#
| Option | What it does | Default |
|---|---|---|
viewport | { kind: 'width', width } in pixels, or { kind: 'scale', scale }; { kind: 'dpi', dpi } for a print picture | scale 1 |
rotation | Turns the image clockwise: 0, 90, 180 or 270 | 0 |
format | 'png' or 'webp' (see Format and quality) | see above |
quality | WebP quality, from 0 to 1 | encoder default |
background | 'white' or 'transparent' | 'white' |
includeAnnotations | Draw the page’s annotations | true when the user may read them |
includeFormFields | Draw the form fields too | false without annotations; else true when the user may read the form |
target | { kind: 'page' }, or { kind: 'rect', rect } for one area | the whole page |
usage | 'display', or 'print' for a print picture | 'display' |
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. A print picture also needs doc.print, and at 300 DPI doc.print.high.
Good to know#
Render policy#
doc.render.getPolicy() says which render sizes this document renders best at:
const policy = await doc.render.getPolicy();A CloudPDF server answers a lattice: a short list of page widths it renders once, stores and
shares with every user of the document (for example [320, 640, 1280, 2560]). Other widths
render too; they just aren’t stored and shared.
{
kind: 'lattice',
fullPage: { widths: [320, 640, 1280, 2560] },
formats: ['webp'],
background: 'white',
enforced: false,
}With enforced: true, the server refuses full-page renders at other widths with InvalidArg.
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 '@cloudpdf/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.
When the image downloads#
render.image() resolves as soon as the engine knows the image’s address. The image itself
downloads when you call objectUrl() or blob(), so an image you never show costs nothing. Renders are cached
on the server with the page, and change only when the page or its annotations do.