@embedpdf/plugin-render 2.15.0 → 3.0.0-next.0

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 (59) hide show
  1. package/LICENSE +192 -21
  2. package/dist/index.cjs +709 -2
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +233 -0
  5. package/dist/index.d.ts +233 -1
  6. package/dist/index.js +703 -137
  7. package/dist/index.js.map +1 -1
  8. package/package.json +19 -58
  9. package/dist/lib/index.d.ts +0 -6
  10. package/dist/lib/manifest.d.ts +0 -4
  11. package/dist/lib/render-plugin.d.ts +0 -21
  12. package/dist/lib/types.d.ts +0 -46
  13. package/dist/preact/adapter.d.ts +0 -5
  14. package/dist/preact/core.d.ts +0 -1
  15. package/dist/preact/index.cjs +0 -2
  16. package/dist/preact/index.cjs.map +0 -1
  17. package/dist/preact/index.d.ts +0 -1
  18. package/dist/preact/index.js +0 -85
  19. package/dist/preact/index.js.map +0 -1
  20. package/dist/react/adapter.d.ts +0 -2
  21. package/dist/react/core.d.ts +0 -1
  22. package/dist/react/index.cjs +0 -2
  23. package/dist/react/index.cjs.map +0 -1
  24. package/dist/react/index.d.ts +0 -1
  25. package/dist/react/index.js +0 -84
  26. package/dist/react/index.js.map +0 -1
  27. package/dist/shared/components/index.d.ts +0 -1
  28. package/dist/shared/components/render-layer.d.ts +0 -35
  29. package/dist/shared/hooks/index.d.ts +0 -1
  30. package/dist/shared/hooks/use-render.d.ts +0 -11
  31. package/dist/shared/index.d.ts +0 -3
  32. package/dist/shared-preact/components/index.d.ts +0 -1
  33. package/dist/shared-preact/components/render-layer.d.ts +0 -35
  34. package/dist/shared-preact/hooks/index.d.ts +0 -1
  35. package/dist/shared-preact/hooks/use-render.d.ts +0 -11
  36. package/dist/shared-preact/index.d.ts +0 -3
  37. package/dist/shared-react/components/index.d.ts +0 -1
  38. package/dist/shared-react/components/render-layer.d.ts +0 -35
  39. package/dist/shared-react/hooks/index.d.ts +0 -1
  40. package/dist/shared-react/hooks/use-render.d.ts +0 -11
  41. package/dist/shared-react/index.d.ts +0 -3
  42. package/dist/svelte/components/RenderLayer.svelte.d.ts +0 -24
  43. package/dist/svelte/components/index.d.ts +0 -1
  44. package/dist/svelte/hooks/index.d.ts +0 -1
  45. package/dist/svelte/hooks/use-render.d.ts +0 -11
  46. package/dist/svelte/index.cjs +0 -2
  47. package/dist/svelte/index.cjs.map +0 -1
  48. package/dist/svelte/index.d.ts +0 -3
  49. package/dist/svelte/index.js +0 -121
  50. package/dist/svelte/index.js.map +0 -1
  51. package/dist/vue/components/index.d.ts +0 -1
  52. package/dist/vue/components/render-layer.vue.d.ts +0 -21
  53. package/dist/vue/hooks/index.d.ts +0 -1
  54. package/dist/vue/hooks/use-render.d.ts +0 -3
  55. package/dist/vue/index.cjs +0 -2
  56. package/dist/vue/index.cjs.map +0 -1
  57. package/dist/vue/index.d.ts +0 -3
  58. package/dist/vue/index.js +0 -112
  59. package/dist/vue/index.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,709 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("@embedpdf/core"),n=class extends e.BasePlugin{constructor(e,n,t){super(e,n),this.config=t}buildCapability(){return{renderPage:e=>this.renderPage(e),renderPageRect:e=>this.renderPageRect(e),renderPageRaw:e=>this.renderPageRaw(e),renderPageRectRaw:e=>this.renderPageRectRaw(e),forDocument:e=>this.createRenderScope(e)}}createRenderScope(e){return{renderPage:n=>this.renderPage(n,e),renderPageRect:n=>this.renderPageRect(n,e),renderPageRaw:n=>this.renderPageRaw(n,e),renderPageRectRaw:n=>this.renderPageRectRaw(n,e)}}renderPage({pageIndex:e,options:n},t){const i=t??this.getActiveDocumentId(),o=this.coreState.core.documents[i];if(!(null==o?void 0:o.document))throw new Error(`Document ${i} not loaded`);const r=o.document.pages.find(n=>n.index===e);if(!r)throw new Error(`Page ${e} not found in document ${i}`);const d={...n??{},withForms:(null==n?void 0:n.withForms)??this.config.withForms??!1,withAnnotations:(null==n?void 0:n.withAnnotations)??this.config.withAnnotations??!1,imageType:(null==n?void 0:n.imageType)??this.config.defaultImageType??"image/png",imageQuality:(null==n?void 0:n.imageQuality)??this.config.defaultImageQuality??.92};return this.engine.renderPage(o.document,r,d)}renderPageRect({pageIndex:e,rect:n,options:t},i){const o=i??this.getActiveDocumentId(),r=this.coreState.core.documents[o];if(!(null==r?void 0:r.document))throw new Error(`Document ${o} not loaded`);const d=r.document.pages.find(n=>n.index===e);if(!d)throw new Error(`Page ${e} not found in document ${o}`);const a={...t??{},withForms:(null==t?void 0:t.withForms)??this.config.withForms??!1,withAnnotations:(null==t?void 0:t.withAnnotations)??this.config.withAnnotations??!1,imageType:(null==t?void 0:t.imageType)??this.config.defaultImageType??"image/png",imageQuality:(null==t?void 0:t.imageQuality)??this.config.defaultImageQuality??.92};return this.engine.renderPageRect(r.document,d,n,a)}renderPageRaw({pageIndex:e,options:n},t){const i=t??this.getActiveDocumentId(),o=this.coreState.core.documents[i];if(!(null==o?void 0:o.document))throw new Error(`Document ${i} not loaded`);const r=o.document.pages.find(n=>n.index===e);if(!r)throw new Error(`Page ${e} not found in document ${i}`);const d={...n??{},withForms:(null==n?void 0:n.withForms)??this.config.withForms??!1,withAnnotations:(null==n?void 0:n.withAnnotations)??this.config.withAnnotations??!1};return this.engine.renderPageRaw(o.document,r,d)}renderPageRectRaw({pageIndex:e,rect:n,options:t},i){const o=i??this.getActiveDocumentId(),r=this.coreState.core.documents[o];if(!(null==r?void 0:r.document))throw new Error(`Document ${o} not loaded`);const d=r.document.pages.find(n=>n.index===e);if(!d)throw new Error(`Page ${e} not found in document ${o}`);const a={...t??{},withForms:(null==t?void 0:t.withForms)??this.config.withForms??!1,withAnnotations:(null==t?void 0:t.withAnnotations)??this.config.withAnnotations??!1};return this.engine.renderPageRectRaw(r.document,d,n,a)}async initialize(e){this.logger.info("RenderPlugin","Initialize","Render plugin initialized")}async destroy(){super.destroy()}};n.id="render";let t=n;const i="render",o={manifest:{id:i,name:"Render Plugin",version:"1.0.0",provides:["render"],requires:[],optional:[],defaultConfig:{}},create:(e,n)=>new t(i,e,n),reducer:()=>{},initialState:{}};exports.RenderPlugin=t,exports.RenderPluginPackage=o;
2
- //# sourceMappingURL=index.cjs.map
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _embedpdf_core = require("@embedpdf/core");
3
+ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
4
+ //#region src/paint-plan.ts
5
+ const EMPTY_TILE_PLAN = {
6
+ engaged: false,
7
+ paint: [],
8
+ fetching: [],
9
+ stamp: "empty"
10
+ };
11
+ function resolveTiling(config) {
12
+ return {
13
+ tileSize: config?.tileSize ?? 512,
14
+ scales: config?.scales ?? [
15
+ 1,
16
+ 2,
17
+ 4,
18
+ 8,
19
+ 16,
20
+ 32
21
+ ],
22
+ engageAt: config?.engageAt ?? 1.25,
23
+ prefetchMargin: config?.prefetch?.margin ?? .5,
24
+ velocityBias: config?.prefetch?.velocityBias ?? true,
25
+ settleMs: config?.settleMs ?? 150
26
+ };
27
+ }
28
+ //#endregion
29
+ //#region src/raster-store.ts
30
+ /**
31
+ * Client-side mirror of the server's one-door read-through: per-key
32
+ * SINGLEFLIGHT + LRU over encoded page images.
33
+ *
34
+ * The key is the raster's canonical identity (conformed viewport +
35
+ * annotations flag + epoch — see `renderSourceKey`), so the lattice does the
36
+ * caching work: every ask inside one rung is the same key, and rung
37
+ * re-crossings (zoom 1280 → 2560 → back) resolve instantly from the LRU
38
+ * instead of re-rendering. Epoch bumps mint new keys; stale-epoch entries
39
+ * age out through the LRU (they are never re-requested).
40
+ *
41
+ * Consumer aborts are REFCOUNTED: a caller abandoning a fetch (camera moved,
42
+ * layer unmounted) detaches without killing it for other consumers; the
43
+ * underlying engine call aborts only when the LAST in-flight consumer leaves.
44
+ * Resolved entries are kept (evictable by LRU) — an abort after resolution
45
+ * is a no-op by design.
46
+ */
47
+ var RasterStore = class {
48
+ constructor(maxEntries = 48) {
49
+ this.maxEntries = maxEntries;
50
+ this.entries = /* @__PURE__ */ new Map();
51
+ this.tick = 0;
52
+ }
53
+ acquire(key, fetch, signal) {
54
+ const existing = this.entries.get(key);
55
+ if (existing) {
56
+ existing.used = ++this.tick;
57
+ return this.attach(key, existing, signal);
58
+ }
59
+ const abort = new AbortController();
60
+ const entry = {
61
+ promise: void 0,
62
+ refs: 0,
63
+ abort,
64
+ used: ++this.tick
65
+ };
66
+ entry.promise = fetch(abort.signal).then((handle) => {
67
+ entry.handle = handle;
68
+ return handle;
69
+ }, (err) => {
70
+ if (this.entries.get(key) === entry) this.entries.delete(key);
71
+ throw err;
72
+ });
73
+ entry.promise.catch(() => {});
74
+ this.entries.set(key, entry);
75
+ this.evict();
76
+ return this.attach(key, entry, signal);
77
+ }
78
+ /** Resolved handle if present — synchronous peek for paint-time reuse. */
79
+ peek(key) {
80
+ const entry = this.entries.get(key);
81
+ if (entry?.handle) entry.used = ++this.tick;
82
+ return entry?.handle;
83
+ }
84
+ get size() {
85
+ return this.entries.size;
86
+ }
87
+ attach(key, entry, signal) {
88
+ if (signal?.aborted) return Promise.reject(abortReason(signal));
89
+ entry.refs += 1;
90
+ if (!signal) {
91
+ const release = () => {
92
+ entry.refs -= 1;
93
+ };
94
+ entry.promise.then(release, release);
95
+ return entry.promise;
96
+ }
97
+ let settled = false;
98
+ return new Promise((resolve, reject) => {
99
+ const onAbort = () => {
100
+ if (settled) return;
101
+ settled = true;
102
+ entry.refs -= 1;
103
+ if (entry.refs <= 0 && entry.handle === void 0) {
104
+ entry.abort.abort(abortReason(signal));
105
+ if (this.entries.get(key) === entry) this.entries.delete(key);
106
+ }
107
+ reject(abortReason(signal));
108
+ };
109
+ signal.addEventListener("abort", onAbort, { once: true });
110
+ entry.promise.then((handle) => {
111
+ if (settled) return;
112
+ settled = true;
113
+ entry.refs -= 1;
114
+ signal.removeEventListener("abort", onAbort);
115
+ resolve(handle);
116
+ }, (err) => {
117
+ if (settled) return;
118
+ settled = true;
119
+ entry.refs -= 1;
120
+ signal.removeEventListener("abort", onAbort);
121
+ reject(err);
122
+ });
123
+ });
124
+ }
125
+ evict() {
126
+ if (this.entries.size <= this.maxEntries) return;
127
+ const candidates = [...this.entries.entries()].filter(([, e]) => e.refs <= 0 && e.handle !== void 0).sort((a, b) => a[1].used - b[1].used);
128
+ for (const [key] of candidates) {
129
+ if (this.entries.size <= this.maxEntries) return;
130
+ this.entries.delete(key);
131
+ }
132
+ }
133
+ };
134
+ function abortReason(signal) {
135
+ return signal.reason ?? new DOMException("aborted", "AbortError");
136
+ }
137
+ //#endregion
138
+ //#region src/tiles.ts
139
+ function tileGrid(page, scale, tileSize) {
140
+ return {
141
+ scale,
142
+ tileSize,
143
+ cols: Math.max(1, Math.ceil(page.width * scale / tileSize)),
144
+ rows: Math.max(1, Math.ceil(page.height * scale / tileSize))
145
+ };
146
+ }
147
+ /** One tile's edge in page points at its level. */
148
+ const tileSpanPt = (grid) => grid.tileSize / grid.scale;
149
+ /**
150
+ * The tiles of `grid` intersecting a y-down page-point rect, clamped to
151
+ * the page. An empty intersection yields an empty list.
152
+ */
153
+ function tilesInRect(grid, page, rect) {
154
+ const span = tileSpanPt(grid);
155
+ const x0 = Math.max(0, Math.floor(rect.x / span));
156
+ const y0 = Math.max(0, Math.floor(rect.y / span));
157
+ const x1 = Math.min(grid.cols - 1, Math.ceil((rect.x + rect.width) / span) - 1);
158
+ const y1 = Math.min(grid.rows - 1, Math.ceil((rect.y + rect.height) / span) - 1);
159
+ const out = [];
160
+ for (let iy = y0; iy <= y1; iy++) for (let ix = x0; ix <= x1; ix++) out.push({
161
+ ix,
162
+ iy
163
+ });
164
+ return out;
165
+ }
166
+ /** Paint rect: y-down page points, clamped to the page edge. */
167
+ function tilePaintRect(grid, page, c) {
168
+ const span = tileSpanPt(grid);
169
+ const x = c.ix * span;
170
+ const y = c.iy * span;
171
+ return {
172
+ x,
173
+ y,
174
+ width: Math.min(span, page.width - x),
175
+ height: Math.min(span, page.height - y)
176
+ };
177
+ }
178
+ /** Engine target rect: y-UP PDF user space (top > bottom). */
179
+ function tileEngineRect(grid, page, c) {
180
+ const paint = tilePaintRect(grid, page, c);
181
+ return {
182
+ left: paint.x,
183
+ right: paint.x + paint.width,
184
+ top: page.height - paint.y,
185
+ bottom: page.height - (paint.y + paint.height)
186
+ };
187
+ }
188
+ /**
189
+ * Does a set of PAINTED want-level tiles cover `region` (y-down points)?
190
+ * The retention release check: a retained source may leave the paint list
191
+ * once its visible footprint answers true here. Pure index arithmetic on
192
+ * the aligned grid — the whole reason the pyramid is aligned.
193
+ */
194
+ function regionCovered(grid, page, region, isPainted) {
195
+ if (region.width <= 0 || region.height <= 0) return true;
196
+ for (const c of tilesInRect(grid, page, region)) if (!isPainted(c)) return false;
197
+ return true;
198
+ }
199
+ /**
200
+ * Inflate a visible rect by a prefetch margin (fractions of the rect's own
201
+ * size per side), optionally velocity-biased: the margin stretches toward
202
+ * the direction of travel and shrinks behind it. Pure — the producer's
203
+ * velocity is just data.
204
+ */
205
+ function inflateRect(rect, margin, velocity) {
206
+ const mx = rect.width * margin;
207
+ const my = rect.height * margin;
208
+ let left = mx;
209
+ let right = mx;
210
+ let up = my;
211
+ let down = my;
212
+ if (velocity) {
213
+ if (velocity.dx > 0) {
214
+ right = mx * 2;
215
+ left = mx / 2;
216
+ } else if (velocity.dx < 0) {
217
+ left = mx * 2;
218
+ right = mx / 2;
219
+ }
220
+ if (velocity.dy > 0) {
221
+ down = my * 2;
222
+ up = my / 2;
223
+ } else if (velocity.dy < 0) {
224
+ up = my * 2;
225
+ down = my / 2;
226
+ }
227
+ }
228
+ return {
229
+ x: rect.x - left,
230
+ y: rect.y - up,
231
+ width: rect.width + left + right,
232
+ height: rect.height + up + down
233
+ };
234
+ }
235
+ /** Snap UP through a pyramid's sorted scales; cap at the top. */
236
+ function snapToPyramid(scales, needed) {
237
+ const sorted = [...scales].sort((a, b) => a - b);
238
+ return sorted.find((s) => s >= needed) ?? sorted[sorted.length - 1];
239
+ }
240
+ //#endregion
241
+ //#region src/tile-manager.ts
242
+ /**
243
+ * The tile retention state machine. One instance per
244
+ * plugin instance (per document), all levels and pages in one place, over
245
+ * the SAME RasterStore the base renders use.
246
+ *
247
+ * Invariant it enforces: every screen region paints the sharpest PAINTED
248
+ * pixels available; quality per region only goes up until the want set
249
+ * resolves. Mechanics:
250
+ * - want vs paint: `plan()` schedules fetches for the want set (P0
251
+ * visible first, P1 prefetch when P0 is fully resolved) and returns a
252
+ * paint list drawn ONLY from resolved entries — current level AND
253
+ * retained older generations.
254
+ * - release-on-occlusion: when a want-level tile reports PAINTED (the
255
+ * layer's img onload — the decode boundary), retained sources whose
256
+ * visible footprint is covered by painted want tiles leave the paint
257
+ * list. Their bytes stay in the RasterStore (demotion, not eviction) —
258
+ * zoom-back re-promotes from cache.
259
+ * - epoch exception: an invalidation bump means retained pixels are
260
+ * WRONG — everything of the old epoch drops immediately.
261
+ *
262
+ * `plan()` is called from selectors: it MUST NOT dispatch. Fetch kickoff
263
+ * is idempotent (the store singleflights); resolution handlers dispatch
264
+ * the wake-up (`onAdvance`) that makes subscribed layers recompute.
265
+ */
266
+ var TileManager = class {
267
+ constructor(deps) {
268
+ this.deps = deps;
269
+ this.pages = /* @__PURE__ */ new Map();
270
+ }
271
+ plan(pon, demand, includeAnnotations) {
272
+ const { config } = this.deps;
273
+ const policy = this.deps.getPolicy();
274
+ const page = this.deps.getPageSize(pon);
275
+ if (!page) return EMPTY_TILE_PLAN;
276
+ const epoch = this.deps.getEpoch(pon, includeAnnotations);
277
+ const state = this.pageState(pon);
278
+ if (state.epoch !== epoch) {
279
+ this.abortAll(state);
280
+ state.entries.clear();
281
+ state.epoch = epoch;
282
+ state.wantScale = null;
283
+ state.planCache = null;
284
+ }
285
+ const baseSupplied = policy.kind === "lattice" ? widthOf((0, _embedpdf_core.snapFullPageViewport)(policy, {
286
+ kind: "width",
287
+ width: demand.desiredDeviceWidth
288
+ })) : demand.desiredDeviceWidth;
289
+ if (demand.desiredDeviceWidth / baseSupplied <= config.engageAt) {
290
+ if (state.entries.size || state.wantScale !== null) {
291
+ this.abortAll(state);
292
+ state.entries.clear();
293
+ state.wantScale = null;
294
+ state.planCache = null;
295
+ }
296
+ return EMPTY_TILE_PLAN;
297
+ }
298
+ const pyramid = policy.kind === "lattice" && policy.tiles ? policy.tiles.scales : config.scales;
299
+ const wantScale = snapToPyramid(pyramid, demand.desiredDeviceWidth / page.width);
300
+ const grid = tileGrid(page, wantScale, config.tileSize);
301
+ const visible = demand.visibleRect ?? {
302
+ x: 0,
303
+ y: 0,
304
+ width: page.width,
305
+ height: page.height
306
+ };
307
+ state.lastVisible = visible;
308
+ const p0 = tilesInRect(grid, page, visible);
309
+ const ring = inflateRect(visible, config.prefetchMargin, config.velocityBias ? demand.velocity : void 0);
310
+ const p0Keys = new Set(p0.map((c) => coordKey(c)));
311
+ const p1 = tilesInRect(grid, page, ring).filter((c) => !p0Keys.has(coordKey(c)));
312
+ this.schedule(pon, state, page, grid, wantScale, includeAnnotations, epoch, p0, p1);
313
+ const demandKey = `${wantScale}|${rectKey(visible)}|e${epoch}`;
314
+ if (state.planCache && state.planCache.demandKey === demandKey && state.planCache.version === state.version) return state.planCache.plan;
315
+ const paint = [];
316
+ const fetching = [];
317
+ for (const entry of state.entries.values()) {
318
+ const visIntersect = (0, _embedpdf_core_geometry.intersectRects)(entry.rect, visible);
319
+ if (entry.handle) {
320
+ if (visIntersect.width > 0 && visIntersect.height > 0) paint.push({
321
+ key: entry.key,
322
+ scale: entry.scale,
323
+ rect: entry.rect,
324
+ z: zOf(pyramid, entry.scale),
325
+ handle: entry.handle
326
+ });
327
+ } else if (entry.scale === wantScale) fetching.push(entry.key);
328
+ }
329
+ paint.sort((a, b) => a.z - b.z || a.key.localeCompare(b.key));
330
+ const plan = {
331
+ engaged: true,
332
+ paint,
333
+ fetching: fetching.sort(),
334
+ stamp: `${demandKey}|v${state.version}`
335
+ };
336
+ state.planCache = {
337
+ demandKey,
338
+ version: state.version,
339
+ plan
340
+ };
341
+ return plan;
342
+ }
343
+ /** The layer's decode-boundary report: this key's <img> actually painted. */
344
+ sourcePainted(pon, key) {
345
+ const state = this.pages.get(pon);
346
+ const entry = state?.entries.get(key);
347
+ if (!state || !entry || entry.painted) return;
348
+ entry.painted = true;
349
+ this.releaseOccluded(state, entry);
350
+ state.version += 1;
351
+ state.planCache = null;
352
+ this.deps.onAdvance(pon);
353
+ }
354
+ /** A lens unmounted its TileLayer: stop fetching, drop bookkeeping.
355
+ * Resolved bytes stay in the RasterStore for a re-mount. */
356
+ releasePage(pon) {
357
+ const state = this.pages.get(pon);
358
+ if (!state) return;
359
+ this.abortAll(state);
360
+ this.pages.delete(pon);
361
+ }
362
+ pageState(pon) {
363
+ let state = this.pages.get(pon);
364
+ if (!state) {
365
+ state = {
366
+ epoch: -1,
367
+ wantScale: null,
368
+ entries: /* @__PURE__ */ new Map(),
369
+ version: 0,
370
+ planCache: null,
371
+ settleTimer: null,
372
+ pendingLevel: null,
373
+ lastVisible: null
374
+ };
375
+ this.pages.set(pon, state);
376
+ }
377
+ return state;
378
+ }
379
+ schedule(pon, state, page, grid, wantScale, includeAnnotations, epoch, p0, p1) {
380
+ const { config } = this.deps;
381
+ const firstEngage = state.wantScale === null;
382
+ const levelChanged = state.wantScale !== null && state.wantScale !== wantScale;
383
+ state.wantScale = wantScale;
384
+ const wanted = new Set([...p0, ...p1].map((c) => this.tileKey(pon, wantScale, c, includeAnnotations, epoch)));
385
+ for (const [key, entry] of state.entries) if (entry.handle === void 0 && !wanted.has(key)) {
386
+ entry.abort?.abort();
387
+ state.entries.delete(key);
388
+ }
389
+ const kickoff = () => {
390
+ if (this.ensureFetches(pon, state, page, grid, includeAnnotations, epoch, p0)) this.ensureFetches(pon, state, page, grid, includeAnnotations, epoch, p1);
391
+ };
392
+ if (levelChanged && config.settleMs > 0) {
393
+ state.pendingLevel = wantScale;
394
+ if (state.settleTimer !== null) clearTimeout(state.settleTimer);
395
+ state.settleTimer = setTimeout(() => {
396
+ state.settleTimer = null;
397
+ if (state.pendingLevel === wantScale) kickoff();
398
+ }, config.settleMs);
399
+ return;
400
+ }
401
+ if (state.settleTimer !== null && !levelChanged && !firstEngage) {
402
+ clearTimeout(state.settleTimer);
403
+ state.settleTimer = null;
404
+ state.pendingLevel = null;
405
+ }
406
+ kickoff();
407
+ }
408
+ /** Start missing fetches; true when every coord is already resolved. */
409
+ ensureFetches(pon, state, page, grid, includeAnnotations, epoch, coords) {
410
+ let allReady = true;
411
+ for (const coord of coords) {
412
+ const key = this.tileKey(pon, grid.scale, coord, includeAnnotations, epoch);
413
+ const existing = state.entries.get(key);
414
+ if (existing) {
415
+ if (existing.handle === void 0) allReady = false;
416
+ continue;
417
+ }
418
+ allReady = false;
419
+ const abort = new AbortController();
420
+ const entry = {
421
+ key,
422
+ scale: grid.scale,
423
+ coord,
424
+ rect: tilePaintRect(grid, page, coord),
425
+ painted: false,
426
+ abort
427
+ };
428
+ state.entries.set(key, entry);
429
+ this.deps.store.acquire(key, (signal) => this.deps.fetchTile(pon, tileEngineRect(grid, page, coord), grid.scale, includeAnnotations, signal), abort.signal).then((handle) => {
430
+ if (state.entries.get(key) !== entry) return;
431
+ entry.handle = handle;
432
+ state.version += 1;
433
+ state.planCache = null;
434
+ this.deps.onAdvance(pon);
435
+ }, () => {
436
+ if (state.entries.get(key) === entry) state.entries.delete(key);
437
+ });
438
+ }
439
+ return allReady;
440
+ }
441
+ /**
442
+ * The release rule: after `painted` lands on a want-level tile, retained
443
+ * sources (any other level) whose footprint ∩ painted-tile region is
444
+ * covered by PAINTED want-level tiles leave the paint list. Index
445
+ * arithmetic via the aligned grid — no rectangle geometry.
446
+ */
447
+ releaseOccluded(state, painted) {
448
+ const wantScale = state.wantScale;
449
+ if (wantScale === null || painted.scale !== wantScale) return;
450
+ const page = this.deps.getPageSize(pagePonOf(painted.key));
451
+ if (!page) return;
452
+ const grid = tileGrid(page, wantScale, this.deps.config.tileSize);
453
+ const paintedAt = (c) => {
454
+ const key = this.tileKey(pagePonOf(painted.key), wantScale, c, annotationsOf(painted.key), state.epoch);
455
+ return state.entries.get(key)?.painted === true;
456
+ };
457
+ for (const [key, entry] of state.entries) {
458
+ if (entry.scale === wantScale || entry.handle === void 0) continue;
459
+ const overlap = (0, _embedpdf_core_geometry.intersectRects)(entry.rect, painted.rect);
460
+ if (overlap.width <= 0 || overlap.height <= 0) continue;
461
+ if (regionCovered(grid, page, state.lastVisible ? (0, _embedpdf_core_geometry.intersectRects)(entry.rect, state.lastVisible) : entry.rect, paintedAt)) state.entries.delete(key);
462
+ }
463
+ }
464
+ abortAll(state) {
465
+ if (state.settleTimer !== null) {
466
+ clearTimeout(state.settleTimer);
467
+ state.settleTimer = null;
468
+ }
469
+ for (const entry of state.entries.values()) if (entry.handle === void 0) entry.abort?.abort();
470
+ }
471
+ tileKey(pon, scale, c, includeAnnotations, epoch) {
472
+ return `t:${pon}|s${scale}|${c.ix},${c.iy}|a${includeAnnotations ? 1 : 0}|e${epoch}`;
473
+ }
474
+ };
475
+ const widthOf = (v) => v.kind === "width" && v.width !== void 0 ? v.width : Number.POSITIVE_INFINITY;
476
+ const coordKey = (c) => `${c.ix},${c.iy}`;
477
+ const rectKey = (r) => `${Math.round(r.x)},${Math.round(r.y)},${Math.round(r.width)},${Math.round(r.height)}`;
478
+ /** Stacking: position in the ascending pyramid (coarse under fine). */
479
+ const zOf = (pyramid, scale) => {
480
+ const sorted = [...pyramid].sort((a, b) => a - b);
481
+ const idx = sorted.indexOf(scale);
482
+ return idx === -1 ? sorted.findIndex((s) => s > scale) : idx;
483
+ };
484
+ const pagePonOf = (key) => Number(key.slice(2, key.indexOf("|")));
485
+ const annotationsOf = (key) => key.includes("|a1|");
486
+ //#endregion
487
+ //#region src/capability.ts
488
+ /**
489
+ * The render capability: the ONE policy consumer in the client stack
490
+ * (the three-layer rule). The engine never snaps; framework
491
+ * layers never see the policy; everything between — conforming a desired
492
+ * scale to the deployment lattice, collapsing same-rung asks in the raster
493
+ * store, exposing stable source keys — happens here.
494
+ */
495
+ function createRenderCapability(ctx, options = {}) {
496
+ const store = new RasterStore();
497
+ const policy = () => ctx.document()?.renderPolicy ?? _embedpdf_core.CONTINUOUS_RENDER_POLICY;
498
+ const pageOf = (pon) => (ctx.document()?.pages ?? []).find((p) => p.pageObjectNumber === pon);
499
+ const pageWidthOf = (pon) => {
500
+ const page = pageOf(pon);
501
+ if (!page) throw new Error(`render: unknown page object number ${pon}`);
502
+ return page.size.width;
503
+ };
504
+ const conformViewport = (pon, scale) => {
505
+ const p = policy();
506
+ if (p.kind !== "lattice") return {
507
+ kind: "scale",
508
+ scale
509
+ };
510
+ return (0, _embedpdf_core.snapFullPageViewport)(p, {
511
+ kind: "scale",
512
+ scale
513
+ }, { pageWidth: pageWidthOf(pon) });
514
+ };
515
+ const viewportKey = (viewport) => viewport.kind === "width" ? `w${viewport.width}` : `s${viewport.scale ?? 1}`;
516
+ const epochOf = (pon, includeAnnotations) => {
517
+ const s = ctx.getState();
518
+ const content = s.contentEpochs[pon] ?? 0;
519
+ if (!includeAnnotations) return content;
520
+ return content + (s.annotatedEpochs[pon] ?? 0);
521
+ };
522
+ const tiles = new TileManager({
523
+ store,
524
+ config: resolveTiling(options.tiling),
525
+ getPolicy: policy,
526
+ getPageSize: (pon) => pageOf(pon)?.size,
527
+ getEpoch: epochOf,
528
+ fetchTile: (pon, rect, scale, includeAnnotations, signal) => {
529
+ const doc = ctx.doc;
530
+ if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
531
+ const task = doc.page(pon).render.image({
532
+ target: {
533
+ kind: "rect",
534
+ rect
535
+ },
536
+ viewport: {
537
+ kind: "scale",
538
+ scale
539
+ },
540
+ includeAnnotations
541
+ });
542
+ if (signal.aborted) task.abort(signal.reason);
543
+ else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
544
+ return task;
545
+ },
546
+ onAdvance: (pon) => ctx.dispatch({
547
+ type: "PAINT_ADVANCED",
548
+ pon
549
+ })
550
+ });
551
+ return {
552
+ renderPage(pon, { scale, includeAnnotations, signal }) {
553
+ const doc = ctx.doc;
554
+ if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
555
+ const annotations = includeAnnotations ?? true;
556
+ const viewport = conformViewport(pon, scale);
557
+ const key = `${pon}|${viewportKey(viewport)}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}`;
558
+ return store.acquire(key, (storeSignal) => {
559
+ const task = doc.page(pon).render.image({
560
+ viewport,
561
+ includeAnnotations: annotations
562
+ });
563
+ if (storeSignal.aborted) task.abort(storeSignal.reason);
564
+ else storeSignal.addEventListener("abort", () => task.abort(storeSignal.reason), { once: true });
565
+ return task;
566
+ }, signal);
567
+ },
568
+ renderSourceKey(pon, { scale, includeAnnotations }) {
569
+ const annotations = includeAnnotations ?? true;
570
+ const viewport = conformViewport(pon, scale);
571
+ return `${pon}|${viewportKey(viewport)}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}`;
572
+ },
573
+ conformViewport,
574
+ renderPolicy: policy,
575
+ tilePlan(pon, demand, opts) {
576
+ return tiles.plan(pon, demand, opts?.includeAnnotations ?? true);
577
+ },
578
+ tilePainted(pon, key) {
579
+ tiles.sourcePainted(pon, key);
580
+ },
581
+ releaseTiles(pon) {
582
+ tiles.releasePage(pon);
583
+ },
584
+ renderEpoch(pon, includeAnnotations = true) {
585
+ return epochOf(pon, includeAnnotations);
586
+ },
587
+ invalidate({ pons, scope = "content" } = {}) {
588
+ const target = pons ?? (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
589
+ if (target.length) ctx.dispatch({
590
+ type: "INVALIDATE",
591
+ scope,
592
+ pons: target
593
+ });
594
+ }
595
+ };
596
+ }
597
+ //#endregion
598
+ //#region src/effects.ts
599
+ /**
600
+ * Which pages' ANNOTATED raster a confirmed mutation repaints — the whole
601
+ * event→scope map. Everything the engine emits today is appearance-scoped;
602
+ * when it grows content-mutation events (redaction.applied, content.edited),
603
+ * they get a case here dispatching scope 'content'. Over-invalidation is
604
+ * acceptable (a z-order move that changes nothing repaints one thumb);
605
+ * under-invalidation is the bug.
606
+ */
607
+ function annotatedPons(event, allPons) {
608
+ switch (event.type) {
609
+ case "annotation.created":
610
+ case "annotation.updated":
611
+ case "annotation.deleted":
612
+ case "annotation.moved": return [event.pageObjectNumber];
613
+ case "form.valueChanged":
614
+ case "form.effectsApplied": return event.changedWidgets.map((w) => w.pageObjectNumber);
615
+ case "form.fieldDeleted": return event.removedWidgets.map((w) => w.pageObjectNumber);
616
+ case "form.fieldCreated":
617
+ case "form.fieldUpdated":
618
+ case "form.widgetAttached":
619
+ case "form.widgetDetached": return event.field.widgets.map((w) => w.pageObjectNumber);
620
+ case "form.imported":
621
+ case "form.repaired": return allPons();
622
+ default: return [];
623
+ }
624
+ }
625
+ function registerRenderEffects(ctx) {
626
+ const doc = ctx.doc;
627
+ if (!doc) return;
628
+ const allPons = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
629
+ const unsubscribe = doc.events.subscribe((event) => {
630
+ if (event.type === "redaction.applied") {
631
+ const pons = event.results.filter((r) => r.status === "applied").map((r) => r.pageObjectNumber);
632
+ if (pons.length) ctx.dispatch({
633
+ type: "INVALIDATE",
634
+ scope: "content",
635
+ pons
636
+ });
637
+ return;
638
+ }
639
+ const pons = annotatedPons(event, allPons);
640
+ if (pons.length) ctx.dispatch({
641
+ type: "INVALIDATE",
642
+ scope: "annotations",
643
+ pons
644
+ });
645
+ });
646
+ ctx.cleanup(unsubscribe);
647
+ }
648
+ //#endregion
649
+ //#region src/reducer.ts
650
+ const initialRenderState = () => ({
651
+ contentEpochs: {},
652
+ annotatedEpochs: {},
653
+ paintVersions: {}
654
+ });
655
+ const bump = (epochs, pons) => {
656
+ const next = { ...epochs };
657
+ for (const pon of pons) next[pon] = (next[pon] ?? 0) + 1;
658
+ return next;
659
+ };
660
+ /** Pure. Two transitions: bump the touched pages' ledger for the fact's
661
+ * scope (one action per fact, so a batch result bumps each pon once — a
662
+ * 'content' fact bumps ONLY the content ledger; annotated readers sum both,
663
+ * which is how content invalidation reaches them too), and the tile
664
+ * wake-up counter. The render POLICY is deliberately NOT here — it's a
665
+ * document fact on `DocumentMeta`, materialized by the kernel at open. */
666
+ const renderReducer = (state, a) => {
667
+ if (a.type === "PAINT_ADVANCED") return {
668
+ ...state,
669
+ paintVersions: bump(state.paintVersions, [a.pon])
670
+ };
671
+ if (a.type !== "INVALIDATE" || a.pons.length === 0) return state;
672
+ return a.scope === "content" ? {
673
+ ...state,
674
+ contentEpochs: bump(state.contentEpochs, a.pons)
675
+ } : {
676
+ ...state,
677
+ annotatedEpochs: bump(state.annotatedEpochs, a.pons)
678
+ };
679
+ };
680
+ //#endregion
681
+ //#region src/types.ts
682
+ const RenderToken = (0, _embedpdf_core.createCapabilityToken)("render");
683
+ //#endregion
684
+ //#region src/render.plugin.ts
685
+ /**
686
+ * Document-scoped. The ONE policy consumer in the client stack: the doc-bind
687
+ * effect resolves the engine's advertised render
688
+ * policy; `renderPage` conforms desired scales to it and collapses
689
+ * same-rung asks in the raster store; the tile manager turns host-supplied
690
+ * demand into a retention-safe paint plan over the SAME store. State is the
691
+ * per-page ledger — raster versions (two doors: the document event stream's
692
+ * built-in map and the `invalidate` verb), the policy latch, and the tile
693
+ * wake-up counter. Layers key on `renderSourceKey`/`tilePlan` and refetch
694
+ * exactly when those change.
695
+ */
696
+ const renderPlugin = (options = {}) => (0, _embedpdf_core.definePlugin)({
697
+ id: "render",
698
+ scope: "document",
699
+ token: RenderToken,
700
+ initialState: initialRenderState,
701
+ reduce: renderReducer,
702
+ capability: (ctx) => createRenderCapability(ctx, options),
703
+ effects: registerRenderEffects
704
+ });
705
+ //#endregion
706
+ exports.RenderToken = RenderToken;
707
+ exports.renderPlugin = renderPlugin;
708
+
709
+ //# sourceMappingURL=index.cjs.map