Pages
List the pages of a document to learn their size and order, then work on them by ref:
const { pageCount, pages } = await doc.pages.list();
await doc.pages.rotate([pages[0].ref], 90); // turn the first page
await doc.pages.delete([pages[pageCount - 1].ref]); // delete the last oneList pages#
doc.pages.list() returns every page in display order. It doesn’t load any page content, so it’s
quick even for a long document.
const { pages } = await doc.pages.list();
for (const page of pages) {
console.log(page.index, page.label, page.size.width, page.size.height, page.rotation);
}| Field | What it is |
|---|---|
index | Position in the document, from 0. Changes when pages move. |
ref | The page’s address. Stays the same when pages move; pass it to every page call. |
label | The page’s label from the PDF ('iii', 'A-1'), or null when the file has none. See Page labels. |
size | { width, height } of the visible area (the crop box) in points, before rotation. |
rotation | 0, 90, 180 or 270: how far the page is turned clockwise when shown. |
userUnit | How large one point is, in multiples of 1/72 inch. 1 for almost every file. |
tabOrder | The order its annotations and form fields take keyboard focus in: 'rows', 'columns', 'structure', 'annotations' or 'widgets'; null when the page doesn’t say. See Keyboard order. |
boxes | The page’s five boxes: media, crop, bleed, trim and art. See below. |
pdfCropBox | Where the page sits in the PDF file’s own numbers, for tools that use those. See below. |
boxes follows the PDF standard. A box the file leaves out takes its default: the crop box is the
media box, and the bleed, trim and art boxes are the crop box. Every box stays inside the media
box, so a crop box that reaches past it is cut back. crop is what the page shows, and size is
its size.
Each box is { x, y, width, height }, measured in points from the top-left corner of what the
page shows, with y going down. So boxes.crop is always { x: 0, y: 0, width, height }, and a
media box that reaches past the page’s top-left corner has a negative x and y:
{
size: { width: 512, height: 672 },
boxes: {
media: { x: -50, y: -60, width: 612, height: 792 },
crop: { x: 0, y: 0, width: 512, height: 672 },
trim: { x: 10, y: 10, width: 492, height: 652 },
// bleed and art
},
pdfCropBox: { left: 50, bottom: 60, right: 562, top: 732 },
}pdfCropBox is the only value in the PDF file’s own numbers: the crop box where the file places
it, as { left, bottom, right, top } with y going up. You need it only beside a tool that reads
those numbers, such as iText, PDFBox or Acrobat JavaScript. pdfRectOf() and pdfPointOf() turn a
box or point into them, and pageBoxOf() and pagePointOf() turn them back. pdfDestinationOf()
and pageDestinationOf() do the same for a destination, with the crop box of the page it goes to:
import { pdfRectOf } from '@embedpdf/engine';
const inTheFile = pdfRectOf({ x: 50, y: 82, width: 50, height: 50 }, page.pdfCropBox);
// { left: 100, right: 150, bottom: 600, top: 650 }size is the page before it’s turned. For a page at 90 or 270, swap width and height to get
the size it’s shown at:
const { size, rotation } = pages[0];
const turned = rotation === 90 || rotation === 270;
const shown = turned ? { width: size.height, height: size.width } : size;Page labels#
A PDF can number its pages its own way: the front matter i, ii, iii, the body from 1,
an appendix A-1, A-2. Each page’s label is its label, and labels in the page list holds
the numbering as sections, in page order:
const { labels } = await doc.pages.list();
// [
// { firstPage: 0, style: 'lowerRoman', firstNumber: 1, prefix: '' },
// { firstPage: 3, style: 'decimal', firstNumber: 1, prefix: '' },
// { firstPage: 20, style: 'decimal', firstNumber: 1, prefix: 'A-' },
// ]| Field | What it is |
|---|---|
firstPage | The index of the section’s first page. A section runs up to the next one’s first page. |
style | 'decimal', 'upperRoman', 'lowerRoman', 'upperLetters' or 'lowerLetters'; null for no number at all. |
firstNumber | The number of its first page. |
prefix | What every label of the section starts with; '' for none. |
The first section always starts at page 0. A file without labels has no sections, and every
page’s label is null.
When pages move, come or go, the sections follow the pages, so a label stays where it belongs:
- A section stays with the page it starts at, and a page takes the numbering of the section it lands in. A page inserted between the front matter and the body is numbered as front matter.
- When a section’s first page is deleted, the section starts on its next page, which becomes
1. A section with no pages left goes. - Pages put before every page join the first section.
- A move is a delete followed by an insert.
An undo puts the labels back as they were.
Keyboard order#
Each page says in what order its annotations and form fields take keyboard focus: its tabOrder.
tabOrderOf() puts annotations in that order, page by page, as Tab visits them:
import { tabOrderOf } from '@embedpdf/engine';
const { pages } = await doc.pages.list();
const { widgets } = await doc.forms.list();
const order = tabOrderOf(widgets, pages); // every field box, in the order Tab visits themtabOrder | The order on the page |
|---|---|
'rows' | Top to bottom, each row left to right |
'columns' | Left to right, each column top to bottom |
'structure' | As the document’s tags read: by each annotation’s structurePosition. Those that aren’t tagged come after them, in the order the page lists them. |
'annotations', null | The order the page lists them |
'widgets' | The form fields first, then the other annotations, each in the order the page lists them |
Most tagged PDFs, from Acrobat, Word or a browser’s print, say 'structure': their order is the
reading order their author tagged. structurePosition is where an annotation sits in those tags,
from the root down, the index of each step among its parent’s kids; compared index by index, two
positions give the reading order.
Refs, not positions#
Every call that works on pages takes refs, never indexes. A ref stays valid when pages move, rotate, or when pages before it are inserted or deleted, so you can keep it for as long as the page exists.
const page = doc.page(ref); // a handle for one page: render, text, annotationsdoc.page(ref) makes no request. When you stored a page’s object number yourself,
toPageRef(12) turns it back into a ref. See Refs & identity.
Rotate#
rotate() sets how pages are turned: 0, 90, 180 or 270 degrees clockwise. It sets the
rotation, it doesn’t add to it, so one value works for several pages at once:
await doc.pages.rotate([pageA.ref, pageB.ref], 90);To turn a page a further quarter, add 90 to its current rotation from list(). After 270,
start again at 0.
Rotating changes how the page is shown, not its content, so annotations and text keep their
positions. Renders don’t turn by themselves: pass the page’s rotation to show it turned (see
Turn the page).
Reorder#
reorder(refs, position) takes the pages out and puts them back together, in the order you pass
them, at position: right after or before another page, or at the start or the end.
const { pages } = await doc.pages.list();
const last = pages[pages.length - 1].ref;
await doc.pages.reorder([last], 'start'); // the last page to the front
await doc.pages.reorder([pages[0].ref], { after: pages[1].ref }); // swap the first twoA position names a page, never a number, so it means the same when other pages come or go
before the call lands. The page you name can’t be one of the pages you move (InvalidArg);
one the document doesn’t have fails with NotFound.
Delete#
await doc.pages.delete([page.ref]);A page’s annotations go with it. A form field with a widget on it stays in the form, with its
value, and that widget shows on no page. Outline items that go to it stay, and their target reads
unsupported from then on (see Outline). A
document keeps at least one page: deleting every page fails with InvalidArg.
Insert another PDF#
insert(bytes, position?) copies every page of another PDF into this one, with their
annotations and form fields. Without a position, the pages go at the end:
const response = await fetch('/appendix.pdf');
const { insertedPages } = await doc.pages.insert(await response.arrayBuffer());insertedPages holds the refs of the new pages, in order. To merge several files, insert them one
after the other.
Form fields#
The pages bring the form fields of their widgets into the document’s form, with their values, so they fill in here and in other PDF apps alike.
A top-level field whose name the document already uses is kept apart: it comes in as name_2, or
the next number that’s free, and the fields under it keep their place. Insert a filled-in form
twice and you get email and email_2, address.city and address_2.city, each with its own
value. Joined, two fields of one name would be one field with one value, and the second file’s
answers would be lost.
await doc.pages.insert(firstCopy);
await doc.pages.insert(secondCopy);
const { fields } = await doc.forms.list();
fields.map((field) => field.name); // ['email', 'address.city', 'email_2', 'address_2.city']A script in the inserted PDF that looks up a renamed field by its name finds the document’s field of that name.
Insert blank pages#
insertBlank() adds empty pages of the size you give, in points:
// Two US Letter pages after the first page
const { pages } = await doc.pages.list();
const letter = { width: 612, height: 792 };
await doc.pages.insertBlank({ size: letter, count: 2 }, { after: pages[0].ref });A4 is { width: 595, height: 842 }. count is 1 by default and at most 100 per call.
Copy pages#
copy(refs, position?) copies pages of the document, in the order you pass them, to position.
Without a position, the copies go at the end:
const { insertedPages } = await doc.pages.copy([page.ref], { after: page.ref }); // duplicate a pageA copy looks like its page and brings the page’s annotations. Its form fields come as fields of
their own, with their values: a name the document already uses gets the first free name_2, as an
insert’s do, so filling in the copy leaves the original alone. Name a page twice to
copy it twice. Unlike extract() then insert(), the pages never leave the document, so copy()
needs only doc.pages.assemble.
Extract pages to a new PDF#
extract(refs) returns a new PDF with copies of those pages, in the order you pass them. The
document itself doesn’t change.
const bytes = await doc.pages.extract([pages[0].ref, pages[1].ref]);The copy has the form fields of those pages’ widgets, with their values. A field with widgets on other pages too keeps only the ones on the pages you copy, and a field with no widget on them stays behind.
That makes splitting a document a loop, one file per page:
for (const { ref, index } of pages) {
const bytes = await doc.pages.extract([ref]);
await save(`page-${index + 1}.pdf`, bytes);
}And taking pages from one document into another is extract() then insert():
const bytes = await source.pages.extract([ref]);
await target.pages.insert(bytes, 'start');A copy as the pages print#
With flatten, the copy is the pages as they look: the annotations and form fields painted into
the pages, and nothing left to click, fill in or edit. It takes the same options as
flatten(), so { usage: 'print' } makes a copy for paper: only
what prints is on it, and layers are in their print state. The document itself doesn’t change.
const printable = await doc.pages.extract(refs, { flatten: { usage: 'print' } });includeAnnotations and includeFormFields choose what’s painted in. Each defaults to what the
caller may read, as in a page picture, and they only go with
flatten:
await doc.pages.extract(refs, { flatten: { usage: 'print' }, includeAnnotations: false });The copy keeps the document’s title, so a print queue or a saved file has a name.
Flatten a page’s annotations#
Flattening draws a page’s annotations into the page itself and removes them, so they can’t be changed or deleted anymore. Filled-in form fields become plain content the same way.
const { results } = await doc.pages.flatten([page.ref]);
// results: [{ page, status: 'applied' }]Each page’s status is 'applied', or 'unchanged' when it had nothing to flatten. Flattening is
all or nothing: when a page fails, or the call is cancelled, no page changes and the call rejects
with the error. Flattening paints what the page shows: an annotation without a stored appearance is
painted as the engine draws it. Hidden annotations and popups stay as they are, and so do the few
annotations the engine draws only from a stored appearance, such as a stamp without one.
Flattening can’t be undone, and it ends undo for every change made before it; see
Changes & undo.
By default the page is flattened as it’s shown on screen, which leaves out annotations shown only
when printed. Pass { usage: 'print' } to flatten it as it prints, which leaves out annotations that
don’t print:
await doc.pages.flatten([page.ref], { usage: 'print' });New annotations print unless you create them with print: false; see
Drawing annotations yourself.
To flatten only some annotations, see Drawing annotations yourself.
Permissions#
| Call | Needs |
|---|---|
list() | doc.open |
rotate(), reorder(), delete(), insert(), insertBlank(), copy() | doc.pages.assemble |
extract() | doc.download: the pages leave the document |
extract() with flatten | doc.download or doc.download.flattened |
flatten() | doc.pages.modify and doc.annotate.modify |
See Permissions.
Good to know#
What a change returns#
rotate(), reorder(), delete(), insert(), insertBlank(), copy(), setName() and
removeName() return the new page list as layout, the same shape list() returns, so you can replace the list
you hold without asking again:
const { layout } = await doc.pages.reorder([ref], 'start');
showThumbnails(layout.pages);Other code that shows pages can listen for pages.reordered, pages.rotated, pages.deleted,
pages.inserted, pages.restored and pages.flattened instead. See
Events.
Undo#
rotate(), reorder(), delete(), insert(), insertBlank() and copy() are changes: each
returns its opId in meta, and doc.apply({ undoOf: opId }) undoes it. An undo of a delete
brings the same pages back, under the same refs, with their annotations and names, and fires
pages.restored. To make several page edits one step, apply them as one change. See
Changes & undo.
const { meta } = await doc.pages.delete([ref]);
await doc.apply({ undoOf: meta.opId }); // the page is back where it wasAn undo of an insert or a copy deletes the pages it brought and the form fields that came with them; a redo brings both back.
Inserting#
A password-protected PDF can’t be inserted: insert() fails with DocPasswordRequired. Open it,
unlock it, and extract() its pages instead. Pages you insert get new refs; the pages already in
the document keep theirs.
To know the new refs before the engine answers, name the pages yourself with numbers from
doc.objectNumbers, one per new page: insertBlank(),
insert() and copy() all take them as objectNumbers. A count other than the number of new
pages, or a number the session doesn’t hold, is refused.
await doc.objectNumbers.reserve(1);
const number = doc.objectNumbers.take()!;
const copy = doc.pages.copy([page.ref], 'end', { objectNumbers: [number] });
const ref = toPageRef(number); // known now, the same ref the answer namesIn the browser, an ArrayBuffer you pass to insert() is transferred to the engine, not copied,
so it’s empty afterwards. Pass a copy to keep yours.
Named pages#
A PDF can give pages names. Acrobat’s stamp files work this way: each page is one stamp, named
identifier=label. The names come with the page list:
const { namedPages } = await doc.pages.list();
// [{ name: 'Approved=Approved', target: { kind: 'page', page: ref } }, …]target is { kind: 'page', page } for a page in the document. A name can also point at a hidden
page template ({ kind: 'template' }), which isn’t a page you can show, or at nothing
({ kind: 'dangling' }) in a file another app made.
Add, change or remove a name with setName() and removeName(). The page itself stays:
await doc.pages.setName({ name: 'Draft=Draft', page: page.ref });
await doc.pages.setName({ name: 'Draft=Concept', page: page.ref, replace: 'Draft=Draft' }); // rename
await doc.pages.removeName({ name: 'Draft=Concept' });Both need doc.pages.assemble. Deleting a page removes its names.
Your feedback goes directly to the documentation team.