@embedpdf/angular 3.0.0-next.14 → 3.0.0-next.15

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.
Files changed (81) hide show
  1. package/README.md +309 -0
  2. package/dist/README.md +309 -0
  3. package/dist/actions/index.d.ts +34 -16
  4. package/dist/anchored/index.d.ts +67 -0
  5. package/dist/annotation/index.d.ts +599 -0
  6. package/dist/commands/index.d.ts +109 -0
  7. package/dist/fesm2022/embedpdf-angular-actions.mjs +117 -36
  8. package/dist/fesm2022/embedpdf-angular-actions.mjs.map +1 -1
  9. package/dist/fesm2022/embedpdf-angular-anchored.mjs +148 -0
  10. package/dist/fesm2022/embedpdf-angular-anchored.mjs.map +1 -0
  11. package/dist/fesm2022/embedpdf-angular-annotation.mjs +1960 -0
  12. package/dist/fesm2022/embedpdf-angular-annotation.mjs.map +1 -0
  13. package/dist/fesm2022/embedpdf-angular-commands.mjs +235 -0
  14. package/dist/fesm2022/embedpdf-angular-commands.mjs.map +1 -0
  15. package/dist/fesm2022/embedpdf-angular-form.mjs +1074 -0
  16. package/dist/fesm2022/embedpdf-angular-form.mjs.map +1 -0
  17. package/dist/fesm2022/embedpdf-angular-history.mjs +54 -0
  18. package/dist/fesm2022/embedpdf-angular-history.mjs.map +1 -0
  19. package/dist/fesm2022/embedpdf-angular-i18n.mjs +100 -0
  20. package/dist/fesm2022/embedpdf-angular-i18n.mjs.map +1 -0
  21. package/dist/fesm2022/embedpdf-angular-interaction.mjs +157 -0
  22. package/dist/fesm2022/embedpdf-angular-interaction.mjs.map +1 -0
  23. package/dist/fesm2022/embedpdf-angular-link.mjs +306 -0
  24. package/dist/fesm2022/embedpdf-angular-link.mjs.map +1 -0
  25. package/dist/fesm2022/embedpdf-angular-measurement.mjs +106 -0
  26. package/dist/fesm2022/embedpdf-angular-measurement.mjs.map +1 -0
  27. package/dist/fesm2022/embedpdf-angular-metadata.mjs +77 -0
  28. package/dist/fesm2022/embedpdf-angular-metadata.mjs.map +1 -0
  29. package/dist/fesm2022/embedpdf-angular-page-edit.mjs +65 -0
  30. package/dist/fesm2022/embedpdf-angular-page-edit.mjs.map +1 -0
  31. package/dist/fesm2022/embedpdf-angular-page-view.mjs +297 -0
  32. package/dist/fesm2022/embedpdf-angular-page-view.mjs.map +1 -0
  33. package/dist/fesm2022/embedpdf-angular-redaction.mjs +93 -0
  34. package/dist/fesm2022/embedpdf-angular-redaction.mjs.map +1 -0
  35. package/dist/fesm2022/embedpdf-angular-render.mjs +331 -69
  36. package/dist/fesm2022/embedpdf-angular-render.mjs.map +1 -1
  37. package/dist/fesm2022/embedpdf-angular-runtime.mjs +1147 -390
  38. package/dist/fesm2022/embedpdf-angular-runtime.mjs.map +1 -1
  39. package/dist/fesm2022/embedpdf-angular-search.mjs +251 -0
  40. package/dist/fesm2022/embedpdf-angular-search.mjs.map +1 -0
  41. package/dist/fesm2022/embedpdf-angular-selection.mjs +477 -0
  42. package/dist/fesm2022/embedpdf-angular-selection.mjs.map +1 -0
  43. package/dist/fesm2022/embedpdf-angular-shell.mjs +177 -0
  44. package/dist/fesm2022/embedpdf-angular-shell.mjs.map +1 -0
  45. package/dist/fesm2022/embedpdf-angular-signature.mjs +120 -0
  46. package/dist/fesm2022/embedpdf-angular-signature.mjs.map +1 -0
  47. package/dist/fesm2022/embedpdf-angular-stage.mjs +688 -288
  48. package/dist/fesm2022/embedpdf-angular-stage.mjs.map +1 -1
  49. package/dist/fesm2022/embedpdf-angular-stamp.mjs +169 -0
  50. package/dist/fesm2022/embedpdf-angular-stamp.mjs.map +1 -0
  51. package/dist/fesm2022/embedpdf-angular-testing.mjs +89 -0
  52. package/dist/fesm2022/embedpdf-angular-testing.mjs.map +1 -0
  53. package/dist/fesm2022/embedpdf-angular-toolbar.mjs +1123 -0
  54. package/dist/fesm2022/embedpdf-angular-toolbar.mjs.map +1 -0
  55. package/dist/fesm2022/embedpdf-angular-view-manager.mjs +69 -0
  56. package/dist/fesm2022/embedpdf-angular-view-manager.mjs.map +1 -0
  57. package/dist/fesm2022/embedpdf-angular.mjs +23 -3
  58. package/dist/fesm2022/embedpdf-angular.mjs.map +1 -1
  59. package/dist/form/index.d.ts +79 -0
  60. package/dist/history/index.d.ts +28 -0
  61. package/dist/i18n/index.d.ts +52 -0
  62. package/dist/index.d.ts +20 -0
  63. package/dist/interaction/index.d.ts +58 -0
  64. package/dist/link/index.d.ts +77 -0
  65. package/dist/measurement/index.d.ts +46 -0
  66. package/dist/metadata/index.d.ts +51 -0
  67. package/dist/page-edit/index.d.ts +22 -0
  68. package/dist/page-view/index.d.ts +94 -0
  69. package/dist/redaction/index.d.ts +38 -0
  70. package/dist/render/index.d.ts +45 -6
  71. package/dist/runtime/index.d.ts +579 -199
  72. package/dist/search/index.d.ts +71 -0
  73. package/dist/selection/index.d.ts +141 -0
  74. package/dist/shell/index.d.ts +88 -0
  75. package/dist/signature/index.d.ts +45 -0
  76. package/dist/stage/index.d.ts +242 -141
  77. package/dist/stamp/index.d.ts +58 -0
  78. package/dist/testing/index.d.ts +53 -0
  79. package/dist/toolbar/index.d.ts +257 -0
  80. package/dist/view-manager/index.d.ts +23 -0
  81. package/package.json +135 -14
@@ -1,150 +1,420 @@
1
- import { createKernel, docInfoListEquals } from '@embedpdf/core';
1
+ import { mergeSettings, viewerSettingsOf, VIEWER_WHOLE_SETTINGS, VIEWER_DEFAULTS, shallowEqual, DocumentsToken, PluginError, isLocalEngine, createKernel, isPluginError, returnsPromise, isReadMember, documentsState, documentState } from '@embedpdf/core';
2
2
  export * from '@embedpdf/core';
3
+ import { browserClock, pageClientSpace, pageLayersOf, createFontMounts, epdfThemeVariables } from '@embedpdf/web';
4
+ export { mountWebFont, saveFile } from '@embedpdf/web';
3
5
  import * as i0 from '@angular/core';
4
- import { inject, PLATFORM_ID, signal, computed, Injectable, makeEnvironmentProviders, InjectionToken, input, forwardRef, Directive, ViewContainerRef, TemplateRef, effect, ChangeDetectionStrategy, Component } from '@angular/core';
6
+ import { InjectionToken, signal, computed, inject, Injector, PLATFORM_ID, DestroyRef, untracked, runInInjectionContext, Injectable, effect, input, forwardRef, Directive, TemplateRef, ViewContainerRef, ChangeDetectorRef, afterNextRender, ElementRef, ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';
5
7
  import { isPlatformBrowser } from '@angular/common';
8
+ import { Observable } from 'rxjs';
6
9
 
7
10
  /**
8
- * The kernel host — one service owns the kernel; ONE tick signal bridges the
9
- * kernel's single change stream into Angular's signal graph. Everything else in
10
- * this adapter is `computed()` over it (`value()` is Angular's `useKernelValue`).
11
+ * The adapter's numbered errors, written like Angular's own (`NG0201`): a code, what went wrong,
12
+ * how to fix it, and the page that explains it. A code keeps its meaning once it ships, so a
13
+ * search for `EPDF-101` always finds the right answer.
11
14
  *
12
- * Two hosting modes share this class:
13
- * component-hosted `<epdf-viewer [engine] [plugins]>` (framework parity)
14
- * provider-hosted `provideEmbedPdf({...})` at route/app level
15
+ * Errors about the document (no document open yet, a permission missing) are the plugins'
16
+ * `PluginError`s, the same in every framework; these are only about how the app set up the
17
+ * adapter.
18
+ */
19
+ const ERRORS_PAGE = 'https://www.embedpdf.com/docs/headless/angular/errors';
20
+ /** A setup mistake, with its code: `EPDF-101`. */
21
+ class EpdfError extends Error {
22
+ code;
23
+ name = 'EpdfError';
24
+ constructor(code, problem) {
25
+ super(`${code}: ${problem} See ${ERRORS_PAGE}#${code.toLowerCase()}`);
26
+ this.code = code;
27
+ }
28
+ }
29
+ /** EPDF-101: a service, component or directive of the viewer, with no viewer above it. */
30
+ const noViewerError = (what) => new EpdfError('EPDF-101', `${what} found no viewer. Add provideEmbedPdf() to this component's providers, or to your app config.`);
31
+ /** EPDF-102: a plugin's service, where no viewer above it has that plugin. */
32
+ const noPluginError = (service, feature) => new EpdfError('EPDF-102', `inject(${service}) found no viewer with its plugin. Add ${feature} to provideEmbedPdf() ` +
33
+ `in this component's providers, or in your app config.`);
34
+ /** EPDF-103: something that draws on a page, outside a page. */
35
+ const outsidePageError = (what) => new EpdfError('EPDF-103', `${what} draws on a page. Put it inside <ng-template epdfPage> in an <epdf-stage>.`);
36
+ /** EPDF-104: part of a Stage, outside one. */
37
+ const outsideStageError = (what) => new EpdfError('EPDF-104', `${what} belongs to a Stage. Put it inside <epdf-stage>.`);
38
+ /** EPDF-105: a call that needs the engine, made while rendering on the server. */
39
+ const onServerError = (what) => new EpdfError('EPDF-105', `${what} needs the engine, which runs in the browser only. Call it from an event or ` +
40
+ `afterNextRender(), or put the viewer in a @defer block.`);
41
+
42
+ /**
43
+ * The DI tokens the runtime's pieces share, in a file of their own so the pieces that provide
44
+ * them and the ones that read them never import each other.
45
+ */
46
+ const EPDF_DOCUMENT_SCOPE = new InjectionToken('EPDF_DOCUMENT_SCOPE');
47
+ const EPDF_SETUP = new InjectionToken('EPDF_SETUP');
48
+
49
+ /**
50
+ * Settings before the kernel exists. With an engine that loads lazily, the app can read and
51
+ * change a plugin's settings (and the viewer's) before the kernel is there to keep them. This
52
+ * keeps the changes in order, shows what they add up to, and hands them to the kernel when it
53
+ * arrives, so `settings()` and `updateSettings()` work from the first moment, as they do in
54
+ * every other framework.
15
55
  *
16
- * CONSTRUCTION RULE (library law): nothing may read `host.kernel` while a
17
- * component is being CONSTRUCTED. Component-hosted configs resolve their inputs
18
- * lazily, so the kernel materializes on first real read (a template binding, an
19
- * effect) — after inputs are set. Every inject* primitive returns signals or
20
- * lazy methods and therefore respects this for free; only the raw
21
- * `injectKernel()` escape hatch can violate it (its doc says so).
56
+ * The merge is the kernel's own (`mergeSettings` from `@embedpdf/core`), so the settings shown
57
+ * before the kernel exists are the ones it then has.
58
+ */
59
+ /** One settings store's changes, kept until the kernel takes them. */
60
+ class SettingsBeforeStart {
61
+ changes = signal([], ...(ngDevMode ? [{ debugName: "changes" }] : []));
62
+ /** The settings the changes add up to, over what the app registered. */
63
+ current;
64
+ constructor(declaration) {
65
+ const registered = mergeSettings(declaration.defaults, declaration.registered, declaration.whole);
66
+ this.current = computed(() => this.changes().reduce((settings, change) => change === 'reset' ? registered : mergeSettings(settings, change, declaration.whole), registered), ...(ngDevMode ? [{ debugName: "current" }] : []));
67
+ }
68
+ update(change) {
69
+ this.changes.update((changes) => [...changes, change]);
70
+ }
71
+ reset() {
72
+ this.changes.update((changes) => [...changes, 'reset']);
73
+ }
74
+ /** Hand the changes to the kernel's store, in order, and forget them. */
75
+ applyTo(store) {
76
+ for (const change of this.changes()) {
77
+ if (change === 'reset')
78
+ store.resetSettings();
79
+ else
80
+ store.updateSettings(change);
81
+ }
82
+ this.changes.set([]);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The viewer behind `provideEmbedPdf()`: one per injector that provides it. In the browser it
88
+ * creates the kernel as soon as the engine is there (at once for an engine instance, later for a
89
+ * factory that loads one), starts it, opens the initial documents, runs the features' `setup`,
90
+ * and destroys it all with the injector. Given a kernel (`provideEmbedPdf({ kernel })`), it uses
91
+ * that one as it is, and leaves starting and destroying it to whoever made it.
92
+ *
93
+ * The kernel has one change stream; the host turns each change into a write of `revision`, and
94
+ * every signal in the adapter is a `computed()` over it (`read()`), which keeps its value, and
95
+ * wakes nothing, while what it reads is unchanged.
22
96
  *
23
- * SSR CONTRACT: the EmbedPDF subtree is browser-only. On the server the host
24
- * never boots and any `kernel` read throws — wrap the viewer region in `@defer`
25
- * (which renders its placeholder on the server) or a platform-guarded `@if`.
97
+ * On the server the host creates no kernel: every read gives its empty value and a call that
98
+ * needs the engine throws EPDF-105, so the page around the viewer still renders.
99
+ */
100
+ /**
101
+ * The viewer's settings as the config gives them (a setting left out is its default), the same
102
+ * way every framework's viewer reads its props; what `resetSettings()` goes back to. A kernel
103
+ * given has its own.
26
104
  */
105
+ const registeredViewerSettings = (config) => viewerSettingsOf('kernel' in config ? {} : config);
106
+ /**
107
+ * Whether `kernel` has a plugin that provides `token`. A document plugin's token has its scope
108
+ * (any other token reads as a workspace one), and a workspace plugin's capability is there from
109
+ * the moment the kernel is created.
110
+ */
111
+ const kernelProvides = (kernel, token) => kernel.scopeOf(token) === 'document' || kernel.tryCapability(token) !== null;
112
+ /** The settings of the plugin that provides `token` in `kernel`, or null when it has none. */
113
+ function kernelSettingsOf(kernel, token) {
114
+ try {
115
+ return kernel.settingsOf(token);
116
+ }
117
+ catch {
118
+ // The kernel's answer for a token no plugin provides, or a plugin without settings.
119
+ return null;
120
+ }
121
+ }
122
+ /** What the viewer is doing, as a kernel someone else started says it. */
123
+ const statusOfKernel = (status) => status === 'started' ? 'ready' : status === 'failed' ? 'error' : 'starting';
124
+ const isPromiseLike = (value) => typeof value.then === 'function';
27
125
  class EpdfKernelHost {
126
+ setup = inject(EPDF_SETUP);
127
+ injector = inject(Injector);
128
+ /** Whether this is a browser. On the server the host creates no kernel. */
28
129
  browser = isPlatformBrowser(inject(PLATFORM_ID));
29
- config = null;
30
- _kernel = null;
31
- /** The engine backing the kernel, and whether WE created it (a recipe) and
32
- * therefore must destroy it. A borrowed instance is left untouched. */
33
- _engine = null;
130
+ /** The kernel `provideEmbedPdf({ kernel })` was given: someone else's, used as it is. */
131
+ given = 'kernel' in this.setup.config ? this.setup.config.kernel : null;
132
+ /** Every plugin the features register, in their order: none for a kernel given, which has its own. */
133
+ plugins = this.given
134
+ ? []
135
+ : this.setup.features.flatMap((feature) => feature.plugins);
136
+ kernelState = signal(null, ...(ngDevMode ? [{ debugName: "kernelState" }] : []));
137
+ revisionState = signal(0, ...(ngDevMode ? [{ debugName: "revisionState" }] : []));
138
+ statusState = signal('starting', ...(ngDevMode ? [{ debugName: "statusState" }] : []));
139
+ errorState = signal(null, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
140
+ listeners = new Set();
141
+ viewerSettingsBeforeStart = new SettingsBeforeStart({
142
+ defaults: VIEWER_DEFAULTS,
143
+ registered: registeredViewerSettings(this.setup.config),
144
+ whole: VIEWER_WHOLE_SETTINGS,
145
+ });
146
+ pluginSettings = new Map();
147
+ pluginSettingsBeforeStart = new Map();
148
+ engine = null;
34
149
  ownsEngine = false;
35
- unsubscribe = null;
36
- initialDocuments;
37
- booted = false;
150
+ stopListening = null;
38
151
  destroyed = false;
39
- /** Bumped on every kernel notification — THE bridge from kernel-world to signals. */
40
- tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : []));
41
- /** Boot lifecycle. `ready`/`error` are derived sugar over it. */
42
- status = signal('starting', ...(ngDevMode ? [{ debugName: "status" }] : []));
43
- /** The STARTUP failure, if any. Per-document open failures are tab state
44
- * (`DocInfo.status === 'error'`), not host state. */
45
- error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
46
- /** True once `kernel.start()` resolved — which never touches the engine, so
47
- * the shell (and every workspace capability: i18n, view-manager, …) is alive
48
- * while WASM compiles or the transport connects. Workspace chrome may render
49
- * before this; gate on it (or on the document gate) where it matters. */
50
- ready = computed(() => this.status() === 'ready', ...(ngDevMode ? [{ debugName: "ready" }] : []));
51
- /** Register the (deferred) config. Called exactly once, by `EpdfViewer`'s
52
- * constructor or `provideEmbedPdf`'s factory. */
53
- connect(config) {
54
- if (this.config || this._kernel) {
55
- throw new Error('[embedpdf] kernel already configured for this injector');
56
- }
57
- this.config = config;
58
- }
59
- /** The kernel — materialized on first read (see the construction rule above). */
60
- get kernel() {
61
- if (!this._kernel) {
62
- if (!this.browser) {
63
- throw new Error('[embedpdf] the viewer does not render on the server — wrap the EmbedPDF subtree in @defer (its placeholder renders during SSR) or a browser-only @if');
64
- }
65
- if (!this.config) {
66
- throw new Error('[embedpdf] no kernel: wrap this subtree in <epdf-viewer> or add provideEmbedPdf(...) to your providers');
67
- }
68
- const { engine, plugins, initialDocuments } = this.config();
69
- // A thunk is host-owned: call it now, destroy on teardown. An instance is
70
- // borrowed and left untouched. Either way construction is synchronous and
71
- // inert (engines boot lazily on first use).
72
- this.ownsEngine = typeof engine === 'function';
73
- this._engine = this.ownsEngine ? engine() : engine;
74
- try {
75
- this._kernel = createKernel({ engine: this._engine, plugins });
76
- }
77
- catch (error) {
78
- // ngOnDestroy() only tears down via `_kernel`, which stays null on this
79
- // path — so an owned engine must be destroyed HERE or it leaks.
80
- if (this.ownsEngine)
81
- void this._engine.destroy();
82
- this._engine = null;
83
- this.ownsEngine = false;
84
- throw error;
85
- }
86
- // Warm up only once the kernel exists (a plugin-planning throw must not
87
- // start a boot we'd immediately have to unwind) — `warmup()` overlaps the
88
- // WASM/transport boot with the rest of initialization.
89
- this._engine.warmup?.();
90
- this.initialDocuments = initialDocuments;
91
- this.unsubscribe = this._kernel.subscribe(() => this.tick.update((n) => n + 1));
92
- }
93
- return this._kernel;
94
- }
95
- /** Start the kernel and open initial documents in the background. Idempotent;
96
- * a no-op on the server and after destroy. */
97
- boot() {
98
- if (this.booted || this.destroyed || !this.browser)
152
+ /**
153
+ * The kernel, once it exists: null while a lazily loaded engine is on its way, and on the
154
+ * server, where only a kernel given is there.
155
+ */
156
+ kernel = this.kernelState.asReadonly();
157
+ /** Goes up on every change of the kernel's state. */
158
+ revision = this.revisionState.asReadonly();
159
+ /** A kernel given says it itself: someone else starts it. */
160
+ status = this.given
161
+ ? this.read((kernel) => statusOfKernel(kernel.status()), () => 'starting')
162
+ : this.statusState.asReadonly();
163
+ /** Why the viewer failed to start, when `status()` is `'error'`. */
164
+ error = this.errorState.asReadonly();
165
+ /** The viewer's own settings, before the kernel exists and after. */
166
+ viewerSettings = this.read((kernel) => kernel.getSettings(), () => this.viewerSettingsBeforeStart.current(), shallowEqual);
167
+ constructor() {
168
+ inject(DestroyRef).onDestroy(() => this.destroy());
169
+ const { config } = this.setup;
170
+ // A kernel given is the kernel, wherever it was made.
171
+ if ('kernel' in config)
172
+ this.adopt(config.kernel);
173
+ if (!this.browser)
99
174
  return;
100
- this.booted = true;
101
- const kernel = this.kernel;
102
- void (async () => {
103
- try {
104
- await kernel.start();
105
- }
106
- catch (err) {
107
- if (!this.destroyed) {
108
- this.error.set(err);
109
- this.status.set('error');
110
- }
111
- return;
112
- }
113
- if (this.destroyed)
114
- return;
115
- this.status.set('ready');
116
- // Kernel-owned boot policy: all tabs appear immediately in array order;
117
- // the `active` entry (else the first) is selected; per-document
118
- // failures surface as that tab's `error`/`locked` status (this host's
119
- // `error` signal reports only startup failures).
120
- kernel.documents.openAll(this.initialDocuments ?? []);
121
- })();
122
- }
123
- /** Read a value derived from the kernel, cached by equality — Angular's
124
- * `useKernelValue`. The selector re-runs per kernel notification (cheap);
125
- * `equal` decides whether dependents ever see it. */
126
- value(select, equal = Object.is) {
175
+ if (!('kernel' in config))
176
+ this.start(config);
177
+ // After construction, because a feature's setup injects services, and they inject this host.
178
+ queueMicrotask(() => this.runFeatureSetups());
179
+ }
180
+ /**
181
+ * A value read from the kernel, as a signal: `select` runs again after each change of the
182
+ * kernel's state, and the signal changes only when `equal` says the value did. `empty` is its
183
+ * value while there is no kernel.
184
+ */
185
+ read(select, empty, equal = Object.is) {
127
186
  return computed(() => {
128
- this.tick();
129
- return select(this.kernel);
187
+ this.revisionState();
188
+ const kernel = this.kernelState();
189
+ return kernel ? select(kernel) : empty();
130
190
  }, { equal });
131
191
  }
132
- ngOnDestroy() {
192
+ /** Call `listener` after each change of the kernel's state, and once when the kernel arrives. */
193
+ subscribe(listener) {
194
+ this.listeners.add(listener);
195
+ return () => this.listeners.delete(listener);
196
+ }
197
+ /**
198
+ * An event of the kernel itself (its settings, a plugin's settings store), as an RxJS
199
+ * stream. A subscription made before the kernel exists starts listening when it arrives.
200
+ */
201
+ stream(pick) {
202
+ return new Observable((subscriber) => {
203
+ let listening = null;
204
+ const listen = () => {
205
+ const kernel = untracked(this.kernelState);
206
+ if (!kernel || listening)
207
+ return;
208
+ listening = new AbortController();
209
+ pick(kernel)((event) => subscriber.next(event), { signal: listening.signal });
210
+ };
211
+ const stop = this.subscribe(listen);
212
+ listen();
213
+ return () => {
214
+ stop();
215
+ listening?.abort();
216
+ };
217
+ });
218
+ }
219
+ /** Whether a plugin of this viewer provides `token` (the documents are always there). */
220
+ provides(token) {
221
+ if (token === DocumentsToken)
222
+ return true;
223
+ return this.given ? kernelProvides(this.given, token) : this.pluginFor(token) !== null;
224
+ }
225
+ /** Whether the plugin of this viewer that provides `token` has settings. */
226
+ hasSettings(token) {
227
+ return this.given
228
+ ? kernelSettingsOf(this.given, token) !== null
229
+ : Boolean(this.pluginFor(token)?.settings);
230
+ }
231
+ /**
232
+ * The kernel, for `call` (`'zoomIn()'`) on `capability` (`'stage'`), which needs it now:
233
+ * `not-ready` while the engine is still on its way, EPDF-105 on the server.
234
+ */
235
+ requireKernel(capability, call) {
236
+ const kernel = this.kernelState();
237
+ if (kernel)
238
+ return kernel;
239
+ if (!this.browser)
240
+ throw onServerError(call);
241
+ throw new PluginError('not-ready', capability, 'the viewer is still starting');
242
+ }
243
+ updateViewerSettings(change) {
244
+ const kernel = this.kernelState();
245
+ if (kernel)
246
+ kernel.updateSettings(change);
247
+ else
248
+ this.viewerSettingsBeforeStart.update(change);
249
+ }
250
+ resetViewerSettings() {
251
+ const kernel = this.kernelState();
252
+ if (kernel)
253
+ kernel.resetSettings();
254
+ else
255
+ this.viewerSettingsBeforeStart.reset();
256
+ }
257
+ /**
258
+ * A plugin's settings, with or without a document, and before the kernel too: they belong to
259
+ * the plugin as the app registered it. One binding per plugin, shared by every service.
260
+ */
261
+ settingsOf(token) {
262
+ const known = this.pluginSettings.get(token);
263
+ if (known)
264
+ return known;
265
+ const binding = this.given
266
+ ? this.givenSettingsOf(this.given, token)
267
+ : this.registeredSettingsOf(token);
268
+ this.pluginSettings.set(token, binding);
269
+ return binding;
270
+ }
271
+ /** A registered plugin's settings, kept from before the kernel exists. */
272
+ registeredSettingsOf(token) {
273
+ const plugin = this.pluginFor(token);
274
+ const declaration = plugin?.settings;
275
+ if (!plugin || !declaration) {
276
+ throw new PluginError('unsupported', token.name, 'this plugin has no settings');
277
+ }
278
+ const before = new SettingsBeforeStart(declaration);
279
+ this.pluginSettingsBeforeStart.set(plugin, before);
280
+ const store = (kernel) => kernel.settingsOf(token);
281
+ return {
282
+ current: this.read((kernel) => store(kernel).getSettings(), () => before.current(), shallowEqual),
283
+ update: (change) => {
284
+ const kernel = this.kernelState();
285
+ if (kernel)
286
+ store(kernel).updateSettings(change);
287
+ else
288
+ before.update(change);
289
+ },
290
+ reset: () => {
291
+ const kernel = this.kernelState();
292
+ if (kernel)
293
+ store(kernel).resetSettings();
294
+ else
295
+ before.reset();
296
+ },
297
+ };
298
+ }
299
+ /** A plugin's settings in a kernel given, which is there from the start. */
300
+ givenSettingsOf(kernel, token) {
301
+ const store = kernelSettingsOf(kernel, token);
302
+ if (!store)
303
+ throw new PluginError('unsupported', token.name, 'this plugin has no settings');
304
+ const current = () => store.getSettings();
305
+ return {
306
+ current: this.read(current, current, shallowEqual),
307
+ update: (change) => store.updateSettings(change),
308
+ reset: () => store.resetSettings(),
309
+ };
310
+ }
311
+ pluginFor(token) {
312
+ return this.plugins.find((plugin) => plugin.token === token) ?? null;
313
+ }
314
+ // ── lifetime ────────────────────────────────────────────────────────────────
315
+ /** Follow a kernel someone else made: its changes, and nothing of its lifetime. */
316
+ adopt(kernel) {
317
+ this.stopListening = kernel.subscribe(() => this.changed());
318
+ this.kernelState.set(kernel);
319
+ this.changed();
320
+ }
321
+ start(config) {
322
+ const source = config.engine;
323
+ const owned = typeof source === 'function';
324
+ let made;
325
+ try {
326
+ made = owned ? source() : source;
327
+ }
328
+ catch (error) {
329
+ this.fail(error);
330
+ return;
331
+ }
332
+ if (isPromiseLike(made)) {
333
+ made.then((engine) => this.attach(config, engine, owned), (error) => this.fail(error));
334
+ }
335
+ else {
336
+ this.attach(config, made, owned);
337
+ }
338
+ }
339
+ /** Create and start the kernel over `engine`. An engine this viewer made is destroyed with it. */
340
+ attach(config, engine, owned) {
341
+ if (this.destroyed) {
342
+ if (owned)
343
+ void engine.destroy();
344
+ return;
345
+ }
346
+ // Warming a local engine up now overlaps its WebAssembly boot with the plugins' setup.
347
+ if (isLocalEngine(engine))
348
+ engine.warmup();
349
+ let kernel;
350
+ try {
351
+ kernel = createKernel({
352
+ engine,
353
+ plugins: [...this.plugins],
354
+ settings: registeredViewerSettings(config),
355
+ clock: browserClock(),
356
+ });
357
+ }
358
+ catch (error) {
359
+ if (owned)
360
+ void engine.destroy();
361
+ this.fail(error);
362
+ return;
363
+ }
364
+ this.engine = engine;
365
+ this.ownsEngine = owned;
366
+ this.viewerSettingsBeforeStart.applyTo(kernel);
367
+ for (const [plugin, before] of this.pluginSettingsBeforeStart) {
368
+ before.applyTo(kernel.settingsOf(plugin.token));
369
+ }
370
+ this.stopListening = kernel.subscribe(() => this.changed());
371
+ this.kernelState.set(kernel);
372
+ this.changed();
373
+ kernel.start().then(() => {
374
+ if (this.destroyed)
375
+ return;
376
+ this.statusState.set('ready');
377
+ // Every tab appears at once, in order; a document that fails shows it in its own tab.
378
+ kernel.documents.openAll(config.initialDocuments ?? []);
379
+ }, (error) => {
380
+ if (!this.destroyed)
381
+ this.fail(error);
382
+ });
383
+ }
384
+ changed() {
385
+ this.revisionState.update((revision) => revision + 1);
386
+ for (const listener of [...this.listeners])
387
+ listener();
388
+ }
389
+ fail(error) {
390
+ this.errorState.set(error);
391
+ this.statusState.set('error');
392
+ console.error('[embedpdf] the viewer failed to start', error);
393
+ }
394
+ runFeatureSetups() {
395
+ if (this.destroyed)
396
+ return;
397
+ for (const feature of this.setup.features) {
398
+ if (feature.setup)
399
+ runInInjectionContext(this.injector, feature.setup);
400
+ }
401
+ }
402
+ destroy() {
133
403
  if (this.destroyed)
134
404
  return;
135
405
  this.destroyed = true;
136
- this.unsubscribe?.();
137
- // destroy() owns the full teardown now: it joins an in-flight start, closes
138
- // every document (engine handles included), and unwinds workspace plugins.
139
- // Async + idempotent, so fire-and-forget is safe in a sync ngOnDestroy.
140
- // Then destroy the engine IF we own it (a thunk) — kernel first so handles
141
- // close before the engine goes; `engine.destroy()` joins any in-flight
142
- // boot or no-ops if it never started.
143
- if (this._kernel) {
144
- const engine = this._engine;
145
- const owns = this.ownsEngine;
146
- void this._kernel.destroy().then(() => {
147
- if (owns && engine)
406
+ this.stopListening?.();
407
+ this.listeners.clear();
408
+ // A kernel given is its maker's to destroy.
409
+ if (this.given)
410
+ return;
411
+ const kernel = this.kernelState();
412
+ const engine = this.engine;
413
+ const owned = this.ownsEngine;
414
+ // The kernel first, which closes every document's engine handle, then the engine it ran on.
415
+ if (kernel) {
416
+ void kernel.destroy().then(() => {
417
+ if (owned && engine)
148
418
  void engine.destroy();
149
419
  });
150
420
  }
@@ -154,349 +424,836 @@ class EpdfKernelHost {
154
424
  }
155
425
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfKernelHost, decorators: [{
156
426
  type: Injectable
157
- }] });
158
- /**
159
- * Host the kernel in an environment injector (route or application providers).
160
- * The kernel's lifetime is the injector's — a route-level workspace tears down
161
- * when the route does. Unlocks chrome OUTSIDE any viewer subtree: a toolbar in
162
- * the app header and the stage in the main outlet share this one kernel.
163
- */
164
- function provideEmbedPdf(config) {
165
- return makeEnvironmentProviders([
166
- {
167
- provide: EpdfKernelHost,
168
- useFactory: () => {
169
- const host = new EpdfKernelHost();
170
- host.connect(() => config);
171
- host.boot();
172
- return host;
173
- },
174
- },
175
- ]);
176
- }
177
-
178
- /**
179
- * DI tokens shared by the runtime's primitives and directives — split out so
180
- * `inject.ts` (consumers) and `scope.ts` (providers) never import each other.
181
- */
182
- const EPDF_DOCUMENT_SCOPE = new InjectionToken('EPDF_DOCUMENT_SCOPE');
427
+ }], ctorParameters: () => [] });
183
428
 
184
429
  /**
185
- * The inject* primitives — Angular's spelling of the React runtime hooks
186
- * (`use*` → `inject*`, plain values → signals, same names otherwise).
430
+ * How the adapter reaches a plugin: its capability token, resolved for one document (the one in
431
+ * scope, else the active one) after every change of the kernel. A `CapabilityBinding` turns that
432
+ * into what Angular code wants: signals for values, late-bound methods for calls, and RxJS
433
+ * streams for events. Services, the Stage and the page layers are all built on it.
187
434
  *
188
- * All of them must run in an injection context (a constructor / field
189
- * initializer, like `inject()` itself). None of them read `host.kernel` during
190
- * construction — they return signals or lazy methods, so they are safe in
191
- * component-hosted mode where the kernel materializes after construction.
435
+ * Resolution is a read, not a cache: a document becomes resolvable when it's ready, while its
436
+ * id stays the same, so a capability looked up once by id would go stale.
192
437
  */
193
- const shallowArray = (a, b) => a === b || (a.length === b.length && a.every((x, i) => x === b[i]));
194
- function injectKernelHost() {
438
+ /** The viewer above this injection context; EPDF-101, naming `what`, when there is none. */
439
+ function injectKernelHost(what) {
195
440
  const host = inject(EpdfKernelHost, { optional: true });
196
- if (!host) {
197
- throw new Error('[embedpdf] no kernel: wrap this subtree in <epdf-viewer> or add provideEmbedPdf(...) to your providers');
198
- }
441
+ if (!host)
442
+ throw noViewerError(what);
199
443
  return host;
200
444
  }
201
- /** The raw kernel — materializes it immediately. Do NOT call while a component
202
- * is being constructed in component-hosted mode (inputs are not set yet);
203
- * prefer the signal-returning primitives, which defer the first kernel read. */
204
- function injectKernel() {
205
- return injectKernelHost().kernel;
206
- }
207
- /** Read a value derived from the kernel, cached by equality — `useKernelValue`. */
208
- function injectKernelValue(select, equal) {
209
- return injectKernelHost().value(select, equal);
445
+ const FOLLOW_ACTIVE = signal(null).asReadonly();
446
+ /**
447
+ * The document this injection context talks to: the id the nearest `[epdfDocumentScope]` (or a
448
+ * Stage with a `[document]`) gives, or null to follow the active document.
449
+ */
450
+ function injectDocumentScope() {
451
+ return inject(EPDF_DOCUMENT_SCOPE, { optional: true })?.id ?? FOLLOW_ACTIVE;
210
452
  }
211
- function injectActiveDocumentId() {
212
- return injectKernelValue((k) => k.documents.activeId());
453
+ /**
454
+ * A plugin's capability for this injection context's document, as a signal: null while it
455
+ * can't resolve (no such plugin, no document, or one that isn't ready yet). For plugin authors;
456
+ * apps use a plugin's service (`inject(EpdfSearch)`). Call it in an injection context.
457
+ */
458
+ function injectCapability(token) {
459
+ const binding = new CapabilityBinding(injectKernelHost(`injectCapability(${token.name})`), () => token, injectDocumentScope());
460
+ return binding.capability;
213
461
  }
214
- /** The document id for this injector subtree: the nearest `[epdfDocumentScope]`,
215
- * else the active document. */
216
- function injectDocumentId() {
217
- const scope = inject(EPDF_DOCUMENT_SCOPE, { optional: true });
218
- const active = injectActiveDocumentId();
219
- return computed(() => scope?.id() ?? active());
462
+ /** One capability token, for one document, as signals, calls and streams. */
463
+ class CapabilityBinding {
464
+ host;
465
+ token;
466
+ documentId;
467
+ /** The capability, or null while it can't resolve. */
468
+ capability;
469
+ /**
470
+ * @param token the token to resolve; a function, because a component's token is an input
471
+ * @param documentId the document to resolve it for, or null for the active one
472
+ */
473
+ constructor(host, token, documentId) {
474
+ this.host = host;
475
+ this.token = token;
476
+ this.documentId = documentId;
477
+ this.capability = host.read((kernel) => this.resolve(kernel), () => null);
478
+ }
479
+ /**
480
+ * A value read through the capability, as a signal that changes only when `equal` says the
481
+ * value did. `empty` while the capability can't resolve, and for the one read that can land
482
+ * between a document closing and its capability going away.
483
+ */
484
+ select(read, empty, equal = shallowEqual) {
485
+ return this.host.read((kernel) => {
486
+ const capability = this.resolve(kernel);
487
+ if (!capability)
488
+ return empty;
489
+ try {
490
+ return read(capability);
491
+ }
492
+ catch {
493
+ return empty;
494
+ }
495
+ }, () => empty, equal);
496
+ }
497
+ /**
498
+ * The capability, for `call`, which needs it now. Without a document a document plugin
499
+ * refuses with `not-ready` ("no document is open"), the same refusal every framework's
500
+ * stand-in gives (`standInFor` in `@embedpdf/core`); a token no plugin provides throws the
501
+ * kernel's reason, because that is a setup mistake.
502
+ */
503
+ require(call) {
504
+ const token = this.token();
505
+ const kernel = this.host.requireKernel(token.name, call);
506
+ const documentId = untracked(this.documentId) ?? undefined;
507
+ const capability = kernel.tryCapability(token, documentId);
508
+ if (capability)
509
+ return capability;
510
+ if (kernel.scopeOf(token) === 'document') {
511
+ throw new PluginError('not-ready', token.name, 'no document is open');
512
+ }
513
+ return kernel.capability(token, documentId);
514
+ }
515
+ /**
516
+ * The capability's method `name`, looked up when it's called, so a method kept in a field
517
+ * acts on the document current at that moment. The same function for the binding's lifetime.
518
+ *
519
+ * A read (a method named `get*`, `list*`, `is*`, `has*` or `can*`: `canUpdate()`,
520
+ * `getZoomLevel()`) is state too: called in a template, a `computed()` or an `effect()`, it's
521
+ * read again after each change of the kernel and of the document in scope, and wakes its
522
+ * reader only when its answer changes. So a button it disables follows the selection, the
523
+ * active document and its permissions. Other methods track nothing: an `effect()` that calls a
524
+ * verb doesn't run again because the verb changed the document.
525
+ *
526
+ * Without a document a method refuses with `not-ready`: one that returns a promise (the token's
527
+ * `promises`) returns the refusal as a rejected promise, so `.catch()` sees it; others throw.
528
+ */
529
+ method(name) {
530
+ return this.late(String(name), (capability) => capability, name);
531
+ }
532
+ /**
533
+ * The method `name` of the capability's namespace `namespace` (`custom.update()`), late-bound
534
+ * the way {@link method} is. A service gives a namespace an object of its own
535
+ * (`metadata.custom`), built from these, {@link select} and {@link stream}.
536
+ */
537
+ namespaceMethod(namespace, name) {
538
+ return this.late(`${String(namespace)}.${String(name)}`, (capability) => capability[namespace], name);
539
+ }
540
+ /**
541
+ * One of the capability's events, as an RxJS stream. A subscription follows the document:
542
+ * when it changes (another tab, a document that opens or closes), it moves to the new
543
+ * document's capability. Nothing is emitted while there is none.
544
+ */
545
+ stream(pick) {
546
+ return new Observable((subscriber) => {
547
+ let current = null;
548
+ let listening = null;
549
+ const follow = () => {
550
+ const kernel = untracked(this.host.kernel);
551
+ const next = kernel ? untracked(() => this.resolve(kernel)) : null;
552
+ if (next === current)
553
+ return;
554
+ listening?.abort();
555
+ listening = null;
556
+ current = next;
557
+ if (!next)
558
+ return;
559
+ listening = new AbortController();
560
+ pick(next)((event) => subscriber.next(event), { signal: listening.signal });
561
+ };
562
+ const stop = this.host.subscribe(follow);
563
+ follow();
564
+ return () => {
565
+ stop();
566
+ listening?.abort();
567
+ };
568
+ });
569
+ }
570
+ resolve(kernel) {
571
+ return kernel.tryCapability(this.token(), this.documentId() ?? undefined);
572
+ }
573
+ /** `owner(capability)[name]`, looked up when called; a read tracks what its answer reads. */
574
+ late(label, owner, name) {
575
+ const call = `${label}()`;
576
+ const run = (args) => {
577
+ let target;
578
+ try {
579
+ target = owner(this.require(call));
580
+ }
581
+ catch (error) {
582
+ // Not ready: a method that returns a promise rejects, as the capability would.
583
+ if (isPluginError(error, 'not-ready') && returnsPromise(this.token(), label)) {
584
+ return Promise.reject(error);
585
+ }
586
+ throw error;
587
+ }
588
+ return target[name].apply(target, args);
589
+ };
590
+ if (!isReadMember(String(name))) {
591
+ return ((...args) => untracked(() => run(args)));
592
+ }
593
+ // A computed of its own for each call: whoever reads the check depends on its answer, not
594
+ // on every change of the kernel. Outside a reactive context it's one plain call.
595
+ return ((...args) => computed(() => {
596
+ this.documentId();
597
+ this.host.revision();
598
+ return run(args);
599
+ })());
600
+ }
220
601
  }
602
+
221
603
  /**
222
- * Resolve a capability against a REACTIVE token — for components that take the
223
- * token as an input (`injectCapability` is the fixed-token sugar). Resolution
224
- * is a REACTIVE read (`tryCapability` through the kernel's one change
225
- * stream), not a computed over the document id — under the request-time
226
- * lifecycle a document can become resolvable while its id stays the same, so
227
- * any id-keyed cache goes stale; subscribing makes staleness structurally
228
- * impossible. Fail-fast like React's `useCapability`: while unresolvable,
229
- * this re-runs the strict resolver so the kernel's truthful reason (`no
230
- * capability` / `no document` / `document is loading|locked`) is what throws.
231
- */
232
- function injectCapabilityFor(token) {
233
- const host = injectKernelHost();
234
- const scope = inject(EPDF_DOCUMENT_SCOPE, { optional: true });
235
- const cap = host.value((k) => k.tryCapability(token(), scope?.id() ?? undefined));
236
- return computed(() => cap() ?? host.kernel.capability(token(), scope?.id() ?? undefined));
237
- }
238
- function injectCapability(token) {
239
- return injectCapabilityFor(() => token);
604
+ * One service per plugin: `inject(EpdfSearch)` has the plugin's methods, every value of its
605
+ * State table as a signal (`search.hitCount()`), its events as RxJS streams
606
+ * (`search.completed$`), and, when the plugin has settings, `settings()`, `getSettings()`,
607
+ * `updateSettings()`, `resetSettings()` and `settingsChanged$`. `pluginService()` builds all of that from what the
608
+ * plugin declares already, so a plugin's service is a few lines; the package README's "Adding a
609
+ * plugin binding" shows one.
610
+ *
611
+ * A service acts on the document of the injector that created it: under `[epdfDocumentScope]`
612
+ * that document, else the active one. Its methods are looked up when they're called, so a
613
+ * method kept in a field acts on the document current at that moment.
614
+ */
615
+ /**
616
+ * The base every plugin service extends. Its members are for the service's own class, which
617
+ * adds what the generic part can't: a signal with arguments (`hitsOn(page)`), a namespace
618
+ * (`custom.fields()`), a helper that needs the caller's injection context.
619
+ */
620
+ class EpdfPluginService {
621
+ /** The plugin's capability for this service's document. */
622
+ binding;
623
+ constructor(spec) {
624
+ const host = inject(EpdfKernelHost, { optional: true });
625
+ // A plugin's service names its feature, whether the viewer is missing or only the plugin.
626
+ if (spec.feature && !host?.provides(spec.token))
627
+ throw noPluginError(spec.name, spec.feature);
628
+ if (!host)
629
+ throw noViewerError(`inject(${spec.name})`);
630
+ this.binding = new CapabilityBinding(host, () => spec.token, injectDocumentScope());
631
+ }
240
632
  }
241
- /** Like `injectCapabilityFor`, but null while the token can't resolve (no
242
- * plugin, no document, or a document that isn't ready yet). */
243
- function injectOptionalCapabilityFor(token) {
244
- const host = injectKernelHost();
245
- const scope = inject(EPDF_DOCUMENT_SCOPE, { optional: true });
246
- return host.value((k) => k.tryCapability(token(), scope?.id() ?? undefined));
633
+ /**
634
+ * The base class of a plugin's service, built from the plugin's declarations:
635
+ *
636
+ * @Injectable({ providedIn: 'root' })
637
+ * export class EpdfSearch extends pluginService({
638
+ * name: 'EpdfSearch',
639
+ * feature: 'withSearch()',
640
+ * token: SearchToken,
641
+ * state: searchState,
642
+ * methods: ['search', 'clear', 'goToHit'],
643
+ * events: ['onCompleted'],
644
+ * }) {}
645
+ *
646
+ * `providedIn: 'root'` is only the fallback that turns a missing viewer into EPDF-102: the
647
+ * service's feature provides it next to the viewer, and `[epdfDocumentScope]` again.
648
+ */
649
+ function pluginService(spec) {
650
+ class PluginService extends EpdfPluginService {
651
+ constructor() {
652
+ super(spec);
653
+ const members = this;
654
+ const binding = this.binding;
655
+ if (spec.state) {
656
+ const state = spec.state;
657
+ // One read of the whole table, compared field by field, then one signal per field: a
658
+ // template that reads `hitCount()` wakes only when the hit count changes.
659
+ const whole = binding.select((capability) => state.read(capability), state.empty);
660
+ for (const field of Object.keys(state.empty)) {
661
+ members[field] = computed(() => whole()[field]);
662
+ }
663
+ }
664
+ const ensure = spec.ensure ?? spec.state?.ensure;
665
+ if (ensure) {
666
+ effect(() => {
667
+ const capability = binding.capability();
668
+ // The state says when the load fails (its status), so the rejection has nothing to add.
669
+ if (capability)
670
+ untracked(() => ensure(capability).catch(() => { }));
671
+ });
672
+ }
673
+ for (const name of spec.methods)
674
+ members[name] = binding.method(name);
675
+ for (const event of spec.events ?? []) {
676
+ const stream = `${event.charAt(2).toLowerCase()}${event.slice(3)}$`;
677
+ members[stream] = binding.stream((capability) => capability[event]);
678
+ }
679
+ // Settings belong to the plugin as the app registered it, not to a document: they work
680
+ // with no document open, and before the kernel exists.
681
+ const host = binding.host;
682
+ if (host.hasSettings(spec.token)) {
683
+ const settings = host.settingsOf(spec.token);
684
+ members['settings'] = settings.current;
685
+ // A read, like every `get*` method: a template or an effect that calls it follows it.
686
+ members['getSettings'] = () => settings.current();
687
+ members['updateSettings'] = settings.update;
688
+ members['resetSettings'] = settings.reset;
689
+ members['settingsChanged$'] = host.stream((kernel) => kernel.settingsOf(spec.token).onSettingsChanged);
690
+ }
691
+ }
692
+ }
693
+ return PluginService;
247
694
  }
248
- function injectOptionalCapability(token) {
249
- return injectOptionalCapabilityFor(() => token);
695
+
696
+ /**
697
+ * The viewer's own services, which `provideEmbedPdf()` always has:
698
+ *
699
+ * inject(EpdfViewer) the viewer's settings (`identity`, `scope`, `accent`, `page`) and status
700
+ * inject(EpdfDocuments) open, close, unlock and download documents; every tab, as signals
701
+ * inject(EpdfDocument) the document this part of the template talks to, as signals
702
+ */
703
+ /**
704
+ * The viewer itself: its settings, and whether it started. The config given to
705
+ * `provideEmbedPdf()` is read once; what may change while the app runs changes here:
706
+ *
707
+ * effect(() => viewer.updateSettings({ identity: auth.identity() }));
708
+ *
709
+ * Documents opened after a change of `identity` or `scope` use the new value; open ones keep
710
+ * theirs.
711
+ */
712
+ class EpdfViewer {
713
+ host = injectKernelHost('inject(EpdfViewer)');
714
+ /** The viewer's settings: `identity`, `scope`, `accent` and `page`. */
715
+ settings = this.host.viewerSettings;
716
+ /** `'starting'`, `'ready'`, or `'error'` when the engine or a plugin failed to start. */
717
+ status = this.host.status;
718
+ /** Why the viewer failed to start, when `status()` is `'error'`. */
719
+ error = this.host.error;
720
+ /** Fires once for each call that changed a setting. */
721
+ settingsChanged$ = this.host.stream((kernel) => kernel.onSettingsChanged);
722
+ /** Merge `changes` into the viewer's settings: `page` merges, `identity` and `scope` replace. */
723
+ updateSettings(changes) {
724
+ this.host.updateViewerSettings(changes);
725
+ }
726
+ /** Go back to the settings `provideEmbedPdf()` was given. */
727
+ resetSettings() {
728
+ this.host.resetViewerSettings();
729
+ }
730
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
731
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, providedIn: 'root' });
250
732
  }
251
- /** Subscribe to a selector over a (document-resolved) capability. */
252
- function injectSelectorFor(token, select, equal) {
253
- const host = injectKernelHost();
254
- const cap = injectCapabilityFor(token);
255
- return host.value(() => select(cap()), equal);
733
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, decorators: [{
734
+ type: Injectable,
735
+ args: [{ providedIn: 'root' }]
736
+ }] });
737
+ /**
738
+ * The documents: open, close, unlock, retry and download them, and every tab with the active
739
+ * one as signals (`documents()`, `activeId()`). Under `[epdfDocumentScope]` the calls that
740
+ * leave out the document use that one: `download()` downloads the document in scope.
741
+ */
742
+ class EpdfDocuments extends pluginService({
743
+ name: 'EpdfDocuments',
744
+ token: DocumentsToken,
745
+ state: documentsState,
746
+ methods: [
747
+ 'open',
748
+ 'canOpenFiles',
749
+ 'openAll',
750
+ 'retry',
751
+ 'rename',
752
+ 'unlock',
753
+ 'close',
754
+ 'closeAll',
755
+ 'setActive',
756
+ 'getActiveId',
757
+ 'getActive',
758
+ 'list',
759
+ 'get',
760
+ 'has',
761
+ 'getCount',
762
+ 'getOrder',
763
+ 'setOrder',
764
+ 'move',
765
+ 'swap',
766
+ 'download',
767
+ 'downloadLayer',
768
+ 'canDownload',
769
+ 'canPrint',
770
+ 'listPages',
771
+ 'getPage',
772
+ 'getPageIndex',
773
+ 'getRevision',
774
+ ],
775
+ events: [
776
+ 'onOpened',
777
+ 'onOpenFailed',
778
+ 'onLocked',
779
+ 'onClosed',
780
+ 'onActiveChanged',
781
+ 'onPagesChanged',
782
+ 'onUnsavedChangesChanged',
783
+ ],
784
+ }) {
785
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
786
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, providedIn: 'root' });
256
787
  }
257
- function injectSelector(token, select, equal) {
258
- return injectSelectorFor(() => token, select, equal);
788
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, decorators: [{
789
+ type: Injectable,
790
+ args: [{ providedIn: 'root' }]
791
+ }] });
792
+ const NO_PAGES = Object.freeze([]);
793
+ /**
794
+ * The document this part of the template talks to: the one `[epdfDocumentScope]` names, else
795
+ * the active one. Its fields are signals (`id()`, `name()`, `status()`, `pageCount()`,
796
+ * `hasUnsavedChanges()`, `passwordProvided()`, `error()`), and `pages()` its pages. With no
797
+ * document it reads as one still on its way: an empty `id` and `status() === 'loading'`.
798
+ */
799
+ class EpdfDocument extends pluginService({
800
+ name: 'EpdfDocument',
801
+ token: DocumentsToken,
802
+ state: documentState,
803
+ methods: [],
804
+ }) {
805
+ /**
806
+ * The document's pages, in order: each with its `ref`, `index`, `label`, size and rotation.
807
+ * Needs no Stage. The same array until a page is added, removed, moved or rotated; empty
808
+ * with no document.
809
+ */
810
+ pages = this.binding.select((documents) => documents.listPages(), NO_PAGES, Object.is);
811
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
812
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, providedIn: 'root' });
259
813
  }
814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, decorators: [{
815
+ type: Injectable,
816
+ args: [{ providedIn: 'root' }]
817
+ }] });
818
+
819
+ const scopedServices = [EpdfDocuments, EpdfDocument];
260
820
  /**
261
- * Null-safe `injectSelector`: `fallback` whenever the token can't resolve — no
262
- * provider, or a document-scoped token with no document. For chrome that stays
263
- * mounted across the empty-workspace state (a zoom readout, a mode band).
264
- * `injectSelector` stays strict (fail-fast) for code that KNOWS a document
265
- * exists — e.g. anything behind the document gate.
821
+ * Every service that acts on one document: the viewer's own, and each feature's, added when a
822
+ * `provideEmbedPdf()` call names the feature. `[epdfDocumentScope]` and a Stage list this array
823
+ * in their `providers`, so a service injected under them acts on their document.
266
824
  *
267
- * The `select` guard also swallows reads through a capability whose document
268
- * closed between the store notification and this read — that teardown race
269
- * resolves to `fallback` for one frame, then recomputes against the new state.
270
- */
271
- function injectOptionalSelectorFor(token, select, fallback, equal) {
272
- const host = injectKernelHost();
273
- const cap = injectOptionalCapabilityFor(token);
274
- return host.value(() => {
275
- const c = cap();
276
- if (c === null)
277
- return fallback;
278
- try {
279
- return select(c);
280
- }
281
- catch {
282
- return fallback;
825
+ * It is one array that grows, not a copy: Angular reads a directive's providers when its
826
+ * template is first created, which is after the `provideEmbedPdf()` calls in decorators and the
827
+ * app config ran. A service whose feature is first named later (a lazily loaded route's
828
+ * providers) is still provided by its viewer, and acts on the active document there.
829
+ */
830
+ const EPDF_SCOPED_SERVICES = scopedServices;
831
+ /** The viewer, with the plugins its features register; or the viewer of a kernel given. */
832
+ function provideEmbedPdf(config, ...features) {
833
+ for (const feature of features) {
834
+ for (const service of feature.services ?? []) {
835
+ if (!scopedServices.includes(service))
836
+ scopedServices.push(service);
283
837
  }
284
- }, equal);
285
- }
286
- function injectOptionalSelector(token, select, fallback, equal) {
287
- return injectOptionalSelectorFor(() => token, select, fallback, equal);
288
- }
289
- function injectDocuments() {
290
- const host = injectKernelHost();
291
- return {
292
- docs: host.value((k) => k.documents.list(), docInfoListEquals),
293
- activeId: host.value((k) => k.documents.activeId()),
294
- open: (input, options) => host.kernel.documents.open(input, options),
295
- unlock: (id, input) => host.kernel.documents.unlock(id, input),
296
- close: (id) => host.kernel.documents.close(id),
297
- setActive: (id) => host.kernel.documents.setActive(id),
298
- move: (id, toIndex) => host.kernel.documents.move(id, toIndex),
299
- swap: (a, b) => host.kernel.documents.swap(a, b),
300
- download: (id, opts) => host.kernel.documents.download(id, opts),
301
- downloadLayer: (id) => host.kernel.documents.downloadLayer(id),
302
- };
838
+ }
839
+ const setup = { config, features };
840
+ return [
841
+ { provide: EPDF_SETUP, useValue: setup },
842
+ EpdfKernelHost,
843
+ EpdfViewer,
844
+ EpdfDocuments,
845
+ EpdfDocument,
846
+ ...features.flatMap((feature) => [...(feature.services ?? []), ...(feature.providers ?? [])]),
847
+ ];
303
848
  }
304
849
 
305
850
  /**
306
- * Document scoping — hierarchical DI is Angular's native React-context.
851
+ * Which document a part of the template talks to, and whether it has one:
307
852
  *
308
- * `[epdfDocumentScope]` binds a subtree to a SPECIFIC document (panes,
309
- * comparison); `*epdfDocumentGate` renders a subtree only WHILE it has one.
310
- * The gate is a structural directive on purpose: a template genuinely defers
311
- * creation, which a conditional `<ng-content>` slot cannot (Angular
312
- * instantiates projected content eagerly). Document-scoped UI — the Stage,
313
- * zoom/page controls, panels — belongs behind it (or behind the app's own
314
- * `@if (documentId())`); workspace chrome lives outside it.
853
+ * <div [epdfDocumentScope]="id"> everything inside talks to that document
854
+ * <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
855
+ *
856
+ * The gate is a structural directive on purpose: a template really defers creating what's
857
+ * inside, which a conditional `<ng-content>` can't (Angular creates projected content whether
858
+ * or not it's shown). Document UI (the Stage, a page counter) goes behind it; workspace UI (a
859
+ * toolbar frame, an open button) stays outside and renders at once.
860
+ */
861
+ /**
862
+ * Binds everything inside the element to one document: services injected there, the Stage
863
+ * and its layers act on it instead of the active document.
315
864
  */
316
865
  class EpdfDocumentScope {
866
+ /** The document's id. */
317
867
  id = input.required(...(ngDevMode ? [{ debugName: "id", alias: 'epdfDocumentScope' }] : [{ alias: 'epdfDocumentScope' }]));
318
868
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentScope, deps: [], target: i0.ɵɵFactoryTarget.Directive });
319
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentScope, isStandalone: true, selector: "[epdfDocumentScope]", inputs: { id: { classPropertyName: "id", publicName: "epdfDocumentScope", isSignal: true, isRequired: true, transformFunction: null } }, providers: [{ provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) }], ngImport: i0 });
869
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentScope, isStandalone: true, selector: "[epdfDocumentScope]", inputs: { id: { classPropertyName: "id", publicName: "epdfDocumentScope", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
870
+ { provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
871
+ // A service injected inside gets an instance of its own here, bound to this document.
872
+ EPDF_SCOPED_SERVICES,
873
+ ], ngImport: i0 });
320
874
  }
321
875
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentScope, decorators: [{
322
876
  type: Directive,
323
877
  args: [{
324
878
  selector: '[epdfDocumentScope]',
325
- standalone: true,
326
- providers: [{ provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) }],
879
+ providers: [
880
+ { provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
881
+ // A service injected inside gets an instance of its own here, bound to this document.
882
+ EPDF_SCOPED_SERVICES,
883
+ ],
327
884
  }]
328
885
  }] });
329
886
  /**
330
- * Render the host template only while this subtree has a document — the
331
- * structural way to say "this UI is defined over a document". Sibling of
332
- * `[epdfDocumentScope]`, which picks WHICH document; this one handles WHETHER.
887
+ * Renders its content only while this part of the template has a ready document, with that
888
+ * document as `let document`. Otherwise it renders `locked` for a document waiting for its
889
+ * password, `error` for one that couldn't be opened, and `fallback` for everything else (none
890
+ * open yet, still opening), each with the document as `let-document`. Without `locked` or
891
+ * `error` those states show `fallback` too.
333
892
  *
334
- * <section *epdfDocumentGate="; fallback: empty">…document UI…</section>
335
- * <ng-template #empty>Drop a PDF to get started</ng-template>
893
+ * <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
894
+ * <ng-template #opening>Opening…</ng-template>
895
+ * <ng-template #locked let-document><app-password-form [document]="document" /></ng-template>
336
896
  *
337
- * The subtree stays mounted across a CHANGE of document (active-tab switch) —
338
- * only the has-a-document boolean creates/destroys it.
897
+ * The content stays mounted when another ready document becomes the active one; only its
898
+ * `document` changes.
339
899
  */
340
900
  class EpdfDocumentGate {
341
- /** Shown while this subtree has NO document (empty workspace, docs still opening). */
901
+ /** Shown while there is no ready document: none is open, or it's still opening. */
342
902
  fallback = input(null, ...(ngDevMode ? [{ debugName: "fallback", alias: 'epdfDocumentGateFallback' }] : [{
343
903
  alias: 'epdfDocumentGateFallback',
344
904
  }]));
905
+ /** Shown while the document waits for its password; `fallback` without it. */
906
+ locked = input(null, ...(ngDevMode ? [{ debugName: "locked", alias: 'epdfDocumentGateLocked' }] : [{
907
+ alias: 'epdfDocumentGateLocked',
908
+ }]));
909
+ /** Shown when the document couldn't be opened; `fallback` without it. */
910
+ error = input(null, ...(ngDevMode ? [{ debugName: "error", alias: 'epdfDocumentGateError' }] : [{
911
+ alias: 'epdfDocumentGateError',
912
+ }]));
913
+ static ngTemplateContextGuard(_gate, context) {
914
+ return true;
915
+ }
345
916
  constructor() {
346
- const docId = injectDocumentId();
347
- const hasDocument = computed(() => docId() !== null, ...(ngDevMode ? [{ debugName: "hasDocument" }] : []));
348
- const container = inject(ViewContainerRef);
917
+ const document = new CapabilityBinding(injectKernelHost('*epdfDocumentGate'), () => DocumentsToken, injectDocumentScope()).select(documentState.read, documentState.empty);
349
918
  const content = inject(TemplateRef);
919
+ const container = inject(ViewContainerRef);
920
+ const changes = inject(ChangeDetectorRef);
921
+ let shown = null;
350
922
  effect(() => {
351
- container.clear();
352
- if (hasDocument()) {
353
- container.createEmbeddedView(content);
354
- }
355
- else {
356
- // `fallback` is only tracked while gated — swapping fallback templates
357
- // never recreates live document UI.
358
- const fallbackTemplate = this.fallback();
359
- if (fallbackTemplate)
360
- container.createEmbeddedView(fallbackTemplate);
361
- }
923
+ const current = document();
924
+ const template = this.templateFor(current, content);
925
+ untracked(() => {
926
+ if (shown && shown.template === template) {
927
+ // The same template for another document (a tab switch): keep the view, pass it on.
928
+ shown.view.context.$implicit = current;
929
+ shown.view.markForCheck();
930
+ return;
931
+ }
932
+ container.clear();
933
+ shown = template
934
+ ? { template, view: container.createEmbeddedView(template, { $implicit: current }) }
935
+ : null;
936
+ changes.markForCheck();
937
+ });
362
938
  });
363
939
  }
940
+ templateFor(document, content) {
941
+ if (document.status === 'ready')
942
+ return content;
943
+ const locked = this.locked();
944
+ if (document.status === 'locked' && locked)
945
+ return locked;
946
+ const failed = this.error();
947
+ if (document.status === 'error' && failed)
948
+ return failed;
949
+ return this.fallback() ?? null;
950
+ }
364
951
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentGate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
365
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentGate, isStandalone: true, selector: "[epdfDocumentGate]", inputs: { fallback: { classPropertyName: "fallback", publicName: "epdfDocumentGateFallback", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
952
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentGate, isStandalone: true, selector: "[epdfDocumentGate]", inputs: { fallback: { classPropertyName: "fallback", publicName: "epdfDocumentGateFallback", isSignal: true, isRequired: false, transformFunction: null }, locked: { classPropertyName: "locked", publicName: "epdfDocumentGateLocked", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "epdfDocumentGateError", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
366
953
  }
367
954
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentGate, decorators: [{
368
955
  type: Directive,
369
- args: [{ selector: '[epdfDocumentGate]', standalone: true }]
956
+ args: [{ selector: '[epdfDocumentGate]' }]
370
957
  }], ctorParameters: () => [] });
371
958
 
372
959
  /**
373
- * `<epdf-viewer>` — component-hosted kernel (framework parity with React's
374
- * `<Viewer>`). Provides `EpdfKernelHost` at its element injector, boots it, and
375
- * projects content UNCONDITIONALLY: it is NOT a gate. Angular instantiates
376
- * projected content eagerly whether or not a slot renders it, so a conditional
377
- * `<ng-content>` cannot defer anything — real gating belongs to templates the
378
- * APP owns: `@if (ready())` for boot chrome, `*epdfDocumentGate` (or
379
- * `@if (documentId())`) for document UI.
960
+ * The page context: what a layer drawn on a page knows about that page. A layer reads only
961
+ * this, never the Stage, so the same layer works on a Stage's page and on a standalone page
962
+ * view. Inside `<ng-template epdfPage>`, `let-page` gives it to the template and `injectPage()`
963
+ * to a component.
380
964
  *
381
- * Prefer `provideEmbedPdf(...)` at route/app level when chrome lives outside
382
- * one subtree; this component is sugar over the same host.
383
- */
384
- class EpdfViewer {
385
- /** The engine, as an instance OR a thunk ({@link EngineFactory}). An
386
- * instance is borrowed (never destroyed here); a thunk is host-owned
387
- * (constructed then destroyed with the viewer). See EmbedPdfConfig.
388
- * Init-only: the kernel is built once from the first values; recreate the
389
- * viewer (e.g. with @if) to swap it. */
390
- engine = input.required(...(ngDevMode ? [{ debugName: "engine" }] : []));
391
- plugins = input.required(...(ngDevMode ? [{ debugName: "plugins" }] : []));
392
- initialDocuments = input(...(ngDevMode ? [undefined, { debugName: "initialDocuments" }] : []));
393
- /** The host this viewer provides — exposed so a template reference can read
394
- * `viewer.host.ready()` / `.status()` without extra imports. */
395
- host = inject(EpdfKernelHost, { self: true });
396
- constructor() {
397
- // Deferred config: inputs are READ on first kernel access, which happens
398
- // after construction (a template binding or effect) — never here.
399
- this.host.connect(() => ({
400
- engine: this.engine(),
401
- plugins: this.plugins(),
402
- initialDocuments: this.initialDocuments(),
403
- }));
404
- }
405
- ngOnInit() {
406
- // Inputs are set by now; materializes the kernel and starts it (no-op on
407
- // the server — the SSR contract lives on the host).
408
- this.host.boot();
409
- }
410
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
411
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.2.4", type: EpdfViewer, isStandalone: true, selector: "epdf-viewer", inputs: { engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: true, transformFunction: null }, initialDocuments: { classPropertyName: "initialDocuments", publicName: "initialDocuments", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents;" }, providers: [EpdfKernelHost], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
412
- }
413
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, decorators: [{
414
- type: Component,
415
- args: [{
416
- selector: 'epdf-viewer',
417
- standalone: true,
418
- changeDetection: ChangeDetectionStrategy.OnPush,
419
- providers: [EpdfKernelHost],
420
- // Layout-transparent, like React's element-less <Viewer>.
421
- host: { style: 'display: contents;' },
422
- template: `<ng-content />`,
423
- }]
424
- }], ctorParameters: () => [] });
425
-
426
- /**
427
- * PageContext — the seam. A layer depends ONLY on this, never on the Stage, so
428
- * the same layer works inside a virtualized `<epdf-stage>` page and a future
429
- * standalone `<epdf-page-view>`.
430
- *
431
- * THE STABILITY INVARIANT (the one performance rule of this adapter): a page's
432
- * context is ONE STABLE OBJECT per mounted surface — identity never changes;
433
- * the volatile parts (`transform`, `frame`, `pageIndex`) are signals INSIDE it.
434
- * The per-page injector providing `EPDF_PAGE` is likewise created once. Camera
435
- * frames then flow as signal writes — `NgTemplateOutlet` never recreates the
436
- * embedded views, in-flight renders survive, and updates stay per-binding.
437
- * Never rebuild the context or the injector per frame.
965
+ * One context per page on screen, for as long as the page is shown: its identity never
966
+ * changes, and what moves (`pageIndex`, `frame`, `transform`) are signals inside it. The
967
+ * template outlet then keeps every layer while the camera moves, and a camera frame updates
968
+ * only the bindings that read the transform. Never build a new context per frame.
438
969
  *
439
- * Coordinate math lives in `@embedpdf/core-geometry`'s `PageTransform` — verified
440
- * once, not re-derived per framework adapter (`toContentPoint` mirrors React's
441
- * `makePageContext` exactly).
970
+ * The coordinate math is `PageTransform` from `@embedpdf/core-geometry`, and the client-space
971
+ * conversions are `pageClientSpace` from `@embedpdf/web`, both shared by every framework.
442
972
  */
443
973
  const EPDF_PAGE = new InjectionToken('EPDF_PAGE');
444
- function injectPage() {
974
+ /** The page this component is drawn on; EPDF-103, naming `what`, outside a page. */
975
+ function injectPage(what = 'This component') {
445
976
  const page = inject(EPDF_PAGE, { optional: true });
446
- if (!page) {
447
- throw new Error('[embedpdf] injectPage() must be used inside an <epdf-stage> page template');
448
- }
977
+ if (!page)
978
+ throw outsidePageError(what);
449
979
  return page;
450
980
  }
451
- /** Build a page context from its reactive parts. `documentId`/`pon` are thunks
452
- * because inputs aren't readable at construction — both are stable per surface. */
981
+ /**
982
+ * A page context from its parts. `documentId` and `ref` are functions because a component's
983
+ * inputs aren't set yet when it's constructed; both stay the same for the page's lifetime.
984
+ */
453
985
  function createPageContext(parts) {
454
986
  const { transform, getRect } = parts;
455
987
  return {
456
988
  get documentId() {
457
989
  return parts.documentId();
458
990
  },
459
- get pon() {
460
- return parts.pon();
991
+ get ref() {
992
+ return parts.ref();
993
+ },
994
+ get view() {
995
+ return parts.view();
461
996
  },
462
997
  pageIndex: parts.pageIndex,
463
998
  frame: parts.frame,
464
999
  transform,
465
- toContentPoint: (clientX, clientY) => {
466
- // Client → box-local view px, then invert rotation + scale via the
467
- // transform (verified once in geometry, not re-derived per adapter).
468
- const r = getRect();
469
- return transform().viewToContent({ x: clientX - r.left, y: clientY - r.top });
470
- },
471
- toClientPoint: (p) => {
472
- // Exact inverse of `toContentPoint`, offset by the same live display-box
473
- // origin — the two can never drift.
474
- const r = getRect();
475
- const v = transform().contentToView(p);
476
- return { x: r.left + v.x, y: r.top + v.y };
477
- },
478
- toClientRect: (rect) => {
479
- const r = getRect();
480
- const v = transform().contentToViewRect(rect);
481
- return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };
482
- },
1000
+ ...(parts.getViewDemand ? { getViewDemand: parts.getViewDemand } : {}),
1001
+ // Client pixels to pixels in the page's box, then the transform undoes the turn and the scale,
1002
+ // against the transform the signal holds when a conversion runs.
1003
+ ...pageClientSpace(transform, getRect),
483
1004
  };
484
1005
  }
485
1006
 
486
1007
  /**
487
- * @embedpdf/angular/runtime — the trunk: the generic kernel binding.
1008
+ * The Angular side of `@embedpdf/web`'s page layers: a layer says, for as long as it's there,
1009
+ * which part of its page it paints, and the render layer reads which parts are painted, to leave
1010
+ * them out of the page's picture.
1011
+ */
1012
+ /**
1013
+ * Say, for as long as this component is there, that it paints `part` of its page (`pageRef()`,
1014
+ * the page context's `ref`; `null` while it paints nothing). Call it in an injection context.
1015
+ */
1016
+ function paintsPagePart(pageRef, part) {
1017
+ effect((onCleanup) => {
1018
+ const ref = pageRef();
1019
+ if (ref)
1020
+ onCleanup(untracked(() => pageLayersOf(ref).paint(part)));
1021
+ });
1022
+ }
1023
+ /**
1024
+ * Which parts of the page (`pageRef()`, the page context's `ref`) the layers on it paint: `null`
1025
+ * until the first render is done, when every layer made with this one has said what it paints,
1026
+ * so the page's picture is never asked for with a part another layer is about to take. Call it
1027
+ * in an injection context.
1028
+ */
1029
+ function injectPaintedParts(pageRef) {
1030
+ const painted = signal(null, ...(ngDevMode ? [{ debugName: "painted" }] : []));
1031
+ const injector = inject(Injector);
1032
+ afterNextRender(() => {
1033
+ effect((onCleanup) => {
1034
+ const layers = pageLayersOf(pageRef());
1035
+ untracked(() => painted.set(layers.painted()));
1036
+ onCleanup(layers.subscribe(() => painted.set(layers.painted())));
1037
+ }, { injector });
1038
+ });
1039
+ return painted.asReadonly();
1040
+ }
1041
+
1042
+ /**
1043
+ * Scene shapes as SVG elements: `@embedpdf/web`'s element descriptions (`svgShapesOf`), one
1044
+ * element each, a clipping `svg` with its children. The annotation and form layers draw their
1045
+ * scenes through `<svg:g [epdfSvgShapes]>` inside an `<svg>`.
1046
+ */
1047
+ /**
1048
+ * An SVG element's attributes from a record of SVG names (`stroke-width`), as `@embedpdf/web`
1049
+ * describes a scene node (templates can't spread). Set again when the record changes; an
1050
+ * attribute that left the record is removed.
1051
+ */
1052
+ class EpdfSvgAttributes {
1053
+ attributes = input.required(...(ngDevMode ? [{ debugName: "attributes", alias: 'epdfSvgAttributes' }] : [{
1054
+ alias: 'epdfSvgAttributes',
1055
+ }]));
1056
+ constructor() {
1057
+ const element = inject(ElementRef).nativeElement;
1058
+ let shown = {};
1059
+ effect(() => {
1060
+ const next = this.attributes();
1061
+ for (const name of Object.keys(shown)) {
1062
+ if (!(name in next))
1063
+ element.removeAttribute(name);
1064
+ }
1065
+ for (const [name, value] of Object.entries(next)) {
1066
+ if (shown[name] !== value)
1067
+ element.setAttribute(name, String(value));
1068
+ }
1069
+ shown = next;
1070
+ });
1071
+ }
1072
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1073
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfSvgAttributes, isStandalone: true, selector: "[epdfSvgAttributes]", inputs: { attributes: { classPropertyName: "attributes", publicName: "epdfSvgAttributes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1074
+ }
1075
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, decorators: [{
1076
+ type: Directive,
1077
+ args: [{ selector: '[epdfSvgAttributes]' }]
1078
+ }], ctorParameters: () => [] });
1079
+ /** The shapes, in drawing order, in the `<svg:g>` it's on, and the faces their text draws in. */
1080
+ class EpdfSvgShapes {
1081
+ shapes = input.required(...(ngDevMode ? [{ debugName: "shapes", alias: 'epdfSvgShapes' }] : [{ alias: 'epdfSvgShapes' }]));
1082
+ constructor() {
1083
+ if (!isPlatformBrowser(inject(PLATFORM_ID)))
1084
+ return;
1085
+ // The faces of this level's text, while it shows: a clipping `svg` mounts its children's.
1086
+ const mounts = createFontMounts();
1087
+ inject(DestroyRef).onDestroy(() => mounts.release());
1088
+ effect(() => mounts.mount(this.shapes().flatMap((shape) => (shape.program ? [shape.program] : []))));
1089
+ }
1090
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, deps: [], target: i0.ɵɵFactoryTarget.Component });
1091
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfSvgShapes, isStandalone: true, selector: "g[epdfSvgShapes]", inputs: { shapes: { classPropertyName: "shapes", publicName: "epdfSvgShapes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
1092
+ @for (shape of shapes(); track $index) {
1093
+ @switch (shape.tag) {
1094
+ @case ('rect') {
1095
+ <svg:rect
1096
+ [epdfSvgAttributes]="shape.attributes"
1097
+ [style.mix-blend-mode]="shape.blend ?? null"
1098
+ ></svg:rect>
1099
+ }
1100
+ @case ('ellipse') {
1101
+ <svg:ellipse
1102
+ [epdfSvgAttributes]="shape.attributes"
1103
+ [style.mix-blend-mode]="shape.blend ?? null"
1104
+ ></svg:ellipse>
1105
+ }
1106
+ @case ('line') {
1107
+ <svg:line
1108
+ [epdfSvgAttributes]="shape.attributes"
1109
+ [style.mix-blend-mode]="shape.blend ?? null"
1110
+ ></svg:line>
1111
+ }
1112
+ @case ('path') {
1113
+ <svg:path
1114
+ [epdfSvgAttributes]="shape.attributes"
1115
+ [style.mix-blend-mode]="shape.blend ?? null"
1116
+ ></svg:path>
1117
+ }
1118
+ @case ('polygon') {
1119
+ <svg:polygon
1120
+ [epdfSvgAttributes]="shape.attributes"
1121
+ [style.mix-blend-mode]="shape.blend ?? null"
1122
+ ></svg:polygon>
1123
+ }
1124
+ @case ('polyline') {
1125
+ <svg:polyline
1126
+ [epdfSvgAttributes]="shape.attributes"
1127
+ [style.mix-blend-mode]="shape.blend ?? null"
1128
+ ></svg:polyline>
1129
+ }
1130
+ @case ('text') {
1131
+ <svg:text
1132
+ [epdfSvgAttributes]="shape.attributes"
1133
+ [style.mix-blend-mode]="shape.blend ?? null"
1134
+ [textContent]="shape.text ?? ''"
1135
+ ></svg:text>
1136
+ }
1137
+ @case ('svg') {
1138
+ <svg:svg [epdfSvgAttributes]="shape.attributes">
1139
+ <svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
1140
+ </svg:svg>
1141
+ }
1142
+ }
1143
+ }
1144
+ `, isInline: true, dependencies: [{ kind: "component", type: EpdfSvgShapes, selector: "g[epdfSvgShapes]", inputs: ["epdfSvgShapes"] }, { kind: "directive", type: EpdfSvgAttributes, selector: "[epdfSvgAttributes]", inputs: ["epdfSvgAttributes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1145
+ }
1146
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, decorators: [{
1147
+ type: Component,
1148
+ args: [{
1149
+ selector: 'g[epdfSvgShapes]',
1150
+ imports: [EpdfSvgAttributes],
1151
+ changeDetection: ChangeDetectionStrategy.OnPush,
1152
+ template: `
1153
+ @for (shape of shapes(); track $index) {
1154
+ @switch (shape.tag) {
1155
+ @case ('rect') {
1156
+ <svg:rect
1157
+ [epdfSvgAttributes]="shape.attributes"
1158
+ [style.mix-blend-mode]="shape.blend ?? null"
1159
+ ></svg:rect>
1160
+ }
1161
+ @case ('ellipse') {
1162
+ <svg:ellipse
1163
+ [epdfSvgAttributes]="shape.attributes"
1164
+ [style.mix-blend-mode]="shape.blend ?? null"
1165
+ ></svg:ellipse>
1166
+ }
1167
+ @case ('line') {
1168
+ <svg:line
1169
+ [epdfSvgAttributes]="shape.attributes"
1170
+ [style.mix-blend-mode]="shape.blend ?? null"
1171
+ ></svg:line>
1172
+ }
1173
+ @case ('path') {
1174
+ <svg:path
1175
+ [epdfSvgAttributes]="shape.attributes"
1176
+ [style.mix-blend-mode]="shape.blend ?? null"
1177
+ ></svg:path>
1178
+ }
1179
+ @case ('polygon') {
1180
+ <svg:polygon
1181
+ [epdfSvgAttributes]="shape.attributes"
1182
+ [style.mix-blend-mode]="shape.blend ?? null"
1183
+ ></svg:polygon>
1184
+ }
1185
+ @case ('polyline') {
1186
+ <svg:polyline
1187
+ [epdfSvgAttributes]="shape.attributes"
1188
+ [style.mix-blend-mode]="shape.blend ?? null"
1189
+ ></svg:polyline>
1190
+ }
1191
+ @case ('text') {
1192
+ <svg:text
1193
+ [epdfSvgAttributes]="shape.attributes"
1194
+ [style.mix-blend-mode]="shape.blend ?? null"
1195
+ [textContent]="shape.text ?? ''"
1196
+ ></svg:text>
1197
+ }
1198
+ @case ('svg') {
1199
+ <svg:svg [epdfSvgAttributes]="shape.attributes">
1200
+ <svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
1201
+ </svg:svg>
1202
+ }
1203
+ }
1204
+ }
1205
+ `,
1206
+ }]
1207
+ }], ctorParameters: () => [] });
1208
+
1209
+ /**
1210
+ * Development-time guardrails. Each warning fires once (by key), says what is wrong and what to
1211
+ * do instead, and stays quiet in production builds (Angular's `isDevMode()`).
1212
+ */
1213
+ const warned = new Set();
1214
+ /** Warn once per key. Quiet in production. */
1215
+ function devWarn(key, message) {
1216
+ if (!isDevMode() || warned.has(key))
1217
+ return;
1218
+ warned.add(key);
1219
+ console.warn(`[embedpdf] ${message}`);
1220
+ }
1221
+ /** For tests: forget every warning, so one can fire again. */
1222
+ function resetDevWarnings() {
1223
+ warned.clear();
1224
+ }
1225
+
1226
+ /**
1227
+ * `epdfTheme()`: a theme written with setting names, as the CSS variables for the element
1228
+ * around your viewer. The variables and their lookup are framework-free (`@embedpdf/web`); this
1229
+ * types the result for Angular's `[style]` binding, which sets custom properties as they are.
1230
+ */
1231
+ /**
1232
+ * The CSS variables of a theme: `<div [style]="theme()">` with
1233
+ * `theme = computed(() => epdfTheme({ accent: this.accent() }))`. Everything inside follows
1234
+ * them, and a CSS variable set in a stylesheet still wins over a setting.
1235
+ */
1236
+ function epdfTheme(theme) {
1237
+ return epdfThemeVariables(theme);
1238
+ }
1239
+
1240
+ /**
1241
+ * @embedpdf/angular/runtime: the viewer and what every feature builds on.
488
1242
  *
489
- * Binds the kernel's one change stream to Angular signals (one tick signal +
490
- * computed), resolves capabilities (document-scoped ones against the active or
491
- * `[epdfDocumentScope]`-given document), and provides the page context seam.
492
- * Every feature entry point and layer rides on this — there is no per-plugin
493
- * framework code.
1243
+ * provideEmbedPdf(config, ...features) the viewer, as providers
1244
+ * inject(EpdfViewer) its settings and status
1245
+ * inject(EpdfDocuments), EpdfDocument the documents, as methods, signals and streams
1246
+ * [epdfDocumentScope], *epdfDocumentGate which document a part of the template talks to
1247
+ * pluginService(), EpdfPluginService how a plugin's service is built
1248
+ * EPDF_PAGE, injectPage() the page a layer draws on
1249
+ * paintsPagePart(), injectPaintedParts() which parts of a page its layers paint
1250
+ * <svg:g [epdfSvgShapes]> a scene's shapes as SVG elements
494
1251
  */
495
- // One-line-per-feature: registration travels with the UI.
1252
+ // The core's types and helpers, so app code has one import for the viewer.
496
1253
 
497
1254
  /**
498
1255
  * Generated bundle index. Do not edit.
499
1256
  */
500
1257
 
501
- export { EPDF_DOCUMENT_SCOPE, EPDF_PAGE, EpdfDocumentGate, EpdfDocumentScope, EpdfKernelHost, EpdfViewer, createPageContext, injectActiveDocumentId, injectCapability, injectCapabilityFor, injectDocumentId, injectDocuments, injectKernel, injectKernelHost, injectKernelValue, injectOptionalCapability, injectOptionalCapabilityFor, injectOptionalSelector, injectOptionalSelectorFor, injectPage, injectSelector, injectSelectorFor, provideEmbedPdf, shallowArray };
1258
+ export { CapabilityBinding, EPDF_DOCUMENT_SCOPE, EPDF_PAGE, EPDF_SCOPED_SERVICES, EPDF_SETUP, EpdfDocument, EpdfDocumentGate, EpdfDocumentScope, EpdfDocuments, EpdfError, EpdfKernelHost, EpdfPluginService, EpdfSvgAttributes, EpdfSvgShapes, EpdfViewer, createPageContext, devWarn, epdfTheme, injectCapability, injectDocumentScope, injectKernelHost, injectPage, injectPaintedParts, noPluginError, noViewerError, onServerError, outsidePageError, outsideStageError, paintsPagePart, pluginService, provideEmbedPdf, resetDevWarnings };
502
1259
  //# sourceMappingURL=embedpdf-angular-runtime.mjs.map