Organizing pages
The page edit plugin changes the document’s pages: rotate, reorder, delete, duplicate, and insert
new ones, blank or from another PDF. These are edits to the file, kept when it’s downloaded. Register
pageEditPlugin() and use usePageEdit():
Loading live preview…
import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';
import './organize.css';
const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
const sameRef = (left: PageRef, right: PageRef) => left.objectNumber === right.objectNumber;
// Every page as a card. Click cards to select them; the toolbar edits every selected page at once.
function PageOrganizer() {
const pageEdit = usePageEdit();
const pages = usePageList();
const canEdit = pageEdit.canEdit();
// The second and third pages start selected. Refs, not indexes: they still name the
// same pages after a reorder.
const [selected, setSelected] = useState(() => pages.slice(1, 3).map((page) => page.ref));
const isSelected = (page: PageRef) => selected.some((ref) => sameRef(ref, page));
const toggle = (page: PageRef) =>
setSelected(
isSelected(page) ? selected.filter((ref) => !sameRef(ref, page)) : [...selected, page],
);
const count = selected.length;
return (
<>
<div className="toolbar">
<output className="readout">
{count} of {pages.length} selected
</output>
<span className="spacer" />
<button
type="button"
className="button"
disabled={!canEdit || count === 0}
onClick={() => pageEdit.rotateBy(selected, -90)}
>
⟲ Rotate left
</button>
<button
type="button"
className="button"
disabled={!canEdit || count === 0}
onClick={() => pageEdit.rotateBy(selected, 90)}
>
⟳ Rotate right
</button>
<button
type="button"
className="button"
disabled={!canEdit || count === 0}
onClick={() => pageEdit.reorder(selected, 'start')}
>
Move to front
</button>
<button
type="button"
className="button danger"
// A document keeps at least one page.
disabled={!canEdit || count === 0 || count === pages.length}
onClick={async () => {
await pageEdit.delete(selected);
setSelected([]);
}}
>
Delete
</button>
</div>
<ol className="pages">
{pages.map((page) => (
<li key={page.ref.objectNumber}>
<button
type="button"
className="card"
aria-pressed={isSelected(page.ref)}
onClick={() => toggle(page.ref)}
>
<PageView page={page.ref} width={120} className="thumbnail">
<RenderLayer />
</PageView>
<span className="label">
Page {page.index + 1}
{page.rotation !== 0 && <span className="turn"> · {page.rotation}°</span>}
</span>
</button>
</li>
))}
</ol>
</>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<PageOrganizer />
</DocumentGate>
</Viewer>
);
}await pageEdit.rotateBy([page.ref], 90);
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.delete([page.ref]);After an edit, everything that shows pages updates by itself: the Stage, thumbnails, page numbers. On a shared document, the others see the change too.
Calls that act on pages take a list of them, so one call works for a single page and for a multi-selection.
Rotate pages#
await pageEdit.rotateBy([page.ref], 90); // a quarter turn clockwise from where it is
await pageEdit.rotateBy([page.ref], -90); // and back
await pageEdit.setRotation([pageA.ref, pageB.ref], 180); // the same rotation for severalLoading live preview…
import { useEffect } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';
import './rotate.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), pageEditPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// Rotates the page you're on, in the file: the turn is kept when the document is downloaded.
function RotateToolbar() {
const pageEdit = usePageEdit();
const pages = usePageList();
const currentPageIndex = useStageState((state) => state.currentPageIndex);
const page = pages[currentPageIndex];
// The first page starts a quarter turn clockwise. setRotation() sets the same rotation
// however often it runs; rotateBy() would turn it again.
useEffect(() => {
void pageEdit.setRotation([0], 90);
}, [pageEdit]);
if (!page) return null;
const canEdit = pageEdit.canEdit();
return (
<div className="toolbar">
<output className="readout">
Page {currentPageIndex + 1} · {page.rotation}°
</output>
<span className="spacer" />
<button
type="button"
className="button"
disabled={!canEdit}
onClick={() => pageEdit.rotateBy([page.ref], -90)}
>
⟲ Rotate left
</button>
<button
type="button"
className="button"
disabled={!canEdit}
onClick={() => pageEdit.rotateBy([page.ref], 90)}
>
⟳ Rotate right
</button>
<button
type="button"
className="button"
disabled={!canEdit}
onClick={() =>
pageEdit.setRotation(
pages.map((each) => each.ref),
0,
)
}
>
Every page upright
</button>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<RotateToolbar />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}This rotates the page in the file. To only turn how pages are shown, use the Stage’s view rotation. Keep the two on different buttons.
Reorder pages#
reorder() takes the pages and where they go:
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.reorder([page.ref], 'end'); // to the back
await pageEdit.reorder([pageA.ref, pageB.ref], { after: pageC.ref }); // after another page
await pageEdit.reorder([page.ref], { before: pageC.ref });Several pages keep their order and land together.
Loading live preview…
import { useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';
import './reorder.css';
const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// Pick a page, then move it. Each button uses another placement.
function PageOrder() {
const pageEdit = usePageEdit();
const pages = usePageList();
// The last page starts selected. Its ref names it wherever it moves.
const [selected, setSelected] = useState(() => pages[pages.length - 1]?.ref ?? null);
const page = pages.find((each) => each.ref.objectNumber === selected?.objectNumber);
const before = page && pages[page.index - 1];
const after = page && pages[page.index + 1];
const canEdit = pageEdit.canEdit();
return (
<>
<div className="toolbar">
<output className="readout">
{page ? `Page ${page.index + 1} selected` : 'Pick a page'}
</output>
<span className="spacer" />
<button
type="button"
className="button"
disabled={!canEdit || !before}
onClick={() => page && pageEdit.reorder([page.ref], 'start')}
>
⇤ To the front
</button>
<button
type="button"
className="button"
disabled={!canEdit || !before}
onClick={() => page && before && pageEdit.reorder([page.ref], { before: before.ref })}
>
← Earlier
</button>
<button
type="button"
className="button"
disabled={!canEdit || !after}
onClick={() => page && after && pageEdit.reorder([page.ref], { after: after.ref })}
>
Later →
</button>
<button
type="button"
className="button"
disabled={!canEdit || !after}
onClick={() => page && pageEdit.reorder([page.ref], 'end')}
>
To the back ⇥
</button>
</div>
<ol className="strip">
{pages.map((each) => (
<li key={each.ref.objectNumber}>
<button
type="button"
className="card"
aria-pressed={each.ref.objectNumber === selected?.objectNumber}
onClick={() => setSelected(each.ref)}
>
<PageView page={each.ref} width={110} className="thumbnail">
<RenderLayer />
</PageView>
<span className="label">{each.index + 1}</span>
</button>
</li>
))}
</ol>
</>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<PageOrder />
</DocumentGate>
</Viewer>
);
}Delete pages#
await pageEdit.delete([pageA.ref, pageB.ref]);A document always keeps at least one page: deleting every page is refused.
Insert pages#
// A blank page after this one, the size of an A4 sheet
await pageEdit.insertBlank({ placement: { after: page.ref }, size: { width: 595, height: 842 } });
// Pages from another PDF: all of them, or the ones you name
await pageEdit.insertFromBytes(otherPdf, { placement: 'end' });
await pageEdit.insertFromBytes(otherPdf, { pageIndexes: [0, 2], placement: 'start' });
// Pages from another document open in the viewer
await pageEdit.insertFromDocument(otherDocumentId, [pageRef], { placement: 'end' });
// Copies of pages, right after them
await pageEdit.duplicate([page.ref]);Each resolves with { pages }: the new pages, so you can scroll to them or select them. Without a
size, a blank page takes the size of the page it’s placed next to.
Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';
import './insert.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), pageEditPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
/** Another PDF's bytes, to insert pages from. */
const otherPdf = async () => (await fetch('https://snippet.embedpdf.com/ebook.pdf')).arrayBuffer();
// Inserts next to the page you're on, then goes to the first new page.
function InsertToolbar() {
const pageEdit = usePageEdit();
const stage = useStage();
const pages = usePageList();
const currentPageIndex = useStageState((state) => state.currentPageIndex);
const page = pages[currentPageIndex];
const [added, setAdded] = useState<readonly PageRef[]>([]);
// Every insert resolves { pages }: the new pages, to go to or select.
const show = (result: { pages: readonly PageRef[] }) => {
setAdded(result.pages);
stage.goToPage(result.pages[0]);
};
// A blank page after the cover, once, on load.
const inserted = useRef(false);
useEffect(() => {
if (inserted.current || !pages[0]) return;
inserted.current = true;
void pageEdit.insertBlank({ placement: { after: pages[0].ref } }).then(show);
});
if (!page) return null;
const canEdit = pageEdit.canEdit();
const positions = added
.map((ref) => pages.findIndex((each) => each.ref.objectNumber === ref.objectNumber) + 1)
.filter((position) => position > 0);
return (
<div className="toolbar">
<button
type="button"
className="button"
disabled={!canEdit}
onClick={() => pageEdit.insertBlank({ placement: { after: page.ref } }).then(show)}
>
+ Blank page after
</button>
<button
type="button"
className="button"
disabled={!canEdit || !pageEdit.canExtract()}
onClick={() => pageEdit.duplicate([page.ref]).then(show)}
>
Duplicate page
</button>
<button
type="button"
className="button"
disabled={!canEdit}
onClick={async () =>
show(
await pageEdit.insertFromBytes(await otherPdf(), {
pageIndexes: [0, 2],
placement: { after: page.ref },
}),
)
}
>
+ Pages 1 and 3 of another PDF
</button>
<span className="spacer" />
<output className="readout">
{positions.length > 0
? `New: page ${positions.join(' and ')} of ${pages.length}`
: `${pages.length} pages`}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<InsertToolbar />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}Extract pages#
extract() gives you some pages as a new PDF, without changing this document:
const bytes = await pageEdit.extract([pageA.ref, pageB.ref]);
saveFile(bytes, 'chapter-2.pdf');Loading live preview…
import { useState } from 'react';
import { Viewer, DocumentGate, saveFile, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput, PageRef } from '@embedpdf/react/runtime';
import { PageView } from '@embedpdf/react/page-view';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { pageEditPlugin, usePageEdit } from '@embedpdf/react/page-edit';
import { localEngine } from '@embedpdf/engine';
import './extract.css';
const engine = localEngine();
const plugins = [renderPlugin(), pageEditPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// Tick pages, then download them as a PDF of their own. This document doesn't change.
function ExtractPages() {
const pageEdit = usePageEdit();
const pages = usePageList();
// The middle two pages start ticked.
const [ticked, setTicked] = useState(() => pages.slice(1, 3).map((page) => page.ref));
const [saved, setSaved] = useState<string | null>(null);
const isTicked = (ref: PageRef) => ticked.some((each) => each.objectNumber === ref.objectNumber);
const toggle = (ref: PageRef) =>
setTicked(
isTicked(ref)
? ticked.filter((each) => each.objectNumber !== ref.objectNumber)
: [...ticked, ref],
);
// In document order, whatever order they were ticked in.
const chosen = pages.filter((page) => isTicked(page.ref)).map((page) => page.ref);
const download = async () => {
const bytes = await pageEdit.extract(chosen);
saveFile(bytes, 'pages.pdf');
setSaved(`pages.pdf · ${chosen.length} pages · ${Math.round(bytes.byteLength / 1024)} KB`);
};
return (
<>
<div className="toolbar">
<button
type="button"
className="button primary"
disabled={chosen.length === 0 || !pageEdit.canExtract()}
onClick={download}
>
Download {chosen.length === 1 ? '1 page' : `${chosen.length} pages`} as a PDF
</button>
<output className="readout">{saved}</output>
</div>
<ol className="grid">
{pages.map((page) => (
<li key={page.ref.objectNumber}>
<label className="card" data-ticked={isTicked(page.ref)}>
<PageView page={page.ref} width={110} className="thumbnail">
<RenderLayer />
</PageView>
<span className="label">
<input
type="checkbox"
checked={isTicked(page.ref)}
onChange={() => toggle(page.ref)}
/>
Page {page.index + 1}
</span>
</label>
</li>
))}
</ol>
</>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<ExtractPages />
</DocumentGate>
</Viewer>
);
}Where a page goes#
| Placement | Means |
|---|---|
{ after: pageRef } | Right after that page |
{ before: pageRef } | Right before that page |
'start' | Before the first page |
'end' | After the last page |
A placement names a page, not a position, so it stays right when pages move while the edit waits its turn. The page can also be an index, as everywhere in this plugin: the page at that index when you call.
Methods#
Everything usePageEdit() gives you.
| Method | What it does |
|---|---|
rotateBy(pages, delta) | Turns each page by 90, -90 or 180 degrees from where it is. Rotate pages |
setRotation(pages, rotation) | Gives every page the same rotation: 0, 90, 180 or 270 |
reorder(pages, placement) | Moves pages to a new place. Several pages keep their order and land together. Reorder pages |
delete(pages) | Deletes pages. Deleting every page is refused. Delete pages |
insertBlank({ placement?, size?, count? }) | Inserts blank pages, one unless you pass a count. Resolves { pages }, the new pages. Insert pages |
insertFromBytes(bytes, { pageIndexes?, placement? }) | Inserts the pages of another PDF, or only the ones at pageIndexes. Resolves { pages }. |
insertFromDocument(documentId, pages, { placement? }) | Inserts copies of pages from another document open in the viewer. Resolves { pages }. |
duplicate(pages, { placement? }) | Copies pages, right after the last of them unless you pass a placement. Resolves { pages }. |
extract(pages) | Copies pages into a new PDF and resolves its bytes. This document doesn’t change. Extract pages |
canEdit(), canExtract() | Whether rotating, reordering, deleting and inserting are allowed, and whether copying pages out is. Permissions |
placement is one of the values in Where a page goes; without one, new pages go
at the end. Every method that returns a promise also takes a signal in its options, to cancel it.
Events#
Page changes are document events, so every part of your app hears them, whichever plugin made the change:
import { useState } from 'react';
import { useDocumentsEvent } from '@embedpdf/react/runtime';
export function PageCount() {
const [pageCount, setPageCount] = useState(0);
useDocumentsEvent(
(documents) => documents.onPagesChanged,
({ pages }) => setPageCount(pages.length),
);
return <span>{pageCount} pages</span>;
}The page edit plugin has no events of its own. onPagesChanged and what it carries are listed under
Opening documents.
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.pages.assemble | Rotate, reorder, delete and insert pages | pageEdit.canEdit() |
doc.download | extract(), duplicate() and insertFromDocument(), which copy pages out | pageEdit.canExtract() |
Good to know#
Pages have a ref, not just a number#
Each page has a ref: its identity in the file. A page’s index changes the moment a page before it
moves or goes; its ref never does. So a call still hits the page the user clicked, even if the
document changed in between. You get refs from usePageList() (page.ref), and anything that
points at a page carries one: a search result’s page, an annotation’s page.
What moves with a page#
Annotations, links and form fields belong to their page, so they move, rotate and go with it. Rotation leaves annotations where they are on the page; they turn with it.
Page edits are saved like every other change; see Saving & downloading.
Your feedback goes directly to the documentation team.