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):
Loading live preview…
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 {}await pageEdit.rotateBy([page.ref], 90);
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.delete([page.ref]);After an edit, everything that shows pages updates by itself: the Stage, thumbnails, page numbers. On a shared document, the others see the change too.
Calls that act on pages take a list of them, so one call works for a single page and for a multi-selection.
Rotate pages#
await pageEdit.rotateBy([page.ref], 90); // a quarter turn clockwise from where it is
await pageEdit.rotateBy([page.ref], -90); // and back
await pageEdit.setRotation([pageA.ref, pageB.ref], 180); // the same rotation for severalLoading live preview…
import {
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 {}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 {
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 {}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 {
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 {}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 {
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 {}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 |
setRotation(pages, rotation) | Gives every page the same rotation: 0, 90, 180 or 270 |
reorder(pages, placement) | Moves pages to a new place. Several pages keep their order and land together. Reorder pages |
delete(pages) | Deletes pages. Deleting every page is refused. Delete pages |
insertBlank({ placement?, size?, count? }) | Inserts blank pages, one unless you pass a count. Resolves { pages }, the new pages. Insert pages |
insertFromBytes(bytes, { pageIndexes?, placement? }) | Inserts the pages of another PDF, or only the ones at pageIndexes. Resolves { pages }. |
insertFromDocument(documentId, pages, { placement? }) | Inserts copies of pages from another document open in the viewer. Resolves { pages }. |
duplicate(pages, { placement? }) | Copies pages, right after the last of them unless you pass a placement. Resolves { pages }. |
extract(pages) | Copies pages into a new PDF and resolves its bytes. This document doesn’t change. Extract pages |
canEdit(), canExtract() | Whether rotating, reordering, deleting and inserting are allowed, and whether copying pages out is. Permissions |
placement is one of the values in Where a page goes; without one, new pages go
at the end. Every method that returns a promise also takes a signal in its options, to cancel it.
Events#
Page changes are document events, so every part of your app hears them, whichever plugin made the change:
import { 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.
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.
Your feedback goes directly to the documentation team.