CloudPDF
DocsPricing
Start building

Pages & rendering

Page work splits across two services: the document-scoped pages service for listing and structure edits, and the per-page render service for producing pixels.

Listing pages#

doc.pages.list() returns a geometry snapshot of every page in display order. It never loads page content, so it’s cheap.

const { pageCount, pages } = await doc.pages.list();
 
for (const page of pages) {
  console.log(page.index, page.pageObjectNumber, page.label, page.width, page.height, page.rotation);
}

Each entry is a PageLayout: display index, durable pageObjectNumber, label (or null), un-rotated crop width/height in PDF points, rotation, userUnit, and the raw PDF boxes (media, crop, and any declared bleed/trim/art).

Sizes are un-rotated. For 90°/270° pages, swap width and height yourself to get the on-screen dimensions — the display transform lives in your code, not in the wire data.

Rendering a page#

Get a PageHandle and call its render service. There are two methods:

interface PageRenderService {
  image(options?: PageImageOptions): AbortablePromise<PageImageHandle>;
  raw(options?: PageRenderOptions): AbortablePromise<PageRaster>;
}

Encoded images#

render.image() returns an encoded image (PNG or WebP) — ideal for <img> elements and HTTP caching.

const page = doc.page(pageObjectNumber);
 
const image = await page.render.image({
  viewport: { kind: 'width', width: 1400 },
  format: 'webp',
  quality: 0.9,
  background: 'white',
});
 
const { url, revoke } = await image.objectUrl();
imgEl.src = url;
// later, when the image is no longer shown:
revoke();

The returned PageImageHandle carries format, contentType, optional width/height, a source (bytes or url), and the objectUrl() helper that builds (and lets you revoke) a browser object URL.

Raw rasters (local engine only)#

For canvas compositing or custom pipelines, render.raw() returns a PageRaster with an RGBA ArrayBuffer. This is local/WASM only — the cloud engine serves encoded images, so use render.image() there.

const raster = await page.render.raw({ viewport: { kind: 'scale', scale: 2 } });
// raster.width, raster.height, raster.stride, raster.data (ArrayBuffer, rgba8)
const pixels = new Uint8ClampedArray(raster.data);

Render options#

Both methods accept the same core options (image() adds format and quality):

OptionTypeDefaultNotes
viewport{ kind: 'scale', scale? } or { kind: 'width', width }scale 1scale renders one PDF point as N device pixels (fold in devicePixelRatio yourself); width sets an exact output width and preserves aspect.
target{ kind: 'page' } or { kind: 'rect', rect }whole pageRender a sub-rectangle in PDF user space (origin bottom-left, top > bottom).
rotation0 | 90 | 180 | 270page rotationExtra rotation applied on top of the page’s own.
background'white' | 'transparent''white'Use transparent with PNG/WebP for alpha.
includeAnnotationsbooleanserver defaultWhether annotations are baked into the raster.
format'png' | 'webp' | 'bmp'server defaultimage() only. Network endpoints serve png/webp.
qualitynumberimage() only; lossy-format quality.

Prefer viewport: { kind: ‘width’ } when you’re filling a known layout column — you get crisp output at exactly the pixel width you need, and the result is cache-friendly on the server’s CDN.

Render policy#

doc.render.policy() answers what render parameters this deployment treats as first-class:

const policy = await doc.render.policy();
// { kind: 'continuous' }  — or a lattice, see below

A cloud deployment answers a lattice: a finite set of render points the server treats as durable, CDN-shared artifacts.

{
  kind: 'lattice',
  fullPage: { widths: [640, 1280, 2560] }, // full-page renders quantize on width
  formats: ['webp', 'png'],
  background: 'white',
  enforced: true, // off-lattice render URLs are rejected with 400
}

Full-page renders quantize on viewport.width — the bounded quantity is output pixels, never zoom (a scale lattice would bound artifact count but not size; a poster page at scale 1 is already enormous). The SDK never conforms a request implicitly — the same render.image() call must not return different pixels on local and cloud. Snap explicitly with the shared helper when you render by scale:

import { snapFullPageViewport } from '@cloudpdf/engine';
 
const viewport = snapFullPageViewport(policy, { kind: 'scale', scale: 2 }, {
  pageWidth: page.size.width,
});
await doc.page(pon).render.image({ viewport }); // always on the lattice

The render plugin consumes this policy for you — it conforms every ask, caches by the resulting identity, and adds deep-zoom tiles past the ladder’s top. If you render through the plugin (<RenderLayer />, renderPage), you never call policy() yourself. See the Render plugin page.

Reordering pages#

doc.pages.move(pageObjectNumbers, destIndex) detaches the listed pages and re-inserts them as a contiguous block at destIndex (in the post-removal index space), preserving the order you pass them.

// Move pages 7 and 9 so they sit starting at display index 0.
await doc.pages.move([7, 9], 0);

Per-page revision tokens survive a move, so index-based annotation references you’re holding remain valid across a reorder.

Rotating and deleting pages#

rotate sets the ABSOLUTE display rotation of one or more pages — one value for all, the multi-select gesture. It is pure presentation metadata: content coordinates are normalized, so cached renders and annotation refs survive untouched. (The relative “+90° from here” button belongs in the page-edit plugin, which derives the absolute value for you.)

await doc.pages.rotate([pon], 90); // 0 | 90 | 180 | 270

delete removes pages by identity. Deleting every page is rejected — a document keeps at least one — and deleted PONs are retired, never recycled; surviving pages keep their identity and revisions.

await doc.pages.delete([pon]);

Adding pages#

Two verbs, one result shape. insert copies every page of a standalone PDF (bytes) in at destIndex (omitted → append) — the merge/import path. insertBlank creates blank pages of an explicit size (PDF points), no source document needed:

// Append every page of another PDF.
const merged = await doc.pages.insert!(bytes);
 
// Two blank US-Letter pages at display index 1.
const added = await doc.pages.insertBlank!(
  { size: { width: 612, height: 792 }, count: 2 },
  1,
);
 
added.insertedPageObjectNumbers; // fresh PONs, in insertion order
added.layout; // the full new layout — same shape as pages.list()

The inserted pages get fresh, never-recycled object numbers; every pre-existing page keeps its identity and revisions — an insert never invalidates refs on its neighbours. Both verbs (and extract, below) are required members of the contract, implemented identically by the local and cloud engines.

Extracting pages#

extract is the read that turns pages into a portable asset: the given pages, in the order you pass them, serialized as a standalone PDF. The source document is untouched — nothing changes, no event fires.

const bytes = await doc.pages.extract!([ponA, ponB]);
// …hand to pages.insert on another document, or download it.

Structure verbs are gated by the document’s doc.pages.assemble capability (PDF permission bit 11); extract egresses content, so it is gated by doc.download instead.