@embedpdf/plugin-page-edit 3.0.0-next.15 → 3.0.0-next.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,8 @@
1
- import { OperationOptions, PageRef, PdfRotation, PdfRotation as PdfRotation$1, PdfSize, PdfSize as PdfSize$1 } from "@embedpdf/core";
1
+ import { OperationOptions, PagePosition, PagePosition as PagePosition$1, PageRef, PdfRotation, PdfRotation as PdfRotation$1, PdfSize, PdfSize as PdfSize$1 } from "@embedpdf/core";
2
2
  //#region src/token.d.ts
3
3
  declare const PageEditToken: import("@embedpdf/core").CapabilityToken<PageEditCapability>;
4
4
  //#endregion
5
5
  //#region src/contract.d.ts
6
- /**
7
- * Where pages land: right after or before a page, or at the start or the end.
8
- * A page named by its index is the page at that index when the verb is
9
- * called, the page the user clicked; from then on it is that page, so the
10
- * placement stays right when pages are reordered before the edit runs.
11
- */
12
- type PagePlacement = {
13
- readonly after: PageRef | number;
14
- } | {
15
- readonly before: PageRef | number;
16
- } | 'start' | 'end';
17
6
  /** What an insert resolves: the new pages, in the order they were inserted. */
18
7
  interface PageEditInsertResult {
19
8
  readonly pages: readonly PageRef[];
@@ -21,8 +10,8 @@ interface PageEditInsertResult {
21
10
  /** Options of `insertBlank()`. */
22
11
  interface PageInsertBlankOptions extends OperationOptions {
23
12
  /** Where the new pages go. Default `'end'`. */
24
- readonly placement?: PagePlacement;
25
- /** The page size, in points. Default: the size of the page the new pages are placed next to. */
13
+ readonly position?: PagePosition;
14
+ /** The page size, in points. Default: the size of the page the new pages go next to. */
26
15
  readonly size?: PdfSize;
27
16
  /** How many blank pages. Default 1. */
28
17
  readonly count?: number;
@@ -32,18 +21,26 @@ interface PageInsertFromBytesOptions extends OperationOptions {
32
21
  /** Only the source PDF's pages at these indexes, in this order. Default: every page. */
33
22
  readonly pageIndexes?: readonly number[];
34
23
  /** Where the new pages go. Default `'end'`. */
35
- readonly placement?: PagePlacement;
24
+ readonly position?: PagePosition;
36
25
  }
37
26
  /** Options of the verbs that copy pages in: `insertFromDocument()` and `duplicate()`. */
38
- interface PagePlacementOptions extends OperationOptions {
39
- readonly placement?: PagePlacement;
27
+ interface PagePositionOptions extends OperationOptions {
28
+ readonly position?: PagePosition;
40
29
  }
41
30
  /**
42
- * Every edit runs in the order it was called, after the edits before it.
31
+ * Every edit is one change: one step of the history, so undo brings the
32
+ * pages back as they were, a deleted page with its annotations under its old
33
+ * ref. It shows once the engine answers. Edits apply in the order they were
34
+ * called, except that the verbs that copy pages first (`duplicate()`,
35
+ * `insertFromDocument()`, `insertFromBytes()` with `pageIndexes`) take their
36
+ * place once the copy is made.
37
+ *
43
38
  * Every verb rejects `permission-denied` (with `error.permission`) when the
44
- * session may not do it, `not-found` for a page the document doesn't have,
45
- * `invalid-input` for no pages, and `operation-cancelled` when its `signal`
46
- * fires; a refusal changes nothing.
39
+ * session may not do it, `not-found` for a page the document doesn't have and
40
+ * `invalid-input` for no pages; a refusal changes nothing. An edit the engine
41
+ * refuses rejects with the engine's reason. When the `signal` fires the verb
42
+ * rejects `operation-cancelled`: before its change is made nothing changes,
43
+ * after it the change still applies.
47
44
  */
48
45
  interface PageEditCapability {
49
46
  /** Whether rotating, reordering, deleting and inserting pages is allowed (`doc.pages.assemble`). */
@@ -53,12 +50,20 @@ interface PageEditCapability {
53
50
  * `duplicate()` and `insertFromDocument()` need it.
54
51
  */
55
52
  canExtract(): boolean;
56
- /** Turn each page by a quarter or half turn from its own rotation. */
53
+ /** The selected pages, in the order they were selected; pages the document no longer has drop out. */
54
+ getSelected(): readonly PageRef[];
55
+ /** Make these pages the selection; `[]` clears it. Pages the document doesn't have are left out. */
56
+ select(pages: readonly (PageRef | number)[]): void;
57
+ /**
58
+ * Turn each page by a quarter or half turn from its rotation, the one it
59
+ * has once the edits on their way apply: two quick quarter turns are a
60
+ * half turn.
61
+ */
57
62
  rotateBy(pages: readonly (PageRef | number)[], delta: 90 | -90 | 180, options?: OperationOptions): Promise<void>;
58
63
  /** Give every page the same rotation. */
59
64
  setRotation(pages: readonly (PageRef | number)[], rotation: PdfRotation, options?: OperationOptions): Promise<void>;
60
- /** Move pages to the placement; several pages keep their order and land together. */
61
- reorder(pages: readonly (PageRef | number)[], placement: PagePlacement, options?: OperationOptions): Promise<void>;
65
+ /** Move pages to `position`; several pages keep their order and land together. */
66
+ reorder(pages: readonly (PageRef | number)[], position: PagePosition, options?: OperationOptions): Promise<void>;
62
67
  /** Delete pages. Deleting every page is refused (`invalid-input`): a document keeps one. */
63
68
  delete(pages: readonly (PageRef | number)[], options?: OperationOptions): Promise<void>;
64
69
  /** Insert blank pages, one unless `count` says more. */
@@ -69,12 +74,18 @@ interface PageEditCapability {
69
74
  * Insert copies of pages from another document open in the viewer. Needs
70
75
  * `doc.download` on that document and `doc.pages.assemble` on this one.
71
76
  */
72
- insertFromDocument(documentId: string, pages: readonly (PageRef | number)[], options?: PagePlacementOptions): Promise<PageEditInsertResult>;
73
- /** Copy pages, right after the last of them unless a placement says otherwise. */
74
- duplicate(pages: readonly (PageRef | number)[], options?: PagePlacementOptions): Promise<PageEditInsertResult>;
75
- /** Copy pages into a new PDF and resolve its bytes. This document doesn't change. */
77
+ insertFromDocument(documentId: string, pages: readonly (PageRef | number)[], options?: PagePositionOptions): Promise<PageEditInsertResult>;
78
+ /**
79
+ * Copy pages as they show, with the edits on their way, right after the
80
+ * last of them unless `position` says otherwise.
81
+ */
82
+ duplicate(pages: readonly (PageRef | number)[], options?: PagePositionOptions): Promise<PageEditInsertResult>;
83
+ /**
84
+ * Copy pages into a new PDF, as they show with the edits on their way, and
85
+ * resolve its bytes. This document doesn't change.
86
+ */
76
87
  extract(pages: readonly (PageRef | number)[], options?: OperationOptions): Promise<Uint8Array>;
77
88
  }
78
89
  //#endregion
79
- export { PagePlacement as a, PdfSize$1 as c, PageInsertFromBytesOptions as i, PageEditToken as l, PageEditInsertResult as n, PagePlacementOptions as o, PageInsertBlankOptions as r, PdfRotation$1 as s, PageEditCapability as t };
80
- //# sourceMappingURL=contract-dl4LfrRX.d.ts.map
90
+ export { PagePosition$1 as a, PdfSize$1 as c, PageInsertFromBytesOptions as i, PageEditToken as l, PageEditInsertResult as n, PagePositionOptions as o, PageInsertBlankOptions as r, PdfRotation$1 as s, PageEditCapability as t };
91
+ //# sourceMappingURL=contract-IN8rk5LG.d.cts.map
@@ -1,19 +1,8 @@
1
- import { OperationOptions, PageRef, PdfRotation, PdfRotation as PdfRotation$1, PdfSize, PdfSize as PdfSize$1 } from "@embedpdf/core";
1
+ import { OperationOptions, PagePosition, PagePosition as PagePosition$1, PageRef, PdfRotation, PdfRotation as PdfRotation$1, PdfSize, PdfSize as PdfSize$1 } from "@embedpdf/core";
2
2
  //#region src/token.d.ts
3
3
  declare const PageEditToken: import("@embedpdf/core").CapabilityToken<PageEditCapability>;
4
4
  //#endregion
5
5
  //#region src/contract.d.ts
6
- /**
7
- * Where pages land: right after or before a page, or at the start or the end.
8
- * A page named by its index is the page at that index when the verb is
9
- * called, the page the user clicked; from then on it is that page, so the
10
- * placement stays right when pages are reordered before the edit runs.
11
- */
12
- type PagePlacement = {
13
- readonly after: PageRef | number;
14
- } | {
15
- readonly before: PageRef | number;
16
- } | 'start' | 'end';
17
6
  /** What an insert resolves: the new pages, in the order they were inserted. */
18
7
  interface PageEditInsertResult {
19
8
  readonly pages: readonly PageRef[];
@@ -21,8 +10,8 @@ interface PageEditInsertResult {
21
10
  /** Options of `insertBlank()`. */
22
11
  interface PageInsertBlankOptions extends OperationOptions {
23
12
  /** Where the new pages go. Default `'end'`. */
24
- readonly placement?: PagePlacement;
25
- /** The page size, in points. Default: the size of the page the new pages are placed next to. */
13
+ readonly position?: PagePosition;
14
+ /** The page size, in points. Default: the size of the page the new pages go next to. */
26
15
  readonly size?: PdfSize;
27
16
  /** How many blank pages. Default 1. */
28
17
  readonly count?: number;
@@ -32,18 +21,26 @@ interface PageInsertFromBytesOptions extends OperationOptions {
32
21
  /** Only the source PDF's pages at these indexes, in this order. Default: every page. */
33
22
  readonly pageIndexes?: readonly number[];
34
23
  /** Where the new pages go. Default `'end'`. */
35
- readonly placement?: PagePlacement;
24
+ readonly position?: PagePosition;
36
25
  }
37
26
  /** Options of the verbs that copy pages in: `insertFromDocument()` and `duplicate()`. */
38
- interface PagePlacementOptions extends OperationOptions {
39
- readonly placement?: PagePlacement;
27
+ interface PagePositionOptions extends OperationOptions {
28
+ readonly position?: PagePosition;
40
29
  }
41
30
  /**
42
- * Every edit runs in the order it was called, after the edits before it.
31
+ * Every edit is one change: one step of the history, so undo brings the
32
+ * pages back as they were, a deleted page with its annotations under its old
33
+ * ref. It shows once the engine answers. Edits apply in the order they were
34
+ * called, except that the verbs that copy pages first (`duplicate()`,
35
+ * `insertFromDocument()`, `insertFromBytes()` with `pageIndexes`) take their
36
+ * place once the copy is made.
37
+ *
43
38
  * Every verb rejects `permission-denied` (with `error.permission`) when the
44
- * session may not do it, `not-found` for a page the document doesn't have,
45
- * `invalid-input` for no pages, and `operation-cancelled` when its `signal`
46
- * fires; a refusal changes nothing.
39
+ * session may not do it, `not-found` for a page the document doesn't have and
40
+ * `invalid-input` for no pages; a refusal changes nothing. An edit the engine
41
+ * refuses rejects with the engine's reason. When the `signal` fires the verb
42
+ * rejects `operation-cancelled`: before its change is made nothing changes,
43
+ * after it the change still applies.
47
44
  */
48
45
  interface PageEditCapability {
49
46
  /** Whether rotating, reordering, deleting and inserting pages is allowed (`doc.pages.assemble`). */
@@ -53,12 +50,20 @@ interface PageEditCapability {
53
50
  * `duplicate()` and `insertFromDocument()` need it.
54
51
  */
55
52
  canExtract(): boolean;
56
- /** Turn each page by a quarter or half turn from its own rotation. */
53
+ /** The selected pages, in the order they were selected; pages the document no longer has drop out. */
54
+ getSelected(): readonly PageRef[];
55
+ /** Make these pages the selection; `[]` clears it. Pages the document doesn't have are left out. */
56
+ select(pages: readonly (PageRef | number)[]): void;
57
+ /**
58
+ * Turn each page by a quarter or half turn from its rotation, the one it
59
+ * has once the edits on their way apply: two quick quarter turns are a
60
+ * half turn.
61
+ */
57
62
  rotateBy(pages: readonly (PageRef | number)[], delta: 90 | -90 | 180, options?: OperationOptions): Promise<void>;
58
63
  /** Give every page the same rotation. */
59
64
  setRotation(pages: readonly (PageRef | number)[], rotation: PdfRotation, options?: OperationOptions): Promise<void>;
60
- /** Move pages to the placement; several pages keep their order and land together. */
61
- reorder(pages: readonly (PageRef | number)[], placement: PagePlacement, options?: OperationOptions): Promise<void>;
65
+ /** Move pages to `position`; several pages keep their order and land together. */
66
+ reorder(pages: readonly (PageRef | number)[], position: PagePosition, options?: OperationOptions): Promise<void>;
62
67
  /** Delete pages. Deleting every page is refused (`invalid-input`): a document keeps one. */
63
68
  delete(pages: readonly (PageRef | number)[], options?: OperationOptions): Promise<void>;
64
69
  /** Insert blank pages, one unless `count` says more. */
@@ -69,12 +74,18 @@ interface PageEditCapability {
69
74
  * Insert copies of pages from another document open in the viewer. Needs
70
75
  * `doc.download` on that document and `doc.pages.assemble` on this one.
71
76
  */
72
- insertFromDocument(documentId: string, pages: readonly (PageRef | number)[], options?: PagePlacementOptions): Promise<PageEditInsertResult>;
73
- /** Copy pages, right after the last of them unless a placement says otherwise. */
74
- duplicate(pages: readonly (PageRef | number)[], options?: PagePlacementOptions): Promise<PageEditInsertResult>;
75
- /** Copy pages into a new PDF and resolve its bytes. This document doesn't change. */
77
+ insertFromDocument(documentId: string, pages: readonly (PageRef | number)[], options?: PagePositionOptions): Promise<PageEditInsertResult>;
78
+ /**
79
+ * Copy pages as they show, with the edits on their way, right after the
80
+ * last of them unless `position` says otherwise.
81
+ */
82
+ duplicate(pages: readonly (PageRef | number)[], options?: PagePositionOptions): Promise<PageEditInsertResult>;
83
+ /**
84
+ * Copy pages into a new PDF, as they show with the edits on their way, and
85
+ * resolve its bytes. This document doesn't change.
86
+ */
76
87
  extract(pages: readonly (PageRef | number)[], options?: OperationOptions): Promise<Uint8Array>;
77
88
  }
78
89
  //#endregion
79
- export { PagePlacement as a, PdfSize$1 as c, PageInsertFromBytesOptions as i, PageEditToken as l, PageEditInsertResult as n, PagePlacementOptions as o, PageInsertBlankOptions as r, PdfRotation$1 as s, PageEditCapability as t };
80
- //# sourceMappingURL=contract-dl4LfrRX.d.cts.map
90
+ export { PagePosition$1 as a, PdfSize$1 as c, PageInsertFromBytesOptions as i, PageEditToken as l, PageEditInsertResult as n, PagePositionOptions as o, PageInsertBlankOptions as r, PdfRotation$1 as s, PageEditCapability as t };
91
+ //# sourceMappingURL=contract-IN8rk5LG.d.ts.map
@@ -1,2 +1,2 @@
1
- import { a as PagePlacement, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePlacementOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-dl4LfrRX.cjs";
2
- export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, PagePlacement, PagePlacementOptions, type PdfRotation, type PdfSize };
1
+ import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.cjs";
2
+ export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize };
@@ -1,2 +1,2 @@
1
- import { a as PagePlacement, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePlacementOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-dl4LfrRX.js";
2
- export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, PagePlacement, PagePlacementOptions, type PdfRotation, type PdfSize };
1
+ import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.js";
2
+ export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize };
package/dist/index.cjs CHANGED
@@ -1,15 +1,28 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_contract = require("./contract-B4DeVaKv.cjs");
3
3
  let _embedpdf_core = require("@embedpdf/core");
4
+ //#region src/model.ts
5
+ const initialPageEditState = () => ({ selected: [] });
6
+ /** The selection becomes `refs`, each page once. */
7
+ function selected(state, refs) {
8
+ const numbers = refs.map((ref) => ref.objectNumber);
9
+ const unique = refs.filter((_ref, i) => numbers.indexOf(numbers[i]) === i);
10
+ return unique.length === state.selected.length && unique.every((ref, i) => ref.objectNumber === state.selected[i].objectNumber) ? state : {
11
+ ...state,
12
+ selected: unique
13
+ };
14
+ }
15
+ //#endregion
4
16
  //#region src/controller.ts
5
17
  /**
6
- * The page-edit controller. Stateless: it turns the engine handle's page
7
- * service into page edits that take refs or indexes. Turning a page relative
8
- * to its own rotation, and fixing a placement's page, live here once instead
9
- * of in every app's click handler.
18
+ * The page-edit controller: page edits that take refs or indexes, and the
19
+ * page selection. Turning a page relative to its own rotation, and fixing a
20
+ * position's page, live here once instead of in every app's click handler.
10
21
  *
11
- * A verb refuses before anything starts (permission, pages, placement), then
12
- * queues its edit: edits run one at a time, in the order they were called.
22
+ * A verb refuses before anything starts (permission, pages, position), then
23
+ * stages its edit as one change on the document's queue (`ctx.changes`): one
24
+ * step of the history, sent after everything staged before it. The page list
25
+ * shows it once the engine answers; nothing predicts pages yet.
13
26
  */
14
27
  /** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */
15
28
  const ASSEMBLE = "doc.pages.assemble";
@@ -20,9 +33,9 @@ const LETTER_SIZE = {
20
33
  width: 612,
21
34
  height: 792
22
35
  };
36
+ const NO_PAGES = Object.freeze([]);
37
+ const NO_LAYOUTS = Object.freeze([]);
23
38
  function createPageEditController(ctx) {
24
- const enqueue = ctx.serialQueue("edits");
25
- ctx.onSettle(() => enqueue.idle());
26
39
  /** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */
27
40
  const mapErrors = async (work) => {
28
41
  try {
@@ -31,7 +44,7 @@ function createPageEditController(ctx) {
31
44
  throw (0, _embedpdf_core.toPluginError)("page-edit", error);
32
45
  }
33
46
  };
34
- const registry = () => ctx.document()?.pages ?? [];
47
+ const registry = () => ctx.document()?.pages ?? NO_LAYOUTS;
35
48
  const noPages = () => new _embedpdf_core.PluginError("invalid-input", "page-edit", "no pages given");
36
49
  /**
37
50
  * A verb's pages as refs, checked when it is called: an index names the
@@ -41,17 +54,14 @@ function createPageEditController(ctx) {
41
54
  if (pages.length === 0) throw noPages();
42
55
  return pages.map((page) => ctx.pageOf(page).ref);
43
56
  };
44
- /**
45
- * A placement as the engine's position, its page fixed when the verb is
46
- * called, like the verb's own pages. Default `'end'`.
47
- */
48
- const positionOf = (placement) => {
49
- if (!placement || placement === "start" || placement === "end") return placement ?? "end";
50
- if ("after" in placement) return { after: ctx.pageOf(placement.after).ref };
51
- return { before: ctx.pageOf(placement.before).ref };
57
+ /** A position, its page checked when the verb is called, like the verb's own pages. Default `'end'`. */
58
+ const positionOf = (position) => {
59
+ if (!position || position === "start" || position === "end") return position ?? "end";
60
+ if ("after" in position) return { after: ctx.pageOf(position.after).ref };
61
+ return { before: ctx.pageOf(position.before).ref };
52
62
  };
53
63
  /**
54
- * Default blank-page size: the page the placement names, else the page the
64
+ * Default blank-page size: the page the position names, else the page the
55
65
  * new ones go next to (the first or the last), else Letter.
56
66
  */
57
67
  const sizeAt = (position) => {
@@ -60,83 +70,188 @@ function createPageEditController(ctx) {
60
70
  if (pages.length === 0) return LETTER_SIZE;
61
71
  return (position === "start" ? pages[0] : pages[pages.length - 1]).size;
62
72
  };
63
- /** Insert a PDF's pages; resolves the new pages. */
64
- const insertBytes = async (bytes, position, signal) => {
65
- return { pages: (await ctx.cancellable(signal, ctx.doc.pages.insert(bytes, position))).insertedPages };
73
+ const getSelected = (0, _embedpdf_core.memo)(() => [registry(), ctx.state.get().selected], (pages, refs) => {
74
+ if (refs.length === 0) return NO_PAGES;
75
+ const present = new Set(pages.map((page) => page.ref.objectNumber));
76
+ return refs.filter((ref) => present.has(ref.objectNumber));
77
+ });
78
+ const select = (pages) => {
79
+ const refs = pages.flatMap((page) => {
80
+ const found = ctx.getPage(page);
81
+ return found ? [found.ref] : [];
82
+ });
83
+ ctx.state.update(selected, refs);
84
+ };
85
+ /** Refuse before anything starts: without the right, or once the caller cancelled. */
86
+ const refuse = (permission, operation, options) => {
87
+ ctx.assertAllowed(permission, operation);
88
+ if (options?.signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "page-edit", `${operation} was cancelled`);
89
+ };
90
+ /**
91
+ * Stage one user action: one change, one step of the history. Its label
92
+ * counts the pages it acts on, when the verb knows how many.
93
+ */
94
+ const stageChange = (verb, count, ops, undo) => {
95
+ const label = count === null ? { key: `pages.${verb}` } : {
96
+ key: `pages.${verb}`,
97
+ count
98
+ };
99
+ return ctx.changes.stage({
100
+ label,
101
+ ops,
102
+ ...undo ? { undo } : {}
103
+ });
104
+ };
105
+ /** The engine's answer to a staged change: its first item. */
106
+ const answer = async (change, options) => {
107
+ try {
108
+ return (await ctx.cancellable(options?.signal, change.result)).items[0];
109
+ } catch (error) {
110
+ throw (0, _embedpdf_core.toPluginError)("page-edit", error);
111
+ }
112
+ };
113
+ /** What an insert's answer names: the new pages. */
114
+ const inserted = async (change, options) => {
115
+ const item = await answer(change, options);
116
+ return { pages: "insertedPages" in item ? item.insertedPages : NO_PAGES };
117
+ };
118
+ /**
119
+ * A page's rotation once this session's changes on their way apply: the
120
+ * last rotation one of them gives it, else the page's own. What a turn
121
+ * starts from, so two quick quarter turns are a half turn.
122
+ */
123
+ const rotationOf = (ref) => {
124
+ let rotation = ctx.pageOf(ref).rotation;
125
+ for (const change of ctx.changes.pending()) for (const op of change.shows) {
126
+ if (op.type !== "pages.rotate") continue;
127
+ if (op.pages.some((page) => page.objectNumber === ref.objectNumber)) rotation = op.rotation;
128
+ }
129
+ return rotation;
130
+ };
131
+ /**
132
+ * Rotate pages as one change: one op per rotation they end up with. What
133
+ * undoing it looks like is each page turned back, so a turn right after an
134
+ * undo starts from where the undo leaves the page.
135
+ */
136
+ const rotate = (refs, rotationAfter) => {
137
+ const after = /* @__PURE__ */ new Map();
138
+ const before = /* @__PURE__ */ new Map();
139
+ for (const ref of refs) {
140
+ const add = (groups, rotation) => groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
141
+ add(before, rotationOf(ref));
142
+ add(after, rotationAfter(ref));
143
+ }
144
+ const opsOf = (groups) => [...groups].map(([rotation, pages]) => ({
145
+ type: "pages.rotate",
146
+ pages,
147
+ rotation
148
+ }));
149
+ return stageChange("rotate", refs.length, opsOf(after), opsOf(before));
150
+ };
151
+ /**
152
+ * The pages as they show: every change on its way sent and answered first,
153
+ * so a copy holds the edits made before it.
154
+ */
155
+ const settled = async (options) => {
156
+ ctx.changes.sendHolds();
157
+ await ctx.cancellable(options?.signal, ctx.changes.whenSettled());
158
+ };
159
+ /** Copy pages of this document out, as they show. */
160
+ const extractShown = async (refs, options) => {
161
+ await settled(options);
162
+ return ctx.cancellable(options?.signal, ctx.doc.pages.extract([...refs]));
163
+ };
164
+ /** Stage the insert of a PDF's pages; resolves the new pages. */
165
+ const insertBytes = (verb, count, bytes, position, options) => {
166
+ const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
167
+ const change = stageChange(verb, count, [{
168
+ type: "pages.insert",
169
+ source,
170
+ position
171
+ }]);
172
+ return inserted(change, options);
173
+ };
174
+ /** Some pages of a PDF: open the bytes beside the document and extract them. */
175
+ const pagesOfBytes = async (bytes, pageIndexes, signal) => {
176
+ const source = await mapErrors(() => ctx.engine.open({
177
+ kind: "bytes",
178
+ id: `page-edit-source-${Date.now().toString(36)}`,
179
+ bytes: new Uint8Array(bytes)
180
+ }, { scope: ["*"] }));
181
+ try {
182
+ const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
183
+ const refs = pageIndexes.map((index) => {
184
+ const page = layout.pages[index];
185
+ if (!page) throw new _embedpdf_core.PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
186
+ return page.ref;
187
+ });
188
+ return await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
189
+ } finally {
190
+ await source.close();
191
+ }
66
192
  };
67
193
  return { api: {
68
194
  canEdit: () => ctx.allows(ASSEMBLE),
69
195
  canExtract: () => ctx.allows(DOWNLOAD),
196
+ getSelected,
197
+ select,
70
198
  rotateBy: async (pages, delta, options) => {
71
- ctx.assertAllowed(ASSEMBLE, "pageEdit.rotateBy");
199
+ refuse(ASSEMBLE, "pageEdit.rotateBy", options);
72
200
  const refs = refsOf(pages);
73
- const signal = options?.signal;
74
- await enqueue(async () => {
75
- const groups = /* @__PURE__ */ new Map();
76
- for (const ref of refs) {
77
- const rotation = ((ctx.pageOf(ref).rotation + delta) % 360 + 360) % 360;
78
- groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
79
- }
80
- for (const [rotation, group] of groups) await ctx.cancellable(signal, ctx.doc.pages.rotate(group, rotation));
81
- }, options);
201
+ const change = rotate(refs, (ref) => ((rotationOf(ref) + delta) % 360 + 360) % 360);
202
+ await answer(change, options);
82
203
  },
83
204
  setRotation: async (pages, rotation, options) => {
84
- ctx.assertAllowed(ASSEMBLE, "pageEdit.setRotation");
85
- const refs = refsOf(pages);
86
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.rotate(refs, rotation)), options);
205
+ refuse(ASSEMBLE, "pageEdit.setRotation", options);
206
+ const change = rotate(refsOf(pages), () => rotation);
207
+ await answer(change, options);
87
208
  },
88
- reorder: async (pages, placement, options) => {
89
- ctx.assertAllowed(ASSEMBLE, "pageEdit.reorder");
209
+ reorder: async (pages, position, options) => {
210
+ refuse(ASSEMBLE, "pageEdit.reorder", options);
90
211
  const refs = refsOf(pages);
91
- const position = positionOf(placement);
92
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.reorder(refs, position)), options);
212
+ const change = stageChange("reorder", refs.length, [{
213
+ type: "pages.reorder",
214
+ pages: refs,
215
+ position: positionOf(position)
216
+ }]);
217
+ await answer(change, options);
93
218
  },
94
219
  delete: async (pages, options) => {
95
- ctx.assertAllowed(ASSEMBLE, "pageEdit.delete");
220
+ refuse(ASSEMBLE, "pageEdit.delete", options);
96
221
  const refs = refsOf(pages);
97
222
  const deleting = new Set(refs.map((ref) => ref.objectNumber));
98
223
  if (registry().every((page) => deleting.has(page.ref.objectNumber))) throw new _embedpdf_core.PluginError("invalid-input", "page-edit", "a document keeps at least one page");
99
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.delete(refs)), options);
224
+ const change = stageChange("delete", refs.length, [{
225
+ type: "pages.delete",
226
+ pages: refs
227
+ }]);
228
+ await answer(change, options);
100
229
  },
101
230
  insertBlank: async (options = {}) => {
102
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertBlank");
103
- const position = positionOf(options.placement);
104
- return enqueue(async () => {
105
- const size = options.size ?? sizeAt(position);
106
- return { pages: (await ctx.cancellable(options.signal, ctx.doc.pages.insertBlank({
231
+ refuse(ASSEMBLE, "pageEdit.insertBlank", options);
232
+ const position = positionOf(options.position);
233
+ const count = options.count ?? 1;
234
+ const size = options.size ?? sizeAt(position);
235
+ const change = stageChange("insertBlank", count, [{
236
+ type: "pages.insertBlank",
237
+ spec: {
107
238
  size,
108
- count: options.count
109
- }, position))).insertedPages };
110
- }, options);
239
+ count
240
+ },
241
+ position
242
+ }]);
243
+ return inserted(change, options);
111
244
  },
112
245
  insertFromBytes: async (bytes, options = {}) => {
113
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertFromBytes");
114
- const position = positionOf(options.placement);
246
+ refuse(ASSEMBLE, "pageEdit.insertFromBytes", options);
247
+ const position = positionOf(options.position);
115
248
  const { pageIndexes, signal } = options;
116
249
  if (pageIndexes?.length === 0) throw noPages();
117
- return enqueue(async () => {
118
- if (!pageIndexes) return insertBytes(bytes, position, signal);
119
- const source = await mapErrors(() => ctx.engine.open({
120
- kind: "bytes",
121
- id: `page-edit-source-${Date.now().toString(36)}`,
122
- bytes: new Uint8Array(bytes)
123
- }, { scope: ["*"] }));
124
- try {
125
- const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
126
- const refs = pageIndexes.map((index) => {
127
- const page = layout.pages[index];
128
- if (!page) throw new _embedpdf_core.PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
129
- return page.ref;
130
- });
131
- const subset = await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
132
- return await insertBytes(subset, position, signal);
133
- } finally {
134
- await source.close();
135
- }
136
- }, options);
250
+ const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;
251
+ return insertBytes("insert", pageIndexes?.length ?? null, source, position, options);
137
252
  },
138
253
  insertFromDocument: async (documentId, pages, options = {}) => {
139
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertFromDocument");
254
+ refuse(ASSEMBLE, "pageEdit.insertFromDocument", options);
140
255
  if (pages.length === 0) throw noPages();
141
256
  const other = ctx.documentHandle(documentId);
142
257
  if (!other) throw new _embedpdf_core.PluginError("not-found", "page-edit", `document '${documentId}' is not open`);
@@ -146,44 +261,59 @@ function createPageEditController(ctx) {
146
261
  if (!found) throw new _embedpdf_core.PluginError("not-found", "page-edit", `document '${documentId}' has no such page`);
147
262
  return found.ref;
148
263
  });
149
- const position = positionOf(options.placement);
150
- return enqueue(async () => {
151
- const bytes = await mapErrors(() => ctx.cancellable(options.signal, other.pages.extract(refs)));
152
- return insertBytes(bytes, position, options.signal);
153
- }, options);
264
+ const position = positionOf(options.position);
265
+ const bytes = await mapErrors(() => ctx.cancellable(options.signal, other.pages.extract(refs)));
266
+ return insertBytes("insert", refs.length, bytes, position, options);
154
267
  },
155
268
  duplicate: async (pages, options = {}) => {
156
- ctx.assertAllowed(ASSEMBLE, "pageEdit.duplicate");
269
+ refuse(ASSEMBLE, "pageEdit.duplicate", options);
157
270
  ctx.assertAllowed(DOWNLOAD, "pageEdit.duplicate");
158
271
  const refs = refsOf(pages);
159
- const position = options.placement ? positionOf(options.placement) : { after: refs[refs.length - 1] };
160
- return enqueue(async () => {
161
- const bytes = await ctx.cancellable(options.signal, ctx.doc.pages.extract(refs));
162
- return insertBytes(bytes, position, options.signal);
163
- }, options);
272
+ const position = options.position ? positionOf(options.position) : { after: refs[refs.length - 1] };
273
+ const bytes = await extractShown(refs, options);
274
+ return insertBytes("duplicate", refs.length, bytes, position, options);
164
275
  },
165
276
  extract: async (pages, options) => {
166
- ctx.assertAllowed(DOWNLOAD, "pageEdit.extract");
167
- const refs = refsOf(pages);
168
- return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.extract(refs)), options);
277
+ refuse(DOWNLOAD, "pageEdit.extract", options);
278
+ return extractShown(refsOf(pages), options);
169
279
  }
170
280
  } };
171
281
  }
172
282
  //#endregion
173
283
  //#region src/page-edit.plugin.ts
174
284
  /**
175
- * Document-scoped and stateless: the engine handle's page service as page
176
- * edits that take refs or indexes. Turning a page relative to its own
177
- * rotation lives in the controller, so no app re-derives it.
285
+ * Document-scoped: the document's page edits, each one change of its
286
+ * history, and which of its pages are selected. Turning a page relative to
287
+ * its own rotation lives in the controller, so no app re-derives it.
178
288
  */
179
289
  const pageEditPlugin = () => (0, _embedpdf_core.definePlugin)({
180
290
  id: "page-edit",
181
291
  scope: "document",
182
292
  token: require_contract.PageEditToken,
293
+ state: initialPageEditState,
183
294
  create: createPageEditController
184
295
  });
185
296
  //#endregion
297
+ //#region src/state.ts
298
+ /**
299
+ * The pages page's State table as code: what `usePageEditState()` returns,
300
+ * and the same fields in every other framework.
301
+ */
302
+ const pageEditState = (0, _embedpdf_core.defineState)(require_contract.PageEditToken, {
303
+ read: (pageEdit) => ({
304
+ selected: pageEdit.getSelected(),
305
+ canEdit: pageEdit.canEdit(),
306
+ canExtract: pageEdit.canExtract()
307
+ }),
308
+ empty: {
309
+ selected: [],
310
+ canEdit: false,
311
+ canExtract: false
312
+ }
313
+ });
314
+ //#endregion
186
315
  exports.PageEditToken = require_contract.PageEditToken;
187
316
  exports.pageEditPlugin = pageEditPlugin;
317
+ exports.pageEditState = pageEditState;
188
318
 
189
319
  //# sourceMappingURL=index.cjs.map