@embedpdf/angular 3.0.0-next.14 → 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.
Files changed (87) 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 +602 -0
  6. package/dist/attachment/index.d.ts +29 -0
  7. package/dist/commands/index.d.ts +109 -0
  8. package/dist/fesm2022/embedpdf-angular-actions.mjs +117 -36
  9. package/dist/fesm2022/embedpdf-angular-actions.mjs.map +1 -1
  10. package/dist/fesm2022/embedpdf-angular-anchored.mjs +148 -0
  11. package/dist/fesm2022/embedpdf-angular-anchored.mjs.map +1 -0
  12. package/dist/fesm2022/embedpdf-angular-annotation.mjs +1933 -0
  13. package/dist/fesm2022/embedpdf-angular-annotation.mjs.map +1 -0
  14. package/dist/fesm2022/embedpdf-angular-attachment.mjs +67 -0
  15. package/dist/fesm2022/embedpdf-angular-attachment.mjs.map +1 -0
  16. package/dist/fesm2022/embedpdf-angular-commands.mjs +235 -0
  17. package/dist/fesm2022/embedpdf-angular-commands.mjs.map +1 -0
  18. package/dist/fesm2022/embedpdf-angular-form.mjs +1074 -0
  19. package/dist/fesm2022/embedpdf-angular-form.mjs.map +1 -0
  20. package/dist/fesm2022/embedpdf-angular-history.mjs +54 -0
  21. package/dist/fesm2022/embedpdf-angular-history.mjs.map +1 -0
  22. package/dist/fesm2022/embedpdf-angular-i18n.mjs +100 -0
  23. package/dist/fesm2022/embedpdf-angular-i18n.mjs.map +1 -0
  24. package/dist/fesm2022/embedpdf-angular-interaction.mjs +157 -0
  25. package/dist/fesm2022/embedpdf-angular-interaction.mjs.map +1 -0
  26. package/dist/fesm2022/embedpdf-angular-link.mjs +306 -0
  27. package/dist/fesm2022/embedpdf-angular-link.mjs.map +1 -0
  28. package/dist/fesm2022/embedpdf-angular-measurement.mjs +106 -0
  29. package/dist/fesm2022/embedpdf-angular-measurement.mjs.map +1 -0
  30. package/dist/fesm2022/embedpdf-angular-metadata.mjs +77 -0
  31. package/dist/fesm2022/embedpdf-angular-metadata.mjs.map +1 -0
  32. package/dist/fesm2022/embedpdf-angular-outline.mjs +76 -0
  33. package/dist/fesm2022/embedpdf-angular-outline.mjs.map +1 -0
  34. package/dist/fesm2022/embedpdf-angular-page-edit.mjs +68 -0
  35. package/dist/fesm2022/embedpdf-angular-page-edit.mjs.map +1 -0
  36. package/dist/fesm2022/embedpdf-angular-page-view.mjs +297 -0
  37. package/dist/fesm2022/embedpdf-angular-page-view.mjs.map +1 -0
  38. package/dist/fesm2022/embedpdf-angular-redaction.mjs +93 -0
  39. package/dist/fesm2022/embedpdf-angular-redaction.mjs.map +1 -0
  40. package/dist/fesm2022/embedpdf-angular-render.mjs +331 -69
  41. package/dist/fesm2022/embedpdf-angular-render.mjs.map +1 -1
  42. package/dist/fesm2022/embedpdf-angular-runtime.mjs +1151 -390
  43. package/dist/fesm2022/embedpdf-angular-runtime.mjs.map +1 -1
  44. package/dist/fesm2022/embedpdf-angular-search.mjs +251 -0
  45. package/dist/fesm2022/embedpdf-angular-search.mjs.map +1 -0
  46. package/dist/fesm2022/embedpdf-angular-selection.mjs +477 -0
  47. package/dist/fesm2022/embedpdf-angular-selection.mjs.map +1 -0
  48. package/dist/fesm2022/embedpdf-angular-shell.mjs +177 -0
  49. package/dist/fesm2022/embedpdf-angular-shell.mjs.map +1 -0
  50. package/dist/fesm2022/embedpdf-angular-signature.mjs +120 -0
  51. package/dist/fesm2022/embedpdf-angular-signature.mjs.map +1 -0
  52. package/dist/fesm2022/embedpdf-angular-stage.mjs +688 -288
  53. package/dist/fesm2022/embedpdf-angular-stage.mjs.map +1 -1
  54. package/dist/fesm2022/embedpdf-angular-stamp.mjs +169 -0
  55. package/dist/fesm2022/embedpdf-angular-stamp.mjs.map +1 -0
  56. package/dist/fesm2022/embedpdf-angular-testing.mjs +89 -0
  57. package/dist/fesm2022/embedpdf-angular-testing.mjs.map +1 -0
  58. package/dist/fesm2022/embedpdf-angular-toolbar.mjs +1123 -0
  59. package/dist/fesm2022/embedpdf-angular-toolbar.mjs.map +1 -0
  60. package/dist/fesm2022/embedpdf-angular-view-manager.mjs +69 -0
  61. package/dist/fesm2022/embedpdf-angular-view-manager.mjs.map +1 -0
  62. package/dist/fesm2022/embedpdf-angular.mjs +25 -3
  63. package/dist/fesm2022/embedpdf-angular.mjs.map +1 -1
  64. package/dist/form/index.d.ts +79 -0
  65. package/dist/history/index.d.ts +28 -0
  66. package/dist/i18n/index.d.ts +52 -0
  67. package/dist/index.d.ts +22 -0
  68. package/dist/interaction/index.d.ts +58 -0
  69. package/dist/link/index.d.ts +77 -0
  70. package/dist/measurement/index.d.ts +46 -0
  71. package/dist/metadata/index.d.ts +51 -0
  72. package/dist/outline/index.d.ts +32 -0
  73. package/dist/page-edit/index.d.ts +27 -0
  74. package/dist/page-view/index.d.ts +94 -0
  75. package/dist/redaction/index.d.ts +38 -0
  76. package/dist/render/index.d.ts +45 -6
  77. package/dist/runtime/index.d.ts +579 -199
  78. package/dist/search/index.d.ts +71 -0
  79. package/dist/selection/index.d.ts +141 -0
  80. package/dist/shell/index.d.ts +88 -0
  81. package/dist/signature/index.d.ts +45 -0
  82. package/dist/stage/index.d.ts +242 -141
  83. package/dist/stamp/index.d.ts +58 -0
  84. package/dist/testing/index.d.ts +53 -0
  85. package/dist/toolbar/index.d.ts +257 -0
  86. package/dist/view-manager/index.d.ts +23 -0
  87. package/package.json +145 -14
@@ -1,150 +1,421 @@
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 { browserPrinter, 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
+ printer: browserPrinter(),
357
+ });
358
+ }
359
+ catch (error) {
360
+ if (owned)
361
+ void engine.destroy();
362
+ this.fail(error);
363
+ return;
364
+ }
365
+ this.engine = engine;
366
+ this.ownsEngine = owned;
367
+ this.viewerSettingsBeforeStart.applyTo(kernel);
368
+ for (const [plugin, before] of this.pluginSettingsBeforeStart) {
369
+ before.applyTo(kernel.settingsOf(plugin.token));
370
+ }
371
+ this.stopListening = kernel.subscribe(() => this.changed());
372
+ this.kernelState.set(kernel);
373
+ this.changed();
374
+ kernel.start().then(() => {
375
+ if (this.destroyed)
376
+ return;
377
+ this.statusState.set('ready');
378
+ // Every tab appears at once, in order; a document that fails shows it in its own tab.
379
+ kernel.documents.openAll(config.initialDocuments ?? []);
380
+ }, (error) => {
381
+ if (!this.destroyed)
382
+ this.fail(error);
383
+ });
384
+ }
385
+ changed() {
386
+ this.revisionState.update((revision) => revision + 1);
387
+ for (const listener of [...this.listeners])
388
+ listener();
389
+ }
390
+ fail(error) {
391
+ this.errorState.set(error);
392
+ this.statusState.set('error');
393
+ console.error('[embedpdf] the viewer failed to start', error);
394
+ }
395
+ runFeatureSetups() {
396
+ if (this.destroyed)
397
+ return;
398
+ for (const feature of this.setup.features) {
399
+ if (feature.setup)
400
+ runInInjectionContext(this.injector, feature.setup);
401
+ }
402
+ }
403
+ destroy() {
133
404
  if (this.destroyed)
134
405
  return;
135
406
  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)
407
+ this.stopListening?.();
408
+ this.listeners.clear();
409
+ // A kernel given is its maker's to destroy.
410
+ if (this.given)
411
+ return;
412
+ const kernel = this.kernelState();
413
+ const engine = this.engine;
414
+ const owned = this.ownsEngine;
415
+ // The kernel first, which closes every document's engine handle, then the engine it ran on.
416
+ if (kernel) {
417
+ void kernel.destroy().then(() => {
418
+ if (owned && engine)
148
419
  void engine.destroy();
149
420
  });
150
421
  }
@@ -154,349 +425,839 @@ class EpdfKernelHost {
154
425
  }
155
426
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfKernelHost, decorators: [{
156
427
  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');
428
+ }], ctorParameters: () => [] });
183
429
 
184
430
  /**
185
- * The inject* primitives — Angular's spelling of the React runtime hooks
186
- * (`use*` → `inject*`, plain values → signals, same names otherwise).
431
+ * How the adapter reaches a plugin: its capability token, resolved for one document (the one in
432
+ * scope, else the active one) after every change of the kernel. A `CapabilityBinding` turns that
433
+ * into what Angular code wants: signals for values, late-bound methods for calls, and RxJS
434
+ * streams for events. Services, the Stage and the page layers are all built on it.
187
435
  *
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.
436
+ * Resolution is a read, not a cache: a document becomes resolvable when it's ready, while its
437
+ * id stays the same, so a capability looked up once by id would go stale.
192
438
  */
193
- const shallowArray = (a, b) => a === b || (a.length === b.length && a.every((x, i) => x === b[i]));
194
- function injectKernelHost() {
439
+ /** The viewer above this injection context; EPDF-101, naming `what`, when there is none. */
440
+ function injectKernelHost(what) {
195
441
  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
- }
442
+ if (!host)
443
+ throw noViewerError(what);
199
444
  return host;
200
445
  }
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);
446
+ const FOLLOW_ACTIVE = signal(null).asReadonly();
447
+ /**
448
+ * The document this injection context talks to: the id the nearest `[epdfDocumentScope]` (or a
449
+ * Stage with a `[document]`) gives, or null to follow the active document.
450
+ */
451
+ function injectDocumentScope() {
452
+ return inject(EPDF_DOCUMENT_SCOPE, { optional: true })?.id ?? FOLLOW_ACTIVE;
210
453
  }
211
- function injectActiveDocumentId() {
212
- return injectKernelValue((k) => k.documents.activeId());
454
+ /**
455
+ * A plugin's capability for this injection context's document, as a signal: null while it
456
+ * can't resolve (no such plugin, no document, or one that isn't ready yet). For plugin authors;
457
+ * apps use a plugin's service (`inject(EpdfSearch)`). Call it in an injection context.
458
+ */
459
+ function injectCapability(token) {
460
+ const binding = new CapabilityBinding(injectKernelHost(`injectCapability(${token.name})`), () => token, injectDocumentScope());
461
+ return binding.capability;
213
462
  }
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());
463
+ /** One capability token, for one document, as signals, calls and streams. */
464
+ class CapabilityBinding {
465
+ host;
466
+ token;
467
+ documentId;
468
+ /** The capability, or null while it can't resolve. */
469
+ capability;
470
+ /**
471
+ * @param token the token to resolve; a function, because a component's token is an input
472
+ * @param documentId the document to resolve it for, or null for the active one
473
+ */
474
+ constructor(host, token, documentId) {
475
+ this.host = host;
476
+ this.token = token;
477
+ this.documentId = documentId;
478
+ this.capability = host.read((kernel) => this.resolve(kernel), () => null);
479
+ }
480
+ /**
481
+ * A value read through the capability, as a signal that changes only when `equal` says the
482
+ * value did. `empty` while the capability can't resolve, and for the one read that can land
483
+ * between a document closing and its capability going away.
484
+ */
485
+ select(read, empty, equal = shallowEqual) {
486
+ return this.host.read((kernel) => {
487
+ const capability = this.resolve(kernel);
488
+ if (!capability)
489
+ return empty;
490
+ try {
491
+ return read(capability);
492
+ }
493
+ catch {
494
+ return empty;
495
+ }
496
+ }, () => empty, equal);
497
+ }
498
+ /**
499
+ * The capability, for `call`, which needs it now. Without a document a document plugin
500
+ * refuses with `not-ready` ("no document is open"), the same refusal every framework's
501
+ * stand-in gives (`standInFor` in `@embedpdf/core`); a token no plugin provides throws the
502
+ * kernel's reason, because that is a setup mistake.
503
+ */
504
+ require(call) {
505
+ const token = this.token();
506
+ const kernel = this.host.requireKernel(token.name, call);
507
+ const documentId = untracked(this.documentId) ?? undefined;
508
+ const capability = kernel.tryCapability(token, documentId);
509
+ if (capability)
510
+ return capability;
511
+ if (kernel.scopeOf(token) === 'document') {
512
+ throw new PluginError('not-ready', token.name, 'no document is open');
513
+ }
514
+ return kernel.capability(token, documentId);
515
+ }
516
+ /**
517
+ * The capability's method `name`, looked up when it's called, so a method kept in a field
518
+ * acts on the document current at that moment. The same function for the binding's lifetime.
519
+ *
520
+ * A read (a method named `get*`, `list*`, `is*`, `has*` or `can*`: `canUpdate()`,
521
+ * `getZoomLevel()`) is state too: called in a template, a `computed()` or an `effect()`, it's
522
+ * read again after each change of the kernel and of the document in scope, and wakes its
523
+ * reader only when its answer changes. So a button it disables follows the selection, the
524
+ * active document and its permissions. Other methods track nothing: an `effect()` that calls a
525
+ * verb doesn't run again because the verb changed the document.
526
+ *
527
+ * Without a document a method refuses with `not-ready`: one that returns a promise (the token's
528
+ * `promises`) returns the refusal as a rejected promise, so `.catch()` sees it; others throw.
529
+ */
530
+ method(name) {
531
+ return this.late(String(name), (capability) => capability, name);
532
+ }
533
+ /**
534
+ * The method `name` of the capability's namespace `namespace` (`custom.update()`), late-bound
535
+ * the way {@link method} is. A service gives a namespace an object of its own
536
+ * (`metadata.custom`), built from these, {@link select} and {@link stream}.
537
+ */
538
+ namespaceMethod(namespace, name) {
539
+ return this.late(`${String(namespace)}.${String(name)}`, (capability) => capability[namespace], name);
540
+ }
541
+ /**
542
+ * One of the capability's events, as an RxJS stream. A subscription follows the document:
543
+ * when it changes (another tab, a document that opens or closes), it moves to the new
544
+ * document's capability. Nothing is emitted while there is none.
545
+ */
546
+ stream(pick) {
547
+ return new Observable((subscriber) => {
548
+ let current = null;
549
+ let listening = null;
550
+ const follow = () => {
551
+ const kernel = untracked(this.host.kernel);
552
+ const next = kernel ? untracked(() => this.resolve(kernel)) : null;
553
+ if (next === current)
554
+ return;
555
+ listening?.abort();
556
+ listening = null;
557
+ current = next;
558
+ if (!next)
559
+ return;
560
+ listening = new AbortController();
561
+ pick(next)((event) => subscriber.next(event), { signal: listening.signal });
562
+ };
563
+ const stop = this.host.subscribe(follow);
564
+ follow();
565
+ return () => {
566
+ stop();
567
+ listening?.abort();
568
+ };
569
+ });
570
+ }
571
+ resolve(kernel) {
572
+ return kernel.tryCapability(this.token(), this.documentId() ?? undefined);
573
+ }
574
+ /** `owner(capability)[name]`, looked up when called; a read tracks what its answer reads. */
575
+ late(label, owner, name) {
576
+ const call = `${label}()`;
577
+ const run = (args) => {
578
+ let target;
579
+ try {
580
+ target = owner(this.require(call));
581
+ }
582
+ catch (error) {
583
+ // Not ready: a method that returns a promise rejects, as the capability would.
584
+ if (isPluginError(error, 'not-ready') && returnsPromise(this.token(), label)) {
585
+ return Promise.reject(error);
586
+ }
587
+ throw error;
588
+ }
589
+ return target[name].apply(target, args);
590
+ };
591
+ if (!isReadMember(String(name))) {
592
+ return ((...args) => untracked(() => run(args)));
593
+ }
594
+ // A computed of its own for each call: whoever reads the check depends on its answer, not
595
+ // on every change of the kernel. Outside a reactive context it's one plain call.
596
+ return ((...args) => computed(() => {
597
+ this.documentId();
598
+ this.host.revision();
599
+ return run(args);
600
+ })());
601
+ }
220
602
  }
603
+
221
604
  /**
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);
605
+ * One service per plugin: `inject(EpdfSearch)` has the plugin's methods, every value of its
606
+ * State table as a signal (`search.hitCount()`), its events as RxJS streams
607
+ * (`search.completed$`), and, when the plugin has settings, `settings()`, `getSettings()`,
608
+ * `updateSettings()`, `resetSettings()` and `settingsChanged$`. `pluginService()` builds all of that from what the
609
+ * plugin declares already, so a plugin's service is a few lines; the package README's "Adding a
610
+ * plugin binding" shows one.
611
+ *
612
+ * A service acts on the document of the injector that created it: under `[epdfDocumentScope]`
613
+ * that document, else the active one. Its methods are looked up when they're called, so a
614
+ * method kept in a field acts on the document current at that moment.
615
+ */
616
+ /**
617
+ * The base every plugin service extends. Its members are for the service's own class, which
618
+ * adds what the generic part can't: a signal with arguments (`hitsOn(page)`), a namespace
619
+ * (`custom.fields()`), a helper that needs the caller's injection context.
620
+ */
621
+ class EpdfPluginService {
622
+ /** The plugin's capability for this service's document. */
623
+ binding;
624
+ constructor(spec) {
625
+ const host = inject(EpdfKernelHost, { optional: true });
626
+ // A plugin's service names its feature, whether the viewer is missing or only the plugin.
627
+ if (spec.feature && !host?.provides(spec.token))
628
+ throw noPluginError(spec.name, spec.feature);
629
+ if (!host)
630
+ throw noViewerError(`inject(${spec.name})`);
631
+ this.binding = new CapabilityBinding(host, () => spec.token, injectDocumentScope());
632
+ }
240
633
  }
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));
634
+ /**
635
+ * The base class of a plugin's service, built from the plugin's declarations:
636
+ *
637
+ * @Injectable({ providedIn: 'root' })
638
+ * export class EpdfSearch extends pluginService({
639
+ * name: 'EpdfSearch',
640
+ * feature: 'withSearch()',
641
+ * token: SearchToken,
642
+ * state: searchState,
643
+ * methods: ['search', 'clear', 'goToHit'],
644
+ * events: ['onCompleted'],
645
+ * }) {}
646
+ *
647
+ * `providedIn: 'root'` is only the fallback that turns a missing viewer into EPDF-102: the
648
+ * service's feature provides it next to the viewer, and `[epdfDocumentScope]` again.
649
+ */
650
+ function pluginService(spec) {
651
+ class PluginService extends EpdfPluginService {
652
+ constructor() {
653
+ super(spec);
654
+ const members = this;
655
+ const binding = this.binding;
656
+ if (spec.state) {
657
+ const state = spec.state;
658
+ // One read of the whole table, compared field by field, then one signal per field: a
659
+ // template that reads `hitCount()` wakes only when the hit count changes.
660
+ const whole = binding.select((capability) => state.read(capability), state.empty);
661
+ for (const field of Object.keys(state.empty)) {
662
+ members[field] = computed(() => whole()[field]);
663
+ }
664
+ }
665
+ const ensure = spec.ensure ?? spec.state?.ensure;
666
+ if (ensure) {
667
+ effect(() => {
668
+ const capability = binding.capability();
669
+ // The state says when the load fails (its status), so the rejection has nothing to add.
670
+ if (capability)
671
+ untracked(() => ensure(capability).catch(() => { }));
672
+ });
673
+ }
674
+ for (const name of spec.methods)
675
+ members[name] = binding.method(name);
676
+ for (const event of spec.events ?? []) {
677
+ const stream = `${event.charAt(2).toLowerCase()}${event.slice(3)}$`;
678
+ members[stream] = binding.stream((capability) => capability[event]);
679
+ }
680
+ // Settings belong to the plugin as the app registered it, not to a document: they work
681
+ // with no document open, and before the kernel exists.
682
+ const host = binding.host;
683
+ if (host.hasSettings(spec.token)) {
684
+ const settings = host.settingsOf(spec.token);
685
+ members['settings'] = settings.current;
686
+ // A read, like every `get*` method: a template or an effect that calls it follows it.
687
+ members['getSettings'] = () => settings.current();
688
+ members['updateSettings'] = settings.update;
689
+ members['resetSettings'] = settings.reset;
690
+ members['settingsChanged$'] = host.stream((kernel) => kernel.settingsOf(spec.token).onSettingsChanged);
691
+ }
692
+ }
693
+ }
694
+ return PluginService;
247
695
  }
248
- function injectOptionalCapability(token) {
249
- return injectOptionalCapabilityFor(() => token);
696
+
697
+ /**
698
+ * The viewer's own services, which `provideEmbedPdf()` always has:
699
+ *
700
+ * inject(EpdfViewer) the viewer's settings (`identity`, `scope`, `accent`, `page`) and status
701
+ * inject(EpdfDocuments) open, close, unlock and download documents; every tab, as signals
702
+ * inject(EpdfDocument) the document this part of the template talks to, as signals
703
+ */
704
+ /**
705
+ * The viewer itself: its settings, and whether it started. The config given to
706
+ * `provideEmbedPdf()` is read once; what may change while the app runs changes here:
707
+ *
708
+ * effect(() => viewer.updateSettings({ identity: auth.identity() }));
709
+ *
710
+ * Documents opened after a change of `identity` or `scope` use the new value; open ones keep
711
+ * theirs.
712
+ */
713
+ class EpdfViewer {
714
+ host = injectKernelHost('inject(EpdfViewer)');
715
+ /** The viewer's settings: `identity`, `scope`, `accent` and `page`. */
716
+ settings = this.host.viewerSettings;
717
+ /** `'starting'`, `'ready'`, or `'error'` when the engine or a plugin failed to start. */
718
+ status = this.host.status;
719
+ /** Why the viewer failed to start, when `status()` is `'error'`. */
720
+ error = this.host.error;
721
+ /** Fires once for each call that changed a setting. */
722
+ settingsChanged$ = this.host.stream((kernel) => kernel.onSettingsChanged);
723
+ /** Merge `changes` into the viewer's settings: `page` merges, `identity` and `scope` replace. */
724
+ updateSettings(changes) {
725
+ this.host.updateViewerSettings(changes);
726
+ }
727
+ /** Go back to the settings `provideEmbedPdf()` was given. */
728
+ resetSettings() {
729
+ this.host.resetViewerSettings();
730
+ }
731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
732
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, providedIn: 'root' });
250
733
  }
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);
734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, decorators: [{
735
+ type: Injectable,
736
+ args: [{ providedIn: 'root' }]
737
+ }] });
738
+ /**
739
+ * The documents: open, close, unlock, retry and download them, and every tab with the active
740
+ * one as signals (`documents()`, `activeId()`). Under `[epdfDocumentScope]` the calls that
741
+ * leave out the document use that one: `download()` downloads the document in scope.
742
+ */
743
+ class EpdfDocuments extends pluginService({
744
+ name: 'EpdfDocuments',
745
+ token: DocumentsToken,
746
+ state: documentsState,
747
+ methods: [
748
+ 'open',
749
+ 'canOpenFiles',
750
+ 'openAll',
751
+ 'retry',
752
+ 'rename',
753
+ 'unlock',
754
+ 'close',
755
+ 'closeAll',
756
+ 'setActive',
757
+ 'getActiveId',
758
+ 'getActive',
759
+ 'list',
760
+ 'get',
761
+ 'has',
762
+ 'getCount',
763
+ 'getOrder',
764
+ 'setOrder',
765
+ 'move',
766
+ 'swap',
767
+ 'download',
768
+ 'downloadLayer',
769
+ 'preparePrint',
770
+ 'print',
771
+ 'getPrintQuality',
772
+ 'canDownload',
773
+ 'canPrint',
774
+ 'listPages',
775
+ 'getPage',
776
+ 'getPageIndex',
777
+ 'getRevision',
778
+ ],
779
+ events: [
780
+ 'onOpened',
781
+ 'onOpenFailed',
782
+ 'onLocked',
783
+ 'onClosed',
784
+ 'onActiveChanged',
785
+ 'onPagesChanged',
786
+ 'onUnsavedChangesChanged',
787
+ ],
788
+ }) {
789
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
790
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, providedIn: 'root' });
256
791
  }
257
- function injectSelector(token, select, equal) {
258
- return injectSelectorFor(() => token, select, equal);
792
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, decorators: [{
793
+ type: Injectable,
794
+ args: [{ providedIn: 'root' }]
795
+ }] });
796
+ const NO_PAGES = Object.freeze([]);
797
+ /**
798
+ * The document this part of the template talks to: the one `[epdfDocumentScope]` names, else
799
+ * the active one. Its fields are signals (`id()`, `name()`, `status()`, `pageCount()`,
800
+ * `hasUnsavedChanges()`, `passwordProvided()`, `error()`), and `pages()` its pages. With no
801
+ * document it reads as one still on its way: an empty `id` and `status() === 'loading'`.
802
+ */
803
+ class EpdfDocument extends pluginService({
804
+ name: 'EpdfDocument',
805
+ token: DocumentsToken,
806
+ state: documentState,
807
+ methods: [],
808
+ }) {
809
+ /**
810
+ * The document's pages, in order: each with its `ref`, `index`, `label`, size and rotation.
811
+ * Needs no Stage. The same array until a page is added, removed, moved or rotated; empty
812
+ * with no document.
813
+ */
814
+ pages = this.binding.select((documents) => documents.listPages(), NO_PAGES, Object.is);
815
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
816
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, providedIn: 'root' });
259
817
  }
818
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, decorators: [{
819
+ type: Injectable,
820
+ args: [{ providedIn: 'root' }]
821
+ }] });
822
+
823
+ const scopedServices = [EpdfDocuments, EpdfDocument];
260
824
  /**
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.
825
+ * Every service that acts on one document: the viewer's own, and each feature's, added when a
826
+ * `provideEmbedPdf()` call names the feature. `[epdfDocumentScope]` and a Stage list this array
827
+ * in their `providers`, so a service injected under them acts on their document.
266
828
  *
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;
829
+ * It is one array that grows, not a copy: Angular reads a directive's providers when its
830
+ * template is first created, which is after the `provideEmbedPdf()` calls in decorators and the
831
+ * app config ran. A service whose feature is first named later (a lazily loaded route's
832
+ * providers) is still provided by its viewer, and acts on the active document there.
833
+ */
834
+ const EPDF_SCOPED_SERVICES = scopedServices;
835
+ /** The viewer, with the plugins its features register; or the viewer of a kernel given. */
836
+ function provideEmbedPdf(config, ...features) {
837
+ for (const feature of features) {
838
+ for (const service of feature.services ?? []) {
839
+ if (!scopedServices.includes(service))
840
+ scopedServices.push(service);
283
841
  }
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
- };
842
+ }
843
+ const setup = { config, features };
844
+ return [
845
+ { provide: EPDF_SETUP, useValue: setup },
846
+ EpdfKernelHost,
847
+ EpdfViewer,
848
+ EpdfDocuments,
849
+ EpdfDocument,
850
+ ...features.flatMap((feature) => [...(feature.services ?? []), ...(feature.providers ?? [])]),
851
+ ];
303
852
  }
304
853
 
305
854
  /**
306
- * Document scoping — hierarchical DI is Angular's native React-context.
855
+ * Which document a part of the template talks to, and whether it has one:
307
856
  *
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.
857
+ * <div [epdfDocumentScope]="id"> everything inside talks to that document
858
+ * <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
859
+ *
860
+ * The gate is a structural directive on purpose: a template really defers creating what's
861
+ * inside, which a conditional `<ng-content>` can't (Angular creates projected content whether
862
+ * or not it's shown). Document UI (the Stage, a page counter) goes behind it; workspace UI (a
863
+ * toolbar frame, an open button) stays outside and renders at once.
864
+ */
865
+ /**
866
+ * Binds everything inside the element to one document: services injected there, the Stage
867
+ * and its layers act on it instead of the active document.
315
868
  */
316
869
  class EpdfDocumentScope {
870
+ /** The document's id. */
317
871
  id = input.required(...(ngDevMode ? [{ debugName: "id", alias: 'epdfDocumentScope' }] : [{ alias: 'epdfDocumentScope' }]));
318
872
  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 });
873
+ 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: [
874
+ { provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
875
+ // A service injected inside gets an instance of its own here, bound to this document.
876
+ EPDF_SCOPED_SERVICES,
877
+ ], ngImport: i0 });
320
878
  }
321
879
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentScope, decorators: [{
322
880
  type: Directive,
323
881
  args: [{
324
882
  selector: '[epdfDocumentScope]',
325
- standalone: true,
326
- providers: [{ provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) }],
883
+ providers: [
884
+ { provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
885
+ // A service injected inside gets an instance of its own here, bound to this document.
886
+ EPDF_SCOPED_SERVICES,
887
+ ],
327
888
  }]
328
889
  }] });
329
890
  /**
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.
891
+ * Renders its content only while this part of the template has a ready document, with that
892
+ * document as `let document`. Otherwise it renders `locked` for a document waiting for its
893
+ * password, `error` for one that couldn't be opened, and `fallback` for everything else (none
894
+ * open yet, still opening), each with the document as `let-document`. Without `locked` or
895
+ * `error` those states show `fallback` too.
333
896
  *
334
- * <section *epdfDocumentGate="; fallback: empty">…document UI…</section>
335
- * <ng-template #empty>Drop a PDF to get started</ng-template>
897
+ * <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
898
+ * <ng-template #opening>Opening…</ng-template>
899
+ * <ng-template #locked let-document><app-password-form [document]="document" /></ng-template>
336
900
  *
337
- * The subtree stays mounted across a CHANGE of document (active-tab switch) —
338
- * only the has-a-document boolean creates/destroys it.
901
+ * The content stays mounted when another ready document becomes the active one; only its
902
+ * `document` changes.
339
903
  */
340
904
  class EpdfDocumentGate {
341
- /** Shown while this subtree has NO document (empty workspace, docs still opening). */
905
+ /** Shown while there is no ready document: none is open, or it's still opening. */
342
906
  fallback = input(null, ...(ngDevMode ? [{ debugName: "fallback", alias: 'epdfDocumentGateFallback' }] : [{
343
907
  alias: 'epdfDocumentGateFallback',
344
908
  }]));
909
+ /** Shown while the document waits for its password; `fallback` without it. */
910
+ locked = input(null, ...(ngDevMode ? [{ debugName: "locked", alias: 'epdfDocumentGateLocked' }] : [{
911
+ alias: 'epdfDocumentGateLocked',
912
+ }]));
913
+ /** Shown when the document couldn't be opened; `fallback` without it. */
914
+ error = input(null, ...(ngDevMode ? [{ debugName: "error", alias: 'epdfDocumentGateError' }] : [{
915
+ alias: 'epdfDocumentGateError',
916
+ }]));
917
+ static ngTemplateContextGuard(_gate, context) {
918
+ return true;
919
+ }
345
920
  constructor() {
346
- const docId = injectDocumentId();
347
- const hasDocument = computed(() => docId() !== null, ...(ngDevMode ? [{ debugName: "hasDocument" }] : []));
348
- const container = inject(ViewContainerRef);
921
+ const document = new CapabilityBinding(injectKernelHost('*epdfDocumentGate'), () => DocumentsToken, injectDocumentScope()).select(documentState.read, documentState.empty);
349
922
  const content = inject(TemplateRef);
923
+ const container = inject(ViewContainerRef);
924
+ const changes = inject(ChangeDetectorRef);
925
+ let shown = null;
350
926
  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
- }
927
+ const current = document();
928
+ const template = this.templateFor(current, content);
929
+ untracked(() => {
930
+ if (shown && shown.template === template) {
931
+ // The same template for another document (a tab switch): keep the view, pass it on.
932
+ shown.view.context.$implicit = current;
933
+ shown.view.markForCheck();
934
+ return;
935
+ }
936
+ container.clear();
937
+ shown = template
938
+ ? { template, view: container.createEmbeddedView(template, { $implicit: current }) }
939
+ : null;
940
+ changes.markForCheck();
941
+ });
362
942
  });
363
943
  }
944
+ templateFor(document, content) {
945
+ if (document.status === 'ready')
946
+ return content;
947
+ const locked = this.locked();
948
+ if (document.status === 'locked' && locked)
949
+ return locked;
950
+ const failed = this.error();
951
+ if (document.status === 'error' && failed)
952
+ return failed;
953
+ return this.fallback() ?? null;
954
+ }
364
955
  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 });
956
+ 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
957
  }
367
958
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentGate, decorators: [{
368
959
  type: Directive,
369
- args: [{ selector: '[epdfDocumentGate]', standalone: true }]
960
+ args: [{ selector: '[epdfDocumentGate]' }]
370
961
  }], ctorParameters: () => [] });
371
962
 
372
963
  /**
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.
964
+ * The page context: what a layer drawn on a page knows about that page. A layer reads only
965
+ * this, never the Stage, so the same layer works on a Stage's page and on a standalone page
966
+ * view. Inside `<ng-template epdfPage>`, `let-page` gives it to the template and `injectPage()`
967
+ * to a component.
380
968
  *
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.
969
+ * One context per page on screen, for as long as the page is shown: its identity never
970
+ * changes, and what moves (`pageIndex`, `frame`, `transform`) are signals inside it. The
971
+ * template outlet then keeps every layer while the camera moves, and a camera frame updates
972
+ * only the bindings that read the transform. Never build a new context per frame.
438
973
  *
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).
974
+ * The coordinate math is `PageTransform` from `@embedpdf/core-geometry`, and the client-space
975
+ * conversions are `pageClientSpace` from `@embedpdf/web`, both shared by every framework.
442
976
  */
443
977
  const EPDF_PAGE = new InjectionToken('EPDF_PAGE');
444
- function injectPage() {
978
+ /** The page this component is drawn on; EPDF-103, naming `what`, outside a page. */
979
+ function injectPage(what = 'This component') {
445
980
  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
- }
981
+ if (!page)
982
+ throw outsidePageError(what);
449
983
  return page;
450
984
  }
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. */
985
+ /**
986
+ * A page context from its parts. `documentId` and `ref` are functions because a component's
987
+ * inputs aren't set yet when it's constructed; both stay the same for the page's lifetime.
988
+ */
453
989
  function createPageContext(parts) {
454
990
  const { transform, getRect } = parts;
455
991
  return {
456
992
  get documentId() {
457
993
  return parts.documentId();
458
994
  },
459
- get pon() {
460
- return parts.pon();
995
+ get ref() {
996
+ return parts.ref();
997
+ },
998
+ get view() {
999
+ return parts.view();
461
1000
  },
462
1001
  pageIndex: parts.pageIndex,
463
1002
  frame: parts.frame,
464
1003
  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
- },
1004
+ ...(parts.getViewDemand ? { getViewDemand: parts.getViewDemand } : {}),
1005
+ // Client pixels to pixels in the page's box, then the transform undoes the turn and the scale,
1006
+ // against the transform the signal holds when a conversion runs.
1007
+ ...pageClientSpace(transform, getRect),
483
1008
  };
484
1009
  }
485
1010
 
486
1011
  /**
487
- * @embedpdf/angular/runtime — the trunk: the generic kernel binding.
1012
+ * The Angular side of `@embedpdf/web`'s page layers: a layer says, for as long as it's there,
1013
+ * which part of its page it paints, and the render layer reads which parts are painted, to leave
1014
+ * them out of the page's picture.
1015
+ */
1016
+ /**
1017
+ * Say, for as long as this component is there, that it paints `part` of its page (`pageRef()`,
1018
+ * the page context's `ref`; `null` while it paints nothing). Call it in an injection context.
1019
+ */
1020
+ function paintsPagePart(pageRef, part) {
1021
+ effect((onCleanup) => {
1022
+ const ref = pageRef();
1023
+ if (ref)
1024
+ onCleanup(untracked(() => pageLayersOf(ref).paint(part)));
1025
+ });
1026
+ }
1027
+ /**
1028
+ * Which parts of the page (`pageRef()`, the page context's `ref`) the layers on it paint: `null`
1029
+ * until the first render is done, when every layer made with this one has said what it paints,
1030
+ * so the page's picture is never asked for with a part another layer is about to take. Call it
1031
+ * in an injection context.
1032
+ */
1033
+ function injectPaintedParts(pageRef) {
1034
+ const painted = signal(null, ...(ngDevMode ? [{ debugName: "painted" }] : []));
1035
+ const injector = inject(Injector);
1036
+ afterNextRender(() => {
1037
+ effect((onCleanup) => {
1038
+ const layers = pageLayersOf(pageRef());
1039
+ untracked(() => painted.set(layers.painted()));
1040
+ onCleanup(layers.subscribe(() => painted.set(layers.painted())));
1041
+ }, { injector });
1042
+ });
1043
+ return painted.asReadonly();
1044
+ }
1045
+
1046
+ /**
1047
+ * Scene shapes as SVG elements: `@embedpdf/web`'s element descriptions (`svgShapesOf`), one
1048
+ * element each, a clipping `svg` with its children. The annotation and form layers draw their
1049
+ * scenes through `<svg:g [epdfSvgShapes]>` inside an `<svg>`.
1050
+ */
1051
+ /**
1052
+ * An SVG element's attributes from a record of SVG names (`stroke-width`), as `@embedpdf/web`
1053
+ * describes a scene node (templates can't spread). Set again when the record changes; an
1054
+ * attribute that left the record is removed.
1055
+ */
1056
+ class EpdfSvgAttributes {
1057
+ attributes = input.required(...(ngDevMode ? [{ debugName: "attributes", alias: 'epdfSvgAttributes' }] : [{
1058
+ alias: 'epdfSvgAttributes',
1059
+ }]));
1060
+ constructor() {
1061
+ const element = inject(ElementRef).nativeElement;
1062
+ let shown = {};
1063
+ effect(() => {
1064
+ const next = this.attributes();
1065
+ for (const name of Object.keys(shown)) {
1066
+ if (!(name in next))
1067
+ element.removeAttribute(name);
1068
+ }
1069
+ for (const [name, value] of Object.entries(next)) {
1070
+ if (shown[name] !== value)
1071
+ element.setAttribute(name, String(value));
1072
+ }
1073
+ shown = next;
1074
+ });
1075
+ }
1076
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1077
+ 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 });
1078
+ }
1079
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, decorators: [{
1080
+ type: Directive,
1081
+ args: [{ selector: '[epdfSvgAttributes]' }]
1082
+ }], ctorParameters: () => [] });
1083
+ /** The shapes, in drawing order, in the `<svg:g>` it's on, and the faces their text draws in. */
1084
+ class EpdfSvgShapes {
1085
+ shapes = input.required(...(ngDevMode ? [{ debugName: "shapes", alias: 'epdfSvgShapes' }] : [{ alias: 'epdfSvgShapes' }]));
1086
+ constructor() {
1087
+ if (!isPlatformBrowser(inject(PLATFORM_ID)))
1088
+ return;
1089
+ // The faces of this level's text, while it shows: a clipping `svg` mounts its children's.
1090
+ const mounts = createFontMounts();
1091
+ inject(DestroyRef).onDestroy(() => mounts.release());
1092
+ effect(() => mounts.mount(this.shapes().flatMap((shape) => (shape.program ? [shape.program] : []))));
1093
+ }
1094
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, deps: [], target: i0.ɵɵFactoryTarget.Component });
1095
+ 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: `
1096
+ @for (shape of shapes(); track $index) {
1097
+ @switch (shape.tag) {
1098
+ @case ('rect') {
1099
+ <svg:rect
1100
+ [epdfSvgAttributes]="shape.attributes"
1101
+ [style.mix-blend-mode]="shape.blend ?? null"
1102
+ ></svg:rect>
1103
+ }
1104
+ @case ('ellipse') {
1105
+ <svg:ellipse
1106
+ [epdfSvgAttributes]="shape.attributes"
1107
+ [style.mix-blend-mode]="shape.blend ?? null"
1108
+ ></svg:ellipse>
1109
+ }
1110
+ @case ('line') {
1111
+ <svg:line
1112
+ [epdfSvgAttributes]="shape.attributes"
1113
+ [style.mix-blend-mode]="shape.blend ?? null"
1114
+ ></svg:line>
1115
+ }
1116
+ @case ('path') {
1117
+ <svg:path
1118
+ [epdfSvgAttributes]="shape.attributes"
1119
+ [style.mix-blend-mode]="shape.blend ?? null"
1120
+ ></svg:path>
1121
+ }
1122
+ @case ('polygon') {
1123
+ <svg:polygon
1124
+ [epdfSvgAttributes]="shape.attributes"
1125
+ [style.mix-blend-mode]="shape.blend ?? null"
1126
+ ></svg:polygon>
1127
+ }
1128
+ @case ('polyline') {
1129
+ <svg:polyline
1130
+ [epdfSvgAttributes]="shape.attributes"
1131
+ [style.mix-blend-mode]="shape.blend ?? null"
1132
+ ></svg:polyline>
1133
+ }
1134
+ @case ('text') {
1135
+ <svg:text
1136
+ [epdfSvgAttributes]="shape.attributes"
1137
+ [style.mix-blend-mode]="shape.blend ?? null"
1138
+ [textContent]="shape.text ?? ''"
1139
+ ></svg:text>
1140
+ }
1141
+ @case ('svg') {
1142
+ <svg:svg [epdfSvgAttributes]="shape.attributes">
1143
+ <svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
1144
+ </svg:svg>
1145
+ }
1146
+ }
1147
+ }
1148
+ `, isInline: true, dependencies: [{ kind: "component", type: EpdfSvgShapes, selector: "g[epdfSvgShapes]", inputs: ["epdfSvgShapes"] }, { kind: "directive", type: EpdfSvgAttributes, selector: "[epdfSvgAttributes]", inputs: ["epdfSvgAttributes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1149
+ }
1150
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, decorators: [{
1151
+ type: Component,
1152
+ args: [{
1153
+ selector: 'g[epdfSvgShapes]',
1154
+ imports: [EpdfSvgAttributes],
1155
+ changeDetection: ChangeDetectionStrategy.OnPush,
1156
+ template: `
1157
+ @for (shape of shapes(); track $index) {
1158
+ @switch (shape.tag) {
1159
+ @case ('rect') {
1160
+ <svg:rect
1161
+ [epdfSvgAttributes]="shape.attributes"
1162
+ [style.mix-blend-mode]="shape.blend ?? null"
1163
+ ></svg:rect>
1164
+ }
1165
+ @case ('ellipse') {
1166
+ <svg:ellipse
1167
+ [epdfSvgAttributes]="shape.attributes"
1168
+ [style.mix-blend-mode]="shape.blend ?? null"
1169
+ ></svg:ellipse>
1170
+ }
1171
+ @case ('line') {
1172
+ <svg:line
1173
+ [epdfSvgAttributes]="shape.attributes"
1174
+ [style.mix-blend-mode]="shape.blend ?? null"
1175
+ ></svg:line>
1176
+ }
1177
+ @case ('path') {
1178
+ <svg:path
1179
+ [epdfSvgAttributes]="shape.attributes"
1180
+ [style.mix-blend-mode]="shape.blend ?? null"
1181
+ ></svg:path>
1182
+ }
1183
+ @case ('polygon') {
1184
+ <svg:polygon
1185
+ [epdfSvgAttributes]="shape.attributes"
1186
+ [style.mix-blend-mode]="shape.blend ?? null"
1187
+ ></svg:polygon>
1188
+ }
1189
+ @case ('polyline') {
1190
+ <svg:polyline
1191
+ [epdfSvgAttributes]="shape.attributes"
1192
+ [style.mix-blend-mode]="shape.blend ?? null"
1193
+ ></svg:polyline>
1194
+ }
1195
+ @case ('text') {
1196
+ <svg:text
1197
+ [epdfSvgAttributes]="shape.attributes"
1198
+ [style.mix-blend-mode]="shape.blend ?? null"
1199
+ [textContent]="shape.text ?? ''"
1200
+ ></svg:text>
1201
+ }
1202
+ @case ('svg') {
1203
+ <svg:svg [epdfSvgAttributes]="shape.attributes">
1204
+ <svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
1205
+ </svg:svg>
1206
+ }
1207
+ }
1208
+ }
1209
+ `,
1210
+ }]
1211
+ }], ctorParameters: () => [] });
1212
+
1213
+ /**
1214
+ * Development-time guardrails. Each warning fires once (by key), says what is wrong and what to
1215
+ * do instead, and stays quiet in production builds (Angular's `isDevMode()`).
1216
+ */
1217
+ const warned = new Set();
1218
+ /** Warn once per key. Quiet in production. */
1219
+ function devWarn(key, message) {
1220
+ if (!isDevMode() || warned.has(key))
1221
+ return;
1222
+ warned.add(key);
1223
+ console.warn(`[embedpdf] ${message}`);
1224
+ }
1225
+ /** For tests: forget every warning, so one can fire again. */
1226
+ function resetDevWarnings() {
1227
+ warned.clear();
1228
+ }
1229
+
1230
+ /**
1231
+ * `epdfTheme()`: a theme written with setting names, as the CSS variables for the element
1232
+ * around your viewer. The variables and their lookup are framework-free (`@embedpdf/web`); this
1233
+ * types the result for Angular's `[style]` binding, which sets custom properties as they are.
1234
+ */
1235
+ /**
1236
+ * The CSS variables of a theme: `<div [style]="theme()">` with
1237
+ * `theme = computed(() => epdfTheme({ accent: this.accent() }))`. Everything inside follows
1238
+ * them, and a CSS variable set in a stylesheet still wins over a setting.
1239
+ */
1240
+ function epdfTheme(theme) {
1241
+ return epdfThemeVariables(theme);
1242
+ }
1243
+
1244
+ /**
1245
+ * @embedpdf/angular/runtime: the viewer and what every feature builds on.
488
1246
  *
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.
1247
+ * provideEmbedPdf(config, ...features) the viewer, as providers
1248
+ * inject(EpdfViewer) its settings and status
1249
+ * inject(EpdfDocuments), EpdfDocument the documents, as methods, signals and streams
1250
+ * [epdfDocumentScope], *epdfDocumentGate which document a part of the template talks to
1251
+ * pluginService(), EpdfPluginService how a plugin's service is built
1252
+ * EPDF_PAGE, injectPage() the page a layer draws on
1253
+ * paintsPagePart(), injectPaintedParts() which parts of a page its layers paint
1254
+ * <svg:g [epdfSvgShapes]> a scene's shapes as SVG elements
494
1255
  */
495
- // One-line-per-feature: registration travels with the UI.
1256
+ // The core's types and helpers, so app code has one import for the viewer.
496
1257
 
497
1258
  /**
498
1259
  * Generated bundle index. Do not edit.
499
1260
  */
500
1261
 
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 };
1262
+ 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
1263
  //# sourceMappingURL=embedpdf-angular-runtime.mjs.map