---
title: "Pages & rendering"
description: "List pages, render them, and edit page structure — reorder, rotate, delete, add."
source: "https://www.embedpdf.com/docs/engine/core-concepts/pages-and-rendering"
---

# 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.

```ts
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:

```ts
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.

```ts
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.

```ts
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`):

| Option               | Type                                                      | Default        | Notes                                                                                                                                          |
| -------------------- | --------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `viewport`           | `{ kind: 'scale', scale? }` or `{ kind: 'width', width }` | scale `1`      | `scale` 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 page     | Render a sub-rectangle in PDF user space (origin bottom-left, `top > bottom`).                                                                 |
| `rotation`           | `0 \| 90 \| 180 \| 270`                                   | page rotation  | Extra rotation applied on top of the page's own.                                                                                               |
| `background`         | `'white' \| 'transparent'`                                | `'white'`      | Use `transparent` with PNG/WebP for alpha.                                                                                                     |
| `includeAnnotations` | `boolean`                                                 | server default | Whether annotations are baked into the raster.                                                                                                 |
| `format`             | `'png' \| 'webp' \| 'bmp'`                                | server default | `image()` only. Network endpoints serve `png`/`webp`.                                                                                          |
| `quality`            | `number`                                                  | —              | `image()` 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:

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

The local engine always answers `{ kind: 'continuous' }`: rendering happens
in-process, there is no shared cache to protect, and any viewport renders
exactly as requested. Exactness is the local product promise — the render
plugin builds on it to keep resting pixels 1:1 with the screen.

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](https://www.embedpdf.com/docs/headless/plugins/render) 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.

```ts
// 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.)

```ts
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.

```ts
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:

```ts
// 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.

```ts
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.
