---
title: "Organizing pages — Svelte"
description: "Rotate, reorder, delete, duplicate and insert pages, and extract pages into a new PDF. Edits are written into the document."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/documents/pages"
---

# 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()`:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { pageEditPlugin } from '@embedpdf/svelte/page-edit';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { localEngine } from '@embedpdf/engine';
  import PageOrganizer from './PageOrganizer.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <PageOrganizer />
  </DocumentGate>
</Viewer>
```

**`organize.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --danger: #c62a2a;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.danger {
  color: var(--danger);
}

/* The pages in one row, scrolling sideways when they don't fit */
.pages {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  overflow-x: auto;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

/* A card is as wide as its page: a page turned a quarter makes it wider */
.pages > li {
  flex: none;
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 140px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

.card:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.turn {
  font-weight: 500;
  color: var(--muted);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`PageOrganizer.svelte`**

```svelte
<!-- Every page as a card. Click cards to select them; the toolbar edits every selected page at once. -->
<script lang="ts">
  import { usePageList, type PageRef } from '@embedpdf/svelte/runtime';
  import { usePageEdit } from '@embedpdf/svelte/page-edit';
  import { PageView } from '@embedpdf/svelte/page-view';
  import { RenderLayer } from '@embedpdf/svelte/render';

  const sameRef = (left: PageRef, right: PageRef) => left.objectNumber === right.objectNumber;

  const pageEdit = usePageEdit();
  const pages = usePageList();
  const canEdit = $derived(pageEdit.canEdit());
  // The second and third pages start selected. Refs, not indexes: they still name the
  // same pages after a reorder.
  let selected = $state.raw(pages.current.slice(1, 3).map((page) => page.ref));

  const isSelected = (page: PageRef) => selected.some((ref) => sameRef(ref, page));
  const toggle = (page: PageRef) => {
    selected = isSelected(page)
      ? selected.filter((ref) => !sameRef(ref, page))
      : [...selected, page];
  };
  const count = $derived(selected.length);

  async function remove() {
    await pageEdit.delete(selected);
    selected = [];
  }
</script>

<div class="toolbar">
  <output class="readout">
    {count} of {pages.current.length} selected
  </output>
  <span class="spacer"></span>
  <button
    type="button"
    class="button"
    disabled={!canEdit || count === 0}
    onclick={() => pageEdit.rotateBy(selected, -90)}
  >
    ⟲ Rotate left
  </button>
  <button
    type="button"
    class="button"
    disabled={!canEdit || count === 0}
    onclick={() => pageEdit.rotateBy(selected, 90)}
  >
    ⟳ Rotate right
  </button>
  <button
    type="button"
    class="button"
    disabled={!canEdit || count === 0}
    onclick={() => pageEdit.reorder(selected, 'start')}
  >
    Move to front
  </button>
  <!-- A document keeps at least one page. -->
  <button
    type="button"
    class="button danger"
    disabled={!canEdit || count === 0 || count === pages.current.length}
    onclick={remove}
  >
    Delete
  </button>
</div>
<ol class="pages">
  {#each pages.current as page (page.ref.objectNumber)}
    <li>
      <button
        type="button"
        class="card"
        aria-pressed={isSelected(page.ref)}
        onclick={() => toggle(page.ref)}
      >
        <PageView page={page.ref} width={120} class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">
          Page {page.index + 1}
          {#if page.rotation !== 0}
            <span class="turn"> · {page.rotation}°</span>
          {/if}
        </span>
      </button>
    </li>
  {/each}
</ol>
```

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

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

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { pageEditPlugin } from '@embedpdf/svelte/page-edit';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import RotateToolbar from './RotateToolbar.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <RotateToolbar />
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`rotate.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`RotateToolbar.svelte`**

```svelte
<!-- Rotates the page you're on, in the file: the turn is kept when the document is downloaded. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { usePageList } from '@embedpdf/svelte/runtime';
  import { usePageEdit } from '@embedpdf/svelte/page-edit';
  import { useStageState } from '@embedpdf/svelte/stage';

  const pageEdit = usePageEdit();
  const pages = usePageList();
  const currentPageIndex = useStageState((state) => state.currentPageIndex);
  const page = $derived(pages.current[currentPageIndex.current]);
  const canEdit = $derived(pageEdit.canEdit());

  // The first page starts a quarter turn clockwise. setRotation() sets the same rotation
  // however often it runs; rotateBy() would turn it again.
  onMount(() => {
    void pageEdit.setRotation([0], 90);
  });
</script>

{#if page}
  <div class="toolbar">
    <output class="readout">
      Page {currentPageIndex.current + 1} · {page.rotation}°
    </output>
    <span class="spacer"></span>
    <button
      type="button"
      class="button"
      disabled={!canEdit}
      onclick={() => pageEdit.rotateBy([page.ref], -90)}
    >
      ⟲ Rotate left
    </button>
    <button
      type="button"
      class="button"
      disabled={!canEdit}
      onclick={() => pageEdit.rotateBy([page.ref], 90)}
    >
      ⟳ Rotate right
    </button>
    <button
      type="button"
      class="button"
      disabled={!canEdit}
      onclick={() =>
        pageEdit.setRotation(
          pages.current.map((each) => each.ref),
          0,
        )}
    >
      Every page upright
    </button>
  </div>
{/if}
```

This rotates the page in the file. To only turn how pages are shown, use the Stage's
[view rotation](../viewing/stage#rotate-the-view). Keep the two on different buttons.

## Reorder pages

`reorder()` takes the pages and where they go:

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

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { pageEditPlugin } from '@embedpdf/svelte/page-edit';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { localEngine } from '@embedpdf/engine';
  import PageOrder from './PageOrder.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <PageOrder />
  </DocumentGate>
</Viewer>
```

**`reorder.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The pages in their order: a row that scrolls sideways where it doesn't fit */
.strip {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  margin: 0;
  padding: 16px;
  overflow-x: auto;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

.card:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`PageOrder.svelte`**

```svelte
<!-- Pick a page, then move it. Each button uses another placement. -->
<script lang="ts">
  import { usePageList } from '@embedpdf/svelte/runtime';
  import { usePageEdit } from '@embedpdf/svelte/page-edit';
  import { PageView } from '@embedpdf/svelte/page-view';
  import { RenderLayer } from '@embedpdf/svelte/render';

  const pageEdit = usePageEdit();
  const pages = usePageList();
  // The last page starts selected. Its ref names it wherever it moves.
  let selected = $state.raw(pages.current[pages.current.length - 1]?.ref ?? null);

  const page = $derived(
    pages.current.find((each) => each.ref.objectNumber === selected?.objectNumber),
  );
  const before = $derived(page && pages.current[page.index - 1]);
  const after = $derived(page && pages.current[page.index + 1]);
  const canEdit = $derived(pageEdit.canEdit());
</script>

<div class="toolbar">
  <output class="readout">
    {page ? `Page ${page.index + 1} selected` : 'Pick a page'}
  </output>
  <span class="spacer"></span>
  <button
    type="button"
    class="button"
    disabled={!canEdit || !before}
    onclick={() => page && pageEdit.reorder([page.ref], 'start')}
  >
    ⇤ To the front
  </button>
  <button
    type="button"
    class="button"
    disabled={!canEdit || !before}
    onclick={() => page && before && pageEdit.reorder([page.ref], { before: before.ref })}
  >
    ← Earlier
  </button>
  <button
    type="button"
    class="button"
    disabled={!canEdit || !after}
    onclick={() => page && after && pageEdit.reorder([page.ref], { after: after.ref })}
  >
    Later →
  </button>
  <button
    type="button"
    class="button"
    disabled={!canEdit || !after}
    onclick={() => page && pageEdit.reorder([page.ref], 'end')}
  >
    To the back ⇥
  </button>
</div>
<ol class="strip">
  {#each pages.current as each (each.ref.objectNumber)}
    <li>
      <button
        type="button"
        class="card"
        aria-pressed={each.ref.objectNumber === selected?.objectNumber}
        onclick={() => (selected = each.ref)}
      >
        <PageView page={each.ref} width={110} class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">{each.index + 1}</span>
      </button>
    </li>
  {/each}
</ol>
```

## Delete pages

```ts
await pageEdit.delete([pageA.ref, pageB.ref]);
```

A document always keeps at least one page: deleting every page is refused.

## Insert pages

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

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { pageEditPlugin } from '@embedpdf/svelte/page-edit';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import InsertToolbar from './InsertToolbar.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <InsertToolbar />
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`insert.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`InsertToolbar.svelte`**

```svelte
<!-- Inserts next to the page you're on, then goes to the first new page. -->
<script lang="ts">
  import { usePageList, type PageRef } from '@embedpdf/svelte/runtime';
  import { usePageEdit } from '@embedpdf/svelte/page-edit';
  import { useStage, useStageState } from '@embedpdf/svelte/stage';

  /** Another PDF's bytes, to insert pages from. */
  const otherPdf = async () =>
    (await fetch('https://snippet.embedpdf.com/ebook.pdf')).arrayBuffer();

  const pageEdit = usePageEdit();
  const stage = useStage();
  const pages = usePageList();
  const currentPageIndex = useStageState((state) => state.currentPageIndex);
  const page = $derived(pages.current[currentPageIndex.current]);
  const canEdit = $derived(pageEdit.canEdit());
  let added = $state.raw<readonly PageRef[]>([]);

  // Every insert resolves { pages }: the new pages, to go to or select.
  function show(result: { pages: readonly PageRef[] }) {
    added = result.pages;
    stage.goToPage(result.pages[0]);
  }

  // A blank page after the cover, once, on load.
  let inserted = false;
  $effect(() => {
    const cover = pages.current[0];
    if (inserted || !cover) return;
    inserted = true;
    void pageEdit.insertBlank({ placement: { after: cover.ref } }).then(show);
  });

  const positions = $derived(
    added
      .map(
        (ref) =>
          pages.current.findIndex((each) => each.ref.objectNumber === ref.objectNumber) + 1,
      )
      .filter((position) => position > 0),
  );

  async function insertFromOtherPdf(after: PageRef) {
    show(
      await pageEdit.insertFromBytes(await otherPdf(), {
        pageIndexes: [0, 2],
        placement: { after },
      }),
    );
  }
</script>

{#if page}
  <div class="toolbar">
    <button
      type="button"
      class="button"
      disabled={!canEdit}
      onclick={() => pageEdit.insertBlank({ placement: { after: page.ref } }).then(show)}
    >
      + Blank page after
    </button>
    <button
      type="button"
      class="button"
      disabled={!canEdit || !pageEdit.canExtract()}
      onclick={() => pageEdit.duplicate([page.ref]).then(show)}
    >
      Duplicate page
    </button>
    <button
      type="button"
      class="button"
      disabled={!canEdit}
      onclick={() => insertFromOtherPdf(page.ref)}
    >
      + Pages 1 and 3 of another PDF
    </button>
    <span class="spacer"></span>
    <output class="readout">
      {positions.length > 0
        ? `New: page ${positions.join(' and ')} of ${pages.current.length}`
        : `${pages.current.length} pages`}
    </output>
  </div>
{/if}
```

## Extract pages

`extract()` gives you some pages as a new PDF, without changing this document:

```ts
const bytes = await pageEdit.extract([pageA.ref, pageB.ref]);
saveFile(bytes, 'chapter-2.pdf');
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { pageEditPlugin } from '@embedpdf/svelte/page-edit';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { localEngine } from '@embedpdf/engine';
  import ExtractPages from './ExtractPages.svelte';

  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()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ExtractPages />
  </DocumentGate>
</Viewer>
```

**`extract.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --accent-strong: #0565da;
  --on-accent: #ffffff;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* The pages: as many in a row as fit */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  align-items: end;
  gap: 12px;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[data-ticked='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.label input {
  margin: 0;
  accent-color: var(--accent);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ExtractPages.svelte`**

```svelte
<!-- Tick pages, then download them as a PDF of their own. This document doesn't change. -->
<script lang="ts">
  import { saveFile, usePageList, type PageRef } from '@embedpdf/svelte/runtime';
  import { usePageEdit } from '@embedpdf/svelte/page-edit';
  import { PageView } from '@embedpdf/svelte/page-view';
  import { RenderLayer } from '@embedpdf/svelte/render';

  const pageEdit = usePageEdit();
  const pages = usePageList();
  // The middle two pages start ticked.
  let ticked = $state.raw(pages.current.slice(1, 3).map((page) => page.ref));
  let saved = $state<string | null>(null);

  const isTicked = (ref: PageRef) => ticked.some((each) => each.objectNumber === ref.objectNumber);
  const toggle = (ref: PageRef) => {
    ticked = isTicked(ref)
      ? ticked.filter((each) => each.objectNumber !== ref.objectNumber)
      : [...ticked, ref];
  };

  // In document order, whatever order they were ticked in.
  const chosen = $derived(pages.current.filter((page) => isTicked(page.ref)).map((page) => page.ref));

  async function download() {
    const bytes = await pageEdit.extract(chosen);
    saveFile(bytes, 'pages.pdf');
    saved = `pages.pdf · ${chosen.length} pages · ${Math.round(bytes.byteLength / 1024)} KB`;
  }
</script>

<div class="toolbar">
  <button
    type="button"
    class="button primary"
    disabled={chosen.length === 0 || !pageEdit.canExtract()}
    onclick={download}
  >
    Download {chosen.length === 1 ? '1 page' : `${chosen.length} pages`} as a PDF
  </button>
  <output class="readout">{saved}</output>
</div>
<ol class="grid">
  {#each pages.current as page (page.ref.objectNumber)}
    <li>
      <label class="card" data-ticked={isTicked(page.ref)}>
        <PageView page={page.ref} width={110} class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">
          <input type="checkbox" checked={isTicked(page.ref)} onchange={() => toggle(page.ref)} />
          Page {page.index + 1}
        </span>
      </label>
    </li>
  {/each}
</ol>
```

## 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](#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](#reorder-pages)                   |
| `delete(pages)`                                         | Deletes pages. Deleting every page is refused. [Delete pages](#delete-pages)                                                    |
| `insertBlank({ placement?, size?, count? })`            | Inserts blank pages, one unless you pass a `count`. Resolves `{ pages }`, the new pages. [Insert 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](#extract-pages)               |
| `canEdit()`, `canExtract()`                             | Whether rotating, reordering, deleting and inserting are allowed, and whether copying pages out is. [Permissions](#permissions) |

`placement` is one of the values in [Where a page goes](#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.svelte`**

```svelte
<script lang="ts">
  import { useDocumentsEvent } from '@embedpdf/svelte/runtime';

  let pageCount = $state(0);

  useDocumentsEvent(
    (documents) => documents.onPagesChanged,
    ({ pages }) => (pageCount = pages.length),
  );
</script>

<span>{pageCount} pages</span>
```

The page edit plugin has no events of its own. `onPagesChanged` and what it carries are listed under
[Opening documents](opening#events).

## 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](saving).
