EmbedPDF

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 several

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

PlacementMeans
{ 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.

MethodWhat 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:

page-count.tsx
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#

PermissionWhat it allowsCheck
doc.pages.assembleRotate, reorder, delete and insert pagespageEdit.canEdit()
doc.downloadextract(), duplicate() and insertFromDocument(), which copy pages outpageEdit.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.

Was this page helpful?

Your feedback goes directly to the documentation team.