---
title: "Organizing pages — Angular"
description: "Rotate, reorder, delete, duplicate and insert pages, and extract pages into a new PDF. Edits are written into the document."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/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
`withPageEdit()` and use `inject(EpdfPageEdit)`:

**`organize.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageEdit, withPageEdit } from '@embedpdf/angular/page-edit';
import { localEngine } from '@embedpdf/engine';

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.
@Component({
  selector: 'demo-page-organizer',
  imports: [EpdfPageView, EpdfRenderLayer],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @let canEdit = pageEdit.canEdit();
    @let count = selected().length;
    <div class="toolbar">
      <output class="readout">{{ count }} of {{ document.pages().length }} selected</output>
      <span class="spacer"></span>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || count === 0"
        (click)="pageEdit.rotateBy(selected(), -90)"
      >
        ⟲ Rotate left
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || count === 0"
        (click)="pageEdit.rotateBy(selected(), 90)"
      >
        ⟳ Rotate right
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || count === 0"
        (click)="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 === document.pages().length"
        (click)="deleteSelected()"
      >
        Delete
      </button>
    </div>
    <ol class="pages">
      @for (page of document.pages(); track page.ref.objectNumber) {
        <li>
          <button
            type="button"
            class="card"
            [attr.aria-pressed]="isSelected(page.ref)"
            (click)="toggle(page.ref)"
          >
            <epdf-page-view [page]="page.ref" [width]="120" class="thumbnail">
              <epdf-render-layer />
            </epdf-page-view>
            <span class="label">
              Page {{ page.index + 1 }}
              @if (page.rotation !== 0) {
                <span class="turn"> · {{ page.rotation }}°</span>
              }
            </span>
          </button>
        </li>
      }
    </ol>
  `,
})
export class PageOrganizer {
  protected readonly pageEdit = inject(EpdfPageEdit);
  protected readonly document = inject(EpdfDocument);
  // The second and third pages start selected. Refs, not indexes: they still name the
  // same pages after a reorder.
  protected readonly selected = signal<PageRef[]>(
    this.document
      .pages()
      .slice(1, 3)
      .map((page) => page.ref),
  );

  protected isSelected(page: PageRef): boolean {
    return this.selected().some((ref) => sameRef(ref, page));
  }

  protected toggle(page: PageRef) {
    this.selected.update((selected) =>
      this.isSelected(page) ? selected.filter((ref) => !sameRef(ref, page)) : [...selected, page],
    );
  }

  protected async deleteSelected() {
    await this.pageEdit.delete(this.selected());
    this.selected.set([]);
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, PageOrganizer],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withRender(),
      withPageEdit(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './organize.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-page-organizer *epdfDocumentGate="let document; fallback: loading" />

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

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

**`rotate.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageEdit, withPageEdit } from '@embedpdf/angular/page-edit';
import { localEngine } from '@embedpdf/engine';

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.
@Component({
  selector: 'demo-rotate-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (page(); as page) {
      <div class="toolbar">
        <output class="readout">
          Page {{ stage().currentPageIndex() + 1 }} · {{ page.rotation }}°
        </output>
        <span class="spacer"></span>
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit()"
          (click)="pageEdit.rotateBy([page.ref], -90)"
        >
          ⟲ Rotate left
        </button>
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit()"
          (click)="pageEdit.rotateBy([page.ref], 90)"
        >
          ⟳ Rotate right
        </button>
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit()"
          (click)="everyPageUpright()"
        >
          Every page upright
        </button>
      </div>
    }
  `,
})
export class RotateToolbar {
  readonly stage = input.required<EpdfStage>();
  protected readonly pageEdit = inject(EpdfPageEdit);
  private readonly document = inject(EpdfDocument);
  protected readonly page = computed(
    () => this.document.pages()[this.stage().currentPageIndex()] ?? null,
  );

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

  protected everyPageUpright() {
    void this.pageEdit.setRotation(
      this.document.pages().map((each) => each.ref),
      0,
    );
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, RotateToolbar],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withPageEdit(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './rotate.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-rotate-toolbar [stage]="stage" />
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

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.

**`reorder.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageInfo, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageEdit, withPageEdit, type PagePlacement } from '@embedpdf/angular/page-edit';
import { localEngine } from '@embedpdf/engine';

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.
@Component({
  selector: 'demo-page-order',
  imports: [EpdfPageView, EpdfRenderLayer],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @let canEdit = pageEdit.canEdit();
    <div class="toolbar">
      <output class="readout">
        @if (page(); as page) {
          Page {{ page.index + 1 }} selected
        } @else {
          Pick a page
        }
      </output>
      <span class="spacer"></span>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || !before()"
        (click)="moveTo('start')"
      >
        ⇤ To the front
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || !before()"
        (click)="moveBefore(before())"
      >
        ← Earlier
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || !after()"
        (click)="moveAfter(after())"
      >
        Later →
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!canEdit || !after()"
        (click)="moveTo('end')"
      >
        To the back ⇥
      </button>
    </div>
    <ol class="strip">
      @for (each of document.pages(); track each.ref.objectNumber) {
        <li>
          <button
            type="button"
            class="card"
            [attr.aria-pressed]="each.ref.objectNumber === selected()?.objectNumber"
            (click)="selected.set(each.ref)"
          >
            <epdf-page-view [page]="each.ref" [width]="110" class="thumbnail">
              <epdf-render-layer />
            </epdf-page-view>
            <span class="label">{{ each.index + 1 }}</span>
          </button>
        </li>
      }
    </ol>
  `,
})
export class PageOrder {
  protected readonly pageEdit = inject(EpdfPageEdit);
  protected readonly document = inject(EpdfDocument);
  // The last page starts selected. Its ref names it wherever it moves.
  protected readonly selected = signal<PageRef | null>(this.document.pages().at(-1)?.ref ?? null);

  protected readonly page = computed(
    () =>
      this.document
        .pages()
        .find((each) => each.ref.objectNumber === this.selected()?.objectNumber) ?? null,
  );
  protected readonly before = computed(() => {
    const page = this.page();
    return (page && this.document.pages()[page.index - 1]) ?? null;
  });
  protected readonly after = computed(() => {
    const page = this.page();
    return (page && this.document.pages()[page.index + 1]) ?? null;
  });

  protected moveTo(placement: PagePlacement) {
    const page = this.page();
    if (page) void this.pageEdit.reorder([page.ref], placement);
  }

  protected moveBefore(other: PageInfo | null) {
    if (other) this.moveTo({ before: other.ref });
  }

  protected moveAfter(other: PageInfo | null) {
    if (other) this.moveTo({ after: other.ref });
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, PageOrder],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withRender(),
      withPageEdit(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './reorder.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-page-order *epdfDocumentGate="let document; fallback: loading" />

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

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

**`insert.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageEdit, withPageEdit } from '@embedpdf/angular/page-edit';
import { localEngine } from '@embedpdf/engine';

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.
@Component({
  selector: 'demo-insert-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (page(); as page) {
      <div class="toolbar">
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit()"
          (click)="insertBlankAfter(page.ref)"
        >
          + Blank page after
        </button>
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit() || !pageEdit.canExtract()"
          (click)="duplicate(page.ref)"
        >
          Duplicate page
        </button>
        <button
          type="button"
          class="button"
          [disabled]="!pageEdit.canEdit()"
          (click)="insertFromOtherPdf(page.ref)"
        >
          + Pages 1 and 3 of another PDF
        </button>
        <span class="spacer"></span>
        <output class="readout">{{ readout() }}</output>
      </div>
    }
  `,
})
export class InsertToolbar {
  readonly stage = input.required<EpdfStage>();
  protected readonly pageEdit = inject(EpdfPageEdit);
  private readonly document = inject(EpdfDocument);
  protected readonly page = computed(
    () => this.document.pages()[this.stage().currentPageIndex()] ?? null,
  );
  private readonly added = signal<readonly PageRef[]>([]);
  protected readonly readout = computed(() => {
    const pages = this.document.pages();
    const positions = this.added()
      .map((ref) => pages.findIndex((each) => each.ref.objectNumber === ref.objectNumber) + 1)
      .filter((position) => position > 0);
    return positions.length > 0
      ? `New: page ${positions.join(' and ')} of ${pages.length}`
      : `${pages.length} pages`;
  });

  constructor() {
    // A blank page after the cover, once, on load.
    const insertOnLoad = effect(() => {
      const cover = this.document.pages()[0];
      if (!cover) return;
      insertOnLoad.destroy();
      untracked(() => this.insertBlankAfter(cover.ref));
    });
  }

  protected async insertBlankAfter(page: PageRef) {
    this.show(await this.pageEdit.insertBlank({ placement: { after: page } }));
  }

  protected async duplicate(page: PageRef) {
    this.show(await this.pageEdit.duplicate([page]));
  }

  protected async insertFromOtherPdf(page: PageRef) {
    this.show(
      await this.pageEdit.insertFromBytes(await otherPdf(), {
        pageIndexes: [0, 2],
        placement: { after: page },
      }),
    );
  }

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

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, InsertToolbar],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withPageEdit(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './insert.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-insert-toolbar [stage]="stage" />
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

## 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');
```

**`extract.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import {
  EpdfDocument,
  EpdfDocumentGate,
  provideEmbedPdf,
  saveFile,
} from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageView } from '@embedpdf/angular/page-view';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageEdit, withPageEdit } from '@embedpdf/angular/page-edit';
import { localEngine } from '@embedpdf/engine';

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.
@Component({
  selector: 'demo-extract-pages',
  imports: [EpdfPageView, EpdfRenderLayer],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button
        type="button"
        class="button primary"
        [disabled]="chosen().length === 0 || !pageEdit.canExtract()"
        (click)="download()"
      >
        Download {{ chosen().length === 1 ? '1 page' : chosen().length + ' pages' }} as a PDF
      </button>
      <output class="readout">{{ saved() }}</output>
    </div>
    <ol class="grid">
      @for (page of document.pages(); track page.ref.objectNumber) {
        <li>
          <label class="card" [attr.data-ticked]="isTicked(page.ref)">
            <epdf-page-view [page]="page.ref" [width]="110" class="thumbnail">
              <epdf-render-layer />
            </epdf-page-view>
            <span class="label">
              <input type="checkbox" [checked]="isTicked(page.ref)" (change)="toggle(page.ref)" />
              Page {{ page.index + 1 }}
            </span>
          </label>
        </li>
      }
    </ol>
  `,
})
export class ExtractPages {
  protected readonly pageEdit = inject(EpdfPageEdit);
  protected readonly document = inject(EpdfDocument);
  // The middle two pages start ticked.
  private readonly ticked = signal<PageRef[]>(
    this.document
      .pages()
      .slice(1, 3)
      .map((page) => page.ref),
  );
  protected readonly saved = signal<string | null>(null);

  // In document order, whatever order they were ticked in.
  protected readonly chosen = computed(() =>
    this.document
      .pages()
      .filter((page) => this.isTicked(page.ref))
      .map((page) => page.ref),
  );

  protected isTicked(ref: PageRef): boolean {
    return this.ticked().some((each) => each.objectNumber === ref.objectNumber);
  }

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

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

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, ExtractPages],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withRender(),
      withPageEdit(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './extract.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-extract-pages *epdfDocumentGate="let document; fallback: loading" />

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`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);
}
```

## 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 `inject(EpdfPageEdit)` 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.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { EpdfDocuments } from '@embedpdf/angular/runtime';

@Component({
  selector: 'app-page-count',
  template: `<span>{{ pageCount() }} pages</span>`,
})
export class PageCount {
  private readonly documents = inject(EpdfDocuments);

  protected readonly pageCount = toSignal(
    this.documents.pagesChanged$.pipe(map(({ pages }) => pages.length)),
    { initialValue: 0 },
  );
}
```

The page edit plugin has no events of its own. `pagesChanged$` 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 `document.pages()` (`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).
