@embedpdf/plugin-render 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.
package/dist/index.cjs CHANGED
@@ -1,72 +1,255 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-Bvlo2Ukp.cjs");
2
+ const require_contract = require("./contract-CGcu-84H.cjs");
3
+ const require_contract_host = require("./contract/host.cjs");
3
4
  let _embedpdf_core = require("@embedpdf/core");
4
5
  let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
5
- //#region src/paint-plan.ts
6
- const EMPTY_TILE_PLAN = {
7
- engaged: false,
8
- paint: [],
9
- fetching: [],
10
- stamp: "empty"
6
+ //#region src/appearance-store.ts
7
+ /**
8
+ * The engine's pictures of a page's annotations or form widgets, shared
9
+ * between the views and plugins that draw them. One ask is one key (the
10
+ * page, the family, the scale and the owner's items at their versions):
11
+ * asks with the same key share one fetch.
12
+ *
13
+ * Consumer aborts are refcounted, as in the raster store: a view that leaves
14
+ * detaches without stopping the fetch for the others; the fetch stops only
15
+ * when the last one leaves before it resolved.
16
+ *
17
+ * Kept for undo:
18
+ * - the last {@link SETS_KEPT} sets of each page and family: an undo that
19
+ * brings a page back to an earlier set asks the engine for nothing;
20
+ * - each item's last pictures ({@link RecentPictures}), a while after it left
21
+ * its page: an undo that brings it back shows them at once.
22
+ */
23
+ var AppearanceStore = class {
24
+ constructor() {
25
+ this.entries = /* @__PURE__ */ new Map();
26
+ this.kept = /* @__PURE__ */ new Map();
27
+ }
28
+ /** `group` names the page and family; `key` the whole ask. */
29
+ acquire(group, key, fetch, signal) {
30
+ const existing = this.entries.get(key);
31
+ if (existing) {
32
+ if (existing.resolved) this.keep(group, key);
33
+ return this.attach(key, existing, signal);
34
+ }
35
+ const entry = {
36
+ group,
37
+ promise: void 0,
38
+ refs: 0,
39
+ abort: new AbortController(),
40
+ resolved: false
41
+ };
42
+ entry.promise = fetch(entry.abort.signal).then((pictures) => {
43
+ entry.resolved = true;
44
+ this.keep(group, key);
45
+ return pictures;
46
+ }, (error) => {
47
+ this.drop(key, entry);
48
+ throw error;
49
+ });
50
+ entry.promise.catch(() => {});
51
+ this.entries.set(key, entry);
52
+ return this.attach(key, entry, signal);
53
+ }
54
+ /** Keep `key` as the group's newest set, letting go of the oldest beyond the few kept. */
55
+ keep(group, key) {
56
+ const kept = (this.kept.get(group) ?? []).filter((each) => each !== key);
57
+ kept.push(key);
58
+ const gone = kept.splice(0, Math.max(0, kept.length - 4));
59
+ this.kept.set(group, kept);
60
+ for (const old of gone) {
61
+ const entry = this.entries.get(old);
62
+ if (entry && entry.refs <= 0) this.entries.delete(old);
63
+ }
64
+ }
65
+ isKept(group, key) {
66
+ return this.kept.get(group)?.includes(key) ?? false;
67
+ }
68
+ attach(key, entry, signal) {
69
+ if (signal?.aborted) return Promise.reject(abortReason$1(signal));
70
+ entry.refs += 1;
71
+ if (!signal) {
72
+ const release = () => this.release(key, entry);
73
+ entry.promise.then(release, release);
74
+ return entry.promise;
75
+ }
76
+ let settled = false;
77
+ return new Promise((resolve, reject) => {
78
+ const onAbort = () => {
79
+ if (settled) return;
80
+ settled = true;
81
+ entry.refs -= 1;
82
+ if (entry.refs <= 0 && !entry.resolved) {
83
+ entry.abort.abort(abortReason$1(signal));
84
+ this.drop(key, entry);
85
+ }
86
+ reject(abortReason$1(signal));
87
+ };
88
+ signal.addEventListener("abort", onAbort, { once: true });
89
+ entry.promise.then((pictures) => {
90
+ if (settled) return;
91
+ settled = true;
92
+ this.release(key, entry);
93
+ signal.removeEventListener("abort", onAbort);
94
+ resolve(pictures);
95
+ }, (error) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ this.release(key, entry);
99
+ signal.removeEventListener("abort", onAbort);
100
+ reject(error);
101
+ });
102
+ });
103
+ }
104
+ /** A consumer let go: a set no longer kept goes with the last one. */
105
+ release(key, entry) {
106
+ entry.refs -= 1;
107
+ if (entry.refs <= 0 && entry.resolved && !this.isKept(entry.group, key)) this.drop(key, entry);
108
+ }
109
+ drop(key, entry) {
110
+ if (this.entries.get(key) === entry) this.entries.delete(key);
111
+ }
11
112
  };
12
- /** The ×2 client pyramid used for lattice deployments without a tiles block. */
13
- const DEFAULT_TILE_PYRAMID = [
14
- 1,
15
- 2,
16
- 4,
17
- 8,
18
- 16,
19
- 32
20
- ];
21
- function resolveRenderOptions(options) {
22
- const tiles = options.tiles === false ? void 0 : options.tiles;
23
- return {
24
- fullPage: {
25
- maxWidth: options.fullPage?.maxWidth ?? 640,
26
- maxWidthExplicit: options.fullPage?.maxWidth !== void 0,
27
- quantize: options.fullPage?.quantize ?? "exact"
28
- },
29
- tiles: {
30
- enabled: options.tiles !== false,
31
- size: tiles?.size ?? 512,
32
- quantize: tiles?.quantize ?? "exact",
33
- maxScale: tiles?.maxScale ?? 128,
34
- bleedPx: tiles?.bleed ?? 1,
35
- engageAt: tiles?.engageAt,
36
- prefetchMargin: tiles?.prefetch?.margin ?? .5,
37
- velocityBias: tiles?.prefetch?.velocityBias ?? true,
38
- settleMs: tiles?.settleMs ?? 150,
39
- fadeMs: tiles?.fadeMs ?? 0,
40
- fallbackPyramid: DEFAULT_TILE_PYRAMID
41
- },
42
- format: options.format,
43
- quality: options.quality,
44
- debug: options.debug ?? false
45
- };
113
+ /**
114
+ * The last pictures of each item (all its states), the newest {@link ITEMS_KEPT}
115
+ * items per family, kept after the item left its page.
116
+ */
117
+ var RecentPictures = class {
118
+ constructor() {
119
+ this.families = /* @__PURE__ */ new Map();
120
+ }
121
+ /** A fetch's pictures, grouped by item: each replaces what its item had. */
122
+ remember(family, pictures, keyOf) {
123
+ const items = this.families.get(family) ?? /* @__PURE__ */ new Map();
124
+ const byItem = /* @__PURE__ */ new Map();
125
+ for (const picture of pictures) {
126
+ const key = keyOf(picture);
127
+ byItem.set(key, [...byItem.get(key) ?? [], picture]);
128
+ }
129
+ for (const [key, itemPictures] of byItem) {
130
+ items.delete(key);
131
+ items.set(key, itemPictures);
132
+ }
133
+ while (items.size > 200) items.delete(items.keys().next().value);
134
+ this.families.set(family, items);
135
+ }
136
+ of(family, key) {
137
+ return this.families.get(family)?.get(key) ?? [];
138
+ }
139
+ };
140
+ function abortReason$1(signal) {
141
+ return signal.reason ?? new DOMException("aborted", "AbortError");
142
+ }
143
+ //#endregion
144
+ //#region src/invalidation.ts
145
+ const placedPages = (widgets) => widgets.flatMap((widget) => widget.page ? [widget.page.objectNumber] : []);
146
+ const changeOf = (pages, scope) => pages.length ? {
147
+ pages: [...new Set(pages)],
148
+ scope
149
+ } : null;
150
+ /**
151
+ * The pages a form fact repaints. Every form fact names the widgets whose
152
+ * look changed or that went (`meta.changedWidgets`), each with its page; the
153
+ * widget rows it carries, and a deleted widget's page, are counted too. An
154
+ * unplaced widget (`page: null`) has no pixels to repaint.
155
+ */
156
+ function formPagesOf(event) {
157
+ const pages = placedPages(event.meta.changedWidgets);
158
+ if ("widgets" in event) pages.push(...placedPages(event.widgets));
159
+ if ("page" in event && event.page) pages.push(event.page.objectNumber);
160
+ return pages;
161
+ }
162
+ function pixelChangeOf(event, allPageObjectNumbers) {
163
+ switch (event.type) {
164
+ case "annotations.created":
165
+ case "annotations.updated":
166
+ case "annotations.deleted":
167
+ case "annotations.restored":
168
+ case "annotations.reordered": return changeOf([event.page.objectNumber], "annotations");
169
+ case "forms.valueSet":
170
+ case "forms.created":
171
+ case "forms.updated":
172
+ case "forms.deleted":
173
+ case "forms.restored":
174
+ case "forms.widgetAdded":
175
+ case "forms.widgetRemoved":
176
+ case "forms.widgetDeleted":
177
+ case "forms.widgetRestored":
178
+ case "forms.widgetUpdated":
179
+ case "forms.widgetsReordered": return changeOf(formPagesOf(event), "fields");
180
+ case "forms.repaired": return changeOf(allPageObjectNumbers(), "fields");
181
+ case "signatures.completed": return changeOf(event.signature.widget ? placedPages([event.signature.widget]) : [], "fields");
182
+ case "redaction.applied":
183
+ case "pages.flattened": return changeOf(event.results.filter((result) => result.status === "applied").map((result) => result.page.objectNumber), "content");
184
+ case "annotations.flattened": return changeOf(event.results.some((result) => result.status === "applied") ? [event.page.objectNumber] : [], "content");
185
+ case "stream.desynced": return changeOf(allPageObjectNumbers(), "content");
186
+ default: return null;
187
+ }
188
+ }
189
+ //#endregion
190
+ //#region src/model.ts
191
+ const initialRenderState = () => ({
192
+ contentEpochs: {},
193
+ annotationEpochs: {},
194
+ fieldEpochs: {}
195
+ });
196
+ const bump = (epochs, pageObjectNumbers) => {
197
+ const next = { ...epochs };
198
+ for (const pageObjectNumber of pageObjectNumbers) next[pageObjectNumber] = (next[pageObjectNumber] ?? 0) + 1;
199
+ return next;
200
+ };
201
+ /**
202
+ * Bump the pages' versions in the ledger the scope names. A content fact
203
+ * bumps only the content ledger; every picture counts content (see
204
+ * {@link renderEpochOf}), which is how content invalidation reaches them all.
205
+ */
206
+ function invalidatePages(state, pageObjectNumbers, scope) {
207
+ if (pageObjectNumbers.length === 0) return state;
208
+ switch (scope) {
209
+ case "content": return {
210
+ ...state,
211
+ contentEpochs: bump(state.contentEpochs, pageObjectNumbers)
212
+ };
213
+ case "annotations": return {
214
+ ...state,
215
+ annotationEpochs: bump(state.annotationEpochs, pageObjectNumbers)
216
+ };
217
+ case "fields": return {
218
+ ...state,
219
+ fieldEpochs: bump(state.fieldEpochs, pageObjectNumbers)
220
+ };
221
+ }
222
+ }
223
+ /** The version of one picture of a page: its content, plus the annotations and form fields it draws. */
224
+ function renderEpochOf(state, pageObjectNumber, layers) {
225
+ return (state.contentEpochs[pageObjectNumber] ?? 0) + (layers.includeAnnotations ? state.annotationEpochs[pageObjectNumber] ?? 0 : 0) + (layers.includeFormFields ? state.fieldEpochs[pageObjectNumber] ?? 0 : 0);
46
226
  }
227
+ /** What a picture draws, as one key part: `a1f0` draws annotations and no form fields. */
228
+ const layersKeyOf = (layers) => `a${layers.includeAnnotations ? 1 : 0}f${layers.includeFormFields ? 1 : 0}`;
47
229
  //#endregion
48
230
  //#region src/raster-store.ts
49
231
  /**
50
- * Client-side mirror of the server's one-door read-through: per-key
51
- * SINGLEFLIGHT + cost-budgeted LRU over encoded page images.
232
+ * The client-side counterpart of the server's read-through cache: per-key
233
+ * singleflight + cost-budgeted LRU over encoded page images.
52
234
  *
53
- * The key is the raster's canonical identity (conformed viewport +
54
- * annotations flag + epoch — see `renderSourceKey`), so the render points do
235
+ * The key is the raster's canonical identity (for page rasters: width,
236
+ * annotations flag, epoch and format, built by `rasterKey` in the
237
+ * controller; for tiles: `TileManager`'s tile key), so the render points do
55
238
  * the caching work: every ask at one point is the same key, and
56
239
  * re-crossings (zoom 1280 → 2560 → back) resolve instantly from the LRU
57
240
  * instead of re-rendering. Epoch bumps mint new keys; stale-epoch entries
58
241
  * age out through the LRU (they are never re-requested).
59
242
  *
60
- * The budget is COST, approximately bytes: an entry costs its encoded byte
243
+ * The budget is cost, approximately bytes: an entry costs its encoded byte
61
244
  * length when the handle carries bytes (local), its pixel count as a
62
245
  * decoded-order estimate otherwise (URL-backed cloud handles), and 1 when
63
246
  * neither is known. Entry counts lie — a 512² webp tile and a BMP full page
64
247
  * differ by 100× — and long deep-zoom sessions cycle hundreds of tile keys,
65
248
  * so the budget is what actually bounds memory.
66
249
  *
67
- * Consumer aborts are REFCOUNTED: a caller abandoning a fetch (camera moved,
250
+ * Consumer aborts are refcounted: a caller abandoning a fetch (camera moved,
68
251
  * layer unmounted) detaches without killing it for other consumers; the
69
- * underlying engine call aborts only when the LAST in-flight consumer leaves.
252
+ * underlying engine call aborts only when the last in-flight consumer leaves.
70
253
  * Resolved entries are kept (evictable by LRU) — an abort after resolution
71
254
  * is a no-op by design.
72
255
  */
@@ -96,9 +279,9 @@ var RasterStore = class {
96
279
  this.total += entry.cost;
97
280
  this.evict();
98
281
  return handle;
99
- }, (err) => {
282
+ }, (error) => {
100
283
  this.drop(key, entry);
101
- throw err;
284
+ throw error;
102
285
  });
103
286
  entry.promise.catch(() => {});
104
287
  this.entries.set(key, entry);
@@ -147,12 +330,12 @@ var RasterStore = class {
147
330
  entry.refs -= 1;
148
331
  signal.removeEventListener("abort", onAbort);
149
332
  resolve(handle);
150
- }, (err) => {
333
+ }, (error) => {
151
334
  if (settled) return;
152
335
  settled = true;
153
336
  entry.refs -= 1;
154
337
  signal.removeEventListener("abort", onAbort);
155
- reject(err);
338
+ reject(error);
156
339
  });
157
340
  });
158
341
  }
@@ -164,7 +347,7 @@ var RasterStore = class {
164
347
  }
165
348
  evict() {
166
349
  if (this.total <= this.maxCost) return;
167
- const candidates = [...this.entries.entries()].filter(([, e]) => e.refs <= 0 && e.handle !== void 0).sort((a, b) => a[1].used - b[1].used);
350
+ const candidates = [...this.entries.entries()].filter(([, entry]) => entry.refs <= 0 && entry.handle !== void 0).sort((left, right) => left[1].used - right[1].used);
168
351
  for (const [key, entry] of candidates) {
169
352
  if (this.total <= this.maxCost) return;
170
353
  this.drop(key, entry);
@@ -183,11 +366,11 @@ function abortReason(signal) {
183
366
  }
184
367
  //#endregion
185
368
  //#region src/strategy.ts
186
- const ascending = (values) => [...values].sort((a, b) => a - b);
369
+ const ascending = (values) => [...values].sort((left, right) => left - right);
187
370
  /** Rung list capped by the budget — always keeps at least the smallest rung. */
188
371
  const capWidths = (widths, maxWidth) => {
189
372
  const sorted = ascending(widths);
190
- const kept = sorted.filter((w) => w <= maxWidth);
373
+ const kept = sorted.filter((width) => width <= maxWidth);
191
374
  return kept.length ? kept : sorted.slice(0, 1);
192
375
  };
193
376
  function resolveStrategy(policy, options) {
@@ -211,15 +394,16 @@ function resolveStrategy(policy, options) {
211
394
  };
212
395
  }
213
396
  /**
214
- * The ONE base-sizing function — four callers: `renderPage`,
215
- * `renderSourceKey`, `conformViewport`, and tile engagement. Exact mode
397
+ * The one base-sizing function. Its callers: the controller's
398
+ * `conformViewport` (which `renderSource` and `getSourceKey` go through)
399
+ * and tile engagement in the tile manager. Exact mode
216
400
  * returns the (settled) demand itself up to the budget; ladder mode snaps
217
- * UP and caps at the top rung.
401
+ * up and caps at the top rung.
218
402
  */
219
403
  function baseAskWidth(strategy, demandWidth) {
220
404
  const wanted = Math.max(1, Math.round(demandWidth));
221
405
  if (strategy.widths === null) return Math.min(wanted, strategy.maxWidth);
222
- return strategy.widths.find((w) => w >= wanted) ?? strategy.widths[strategy.widths.length - 1];
406
+ return strategy.widths.find((width) => width >= wanted) ?? strategy.widths[strategy.widths.length - 1];
223
407
  }
224
408
  //#endregion
225
409
  //#region src/tiles.ts
@@ -239,22 +423,22 @@ const tileSpanPt = (grid) => grid.tileSize / grid.scale;
239
423
  */
240
424
  function tilesInRect(grid, page, rect) {
241
425
  const span = tileSpanPt(grid);
242
- const x0 = Math.max(0, Math.floor(rect.x / span));
243
- const y0 = Math.max(0, Math.floor(rect.y / span));
244
- const x1 = Math.min(grid.cols - 1, Math.ceil((rect.x + rect.width) / span) - 1);
245
- const y1 = Math.min(grid.rows - 1, Math.ceil((rect.y + rect.height) / span) - 1);
246
- const out = [];
247
- for (let iy = y0; iy <= y1; iy++) for (let ix = x0; ix <= x1; ix++) out.push({
426
+ const firstColumn = Math.max(0, Math.floor(rect.x / span));
427
+ const firstRow = Math.max(0, Math.floor(rect.y / span));
428
+ const lastColumn = Math.min(grid.cols - 1, Math.ceil((rect.x + rect.width) / span) - 1);
429
+ const lastRow = Math.min(grid.rows - 1, Math.ceil((rect.y + rect.height) / span) - 1);
430
+ const coords = [];
431
+ for (let iy = firstRow; iy <= lastRow; iy++) for (let ix = firstColumn; ix <= lastColumn; ix++) coords.push({
248
432
  ix,
249
433
  iy
250
434
  });
251
- return out;
435
+ return coords;
252
436
  }
253
437
  /** Paint rect: y-down page points, clamped to the page edge. */
254
- function tilePaintRect(grid, page, c) {
438
+ function tilePaintRect(grid, page, coord) {
255
439
  const span = tileSpanPt(grid);
256
- const x = c.ix * span;
257
- const y = c.iy * span;
440
+ const x = coord.ix * span;
441
+ const y = coord.iy * span;
258
442
  return {
259
443
  x,
260
444
  y,
@@ -262,43 +446,33 @@ function tilePaintRect(grid, page, c) {
262
446
  height: Math.min(span, page.height - y)
263
447
  };
264
448
  }
265
- /** Engine target rect (y-UP PDF user space, top > bottom) for an arbitrary
266
- * y-down page-point rect. */
267
- function toEngineRect(page, rect) {
268
- return {
269
- left: rect.x,
270
- right: rect.x + rect.width,
271
- top: page.height - rect.y,
272
- bottom: page.height - (rect.y + rect.height)
273
- };
274
- }
275
449
  /**
276
- * Expand a paint rect by `pt` page points per side, clamped to the page —
277
- * the tile BLEED. Neighboring tiles rendered with bleed overlap by twice
450
+ * Expand a paint rect by `amountPt` page points per side, clamped to the page —
451
+ * the tile bleed. Neighboring tiles rendered with bleed overlap by twice
278
452
  * this amount, and the overlapping strips contain identical content (same
279
453
  * page region, same scale), so every img edge composites over its
280
454
  * neighbor's duplicated pixels instead of the backdrop: no AA hairlines,
281
455
  * no bilinear edge smear, at any CSS stretch.
282
456
  */
283
- function bleedRect(rect, pt, page) {
284
- const x = Math.max(0, rect.x - pt);
285
- const y = Math.max(0, rect.y - pt);
457
+ function bleedRect(rect, amountPt, page) {
458
+ const x = Math.max(0, rect.x - amountPt);
459
+ const y = Math.max(0, rect.y - amountPt);
286
460
  return {
287
461
  x,
288
462
  y,
289
- width: Math.min(page.width, rect.x + rect.width + pt) - x,
290
- height: Math.min(page.height, rect.y + rect.height + pt) - y
463
+ width: Math.min(page.width, rect.x + rect.width + amountPt) - x,
464
+ height: Math.min(page.height, rect.y + rect.height + amountPt) - y
291
465
  };
292
466
  }
293
467
  /**
294
- * Does a set of PAINTED want-level tiles cover `region` (y-down points)?
468
+ * Does a set of painted want-level tiles cover `region` (y-down points)?
295
469
  * The retention release check: a retained source may leave the paint list
296
470
  * once its visible footprint answers true here. Pure index arithmetic on
297
471
  * the aligned grid — the whole reason the pyramid is aligned.
298
472
  */
299
473
  function regionCovered(grid, page, region, isPainted) {
300
474
  if (region.width <= 0 || region.height <= 0) return true;
301
- for (const c of tilesInRect(grid, page, region)) if (!isPainted(c)) return false;
475
+ for (const coord of tilesInRect(grid, page, region)) if (!isPainted(coord)) return false;
302
476
  return true;
303
477
  }
304
478
  /**
@@ -308,26 +482,26 @@ function regionCovered(grid, page, region, isPainted) {
308
482
  * velocity is just data.
309
483
  */
310
484
  function inflateRect(rect, margin, velocity) {
311
- const mx = rect.width * margin;
312
- const my = rect.height * margin;
313
- let left = mx;
314
- let right = mx;
315
- let up = my;
316
- let down = my;
485
+ const marginX = rect.width * margin;
486
+ const marginY = rect.height * margin;
487
+ let left = marginX;
488
+ let right = marginX;
489
+ let up = marginY;
490
+ let down = marginY;
317
491
  if (velocity) {
318
492
  if (velocity.dx > 0) {
319
- right = mx * 2;
320
- left = mx / 2;
493
+ right = marginX * 2;
494
+ left = marginX / 2;
321
495
  } else if (velocity.dx < 0) {
322
- left = mx * 2;
323
- right = mx / 2;
496
+ left = marginX * 2;
497
+ right = marginX / 2;
324
498
  }
325
499
  if (velocity.dy > 0) {
326
- down = my * 2;
327
- up = my / 2;
500
+ down = marginY * 2;
501
+ up = marginY / 2;
328
502
  } else if (velocity.dy < 0) {
329
- up = my * 2;
330
- down = my / 2;
503
+ up = marginY * 2;
504
+ down = marginY / 2;
331
505
  }
332
506
  }
333
507
  return {
@@ -337,74 +511,70 @@ function inflateRect(rect, margin, velocity) {
337
511
  height: rect.height + up + down
338
512
  };
339
513
  }
340
- /** Snap UP through a pyramid's sorted scales; cap at the top. */
514
+ /** Snap up through a pyramid's sorted scales; cap at the top. */
341
515
  function snapToPyramid(scales, needed) {
342
- const sorted = [...scales].sort((a, b) => a - b);
343
- return sorted.find((s) => s >= needed) ?? sorted[sorted.length - 1];
516
+ const sorted = [...scales].sort((left, right) => left - right);
517
+ return sorted.find((scale) => scale >= needed) ?? sorted[sorted.length - 1];
344
518
  }
345
519
  //#endregion
346
520
  //#region src/tile-manager.ts
521
+ /** One state entry per view of a page (see {@link TileManager}'s `pages`). */
522
+ const stateKey = (view, pageObjectNumber) => `${view}\u0000${pageObjectNumber}`;
523
+ /** Backpressure: raw rasters in flight at once (render + encode transit). */
524
+ const MAX_IN_FLIGHT = 8;
525
+ /** Stage-less (no visibleRect) demand is capped to this many whole-page tiles. */
526
+ const STAGELESS_TILE_CAP = 64;
347
527
  /**
348
- * The tile retention state machine. One instance per
349
- * plugin instance (per document), all levels and pages in one place, over
350
- * the SAME RasterStore the base renders use.
528
+ * The tile retention state machine. One instance per plugin instance (per
529
+ * document), all levels and pages in one place, over the same RasterStore
530
+ * the base renders use.
351
531
  *
352
- * Invariant it enforces: every screen region paints the sharpest PAINTED
532
+ * Invariant it enforces: every screen region paints the sharpest painted
353
533
  * pixels available; quality per region only goes up until the want set
354
534
  * resolves. Mechanics:
355
- * - want vs paint: `plan()` schedules fetches for the want set (P0
356
- * visible first — center-out — then P1 prefetch when P0 is fully
357
- * resolved) and returns a paint list drawn ONLY from resolved entries —
358
- * current level AND retained older generations.
359
- * - release-on-occlusion: when a want-level tile reports PAINTED (after the
535
+ * - want vs paint: `plan()` queues the want set (visible tiles first,
536
+ * center-out, then the prefetch ring once every visible tile resolved)
537
+ * and returns a paint list drawn only from resolved entries: the
538
+ * current level and retained older generations.
539
+ * - one line for every view and page: a transit slot goes to an on-screen
540
+ * tile of any page before a tile of any prefetch ring, so a ring never
541
+ * takes the slots an on-screen tile needs. The engine ranks the tiles it
542
+ * is given by where each one is on screen.
543
+ * - release-on-occlusion: when a want-level tile reports painted (after the
360
544
  * layer's first presentation opportunity), retained sources whose
361
- * visible footprint is
362
- * covered by painted want tiles leave the paint list. Their bytes stay
363
- * in the RasterStore (demotion, not eviction) — zoom-back re-promotes
364
- * from cache.
545
+ * visible footprint is covered by painted want tiles leave the paint
546
+ * list. Their bytes stay in the RasterStore (demotion, not eviction), so
547
+ * zooming back re-promotes from cache.
365
548
  * - epoch exception: an invalidation bump means retained pixels are
366
- * WRONG — everything of the old epoch drops immediately.
549
+ * wrong, so everything of the old epoch drops immediately.
367
550
  *
368
- * Levels come from the resolved STRATEGY (policy ∧ options): a pyramid
369
- * under a lattice (or the opt-in client ladder), the EXACT settled scale
370
- * under exact mode — where the level identity is the demand's device width
551
+ * Levels come from the resolved strategy (policy ∧ options): a pyramid
552
+ * under a lattice (or the opt-in client ladder), the exact settled scale
553
+ * under exact mode, where the level identity is the demand's device width
371
554
  * across the page, so keys stay integer and stable. The retention/coverage
372
555
  * math is generic over any mix of retained scales.
373
556
  *
374
- * `plan()` is called from selectors: it MUST NOT dispatch. Fetch kickoff
375
- * is idempotent (the store singleflights); resolution handlers dispatch
376
- * the wake-up (`onAdvance`) that makes subscribed layers recompute.
557
+ * `plan()` never wakes readers itself: its callers do, once, when the
558
+ * returned plan changed. Fetch kickoff is idempotent (the store
559
+ * singleflights); resolution handlers call `onAdvance`, which re-plans the
560
+ * page and wakes subscribed layers, and hand the freed slot on.
377
561
  */
378
- /** One state entry per VIEW-of-page (see the class doc). */
379
- const stateKey = (view, pon) => `${view}\u0000${pon}`;
380
- /** Backpressure: raw rasters in flight at once (render + encode transit). */
381
- const MAX_IN_FLIGHT = 8;
382
- /** Stage-less (no visibleRect) demand is capped to this many whole-page tiles. */
383
- const STAGELESS_TILE_CAP = 64;
384
562
  var TileManager = class {
385
563
  constructor(deps) {
386
564
  this.deps = deps;
387
565
  this.pages = /* @__PURE__ */ new Map();
388
- this.strategyMemo = null;
566
+ this.strategy = (0, _embedpdf_core.memo)(() => [this.deps.getPolicy(), this.deps.getOptions()], (policy, options) => resolveStrategy(policy, options));
389
567
  this.inFlight = 0;
390
568
  this.warnedStageless = false;
391
569
  }
392
- strategy() {
393
- const policy = this.deps.getPolicy();
394
- if (this.strategyMemo?.policy !== policy) this.strategyMemo = {
395
- policy,
396
- strategy: resolveStrategy(policy, this.deps.options)
397
- };
398
- return this.strategyMemo.strategy;
399
- }
400
- plan(view, pon, demand, includeAnnotations) {
401
- const { options } = this.deps;
402
- if (!options.tiles.enabled) return EMPTY_TILE_PLAN;
570
+ plan(view, pageObjectNumber, demand, layers) {
571
+ const options = this.deps.getOptions();
572
+ if (!options.tiles.enabled) return require_contract.EMPTY_TILE_PLAN;
403
573
  const strategy = this.strategy();
404
- const page = this.deps.getPageSize(pon);
405
- if (!page) return EMPTY_TILE_PLAN;
406
- const epoch = this.deps.getEpoch(pon, includeAnnotations);
407
- const state = this.pageState(pon, view);
574
+ const page = this.deps.getPageSize(pageObjectNumber);
575
+ if (!page) return require_contract.EMPTY_TILE_PLAN;
576
+ const epoch = this.deps.getEpoch(pageObjectNumber, layers);
577
+ const state = this.pageState(pageObjectNumber, view);
408
578
  if (state.epoch !== epoch) {
409
579
  this.abortAll(state);
410
580
  state.entries.clear();
@@ -418,15 +588,16 @@ var TileManager = class {
418
588
  const deficit = demand.desiredDeviceWidth / supplied;
419
589
  if (deficit <= strategy.engageAt) {
420
590
  if (state.entries.size || state.wantScale !== null) {
421
- this.deps.debug?.(`disengage pon=${pon} (deficit ${deficit.toFixed(2)})`);
591
+ this.deps.debug?.(`disengage page=${pageObjectNumber} (deficit ${deficit.toFixed(2)})`);
422
592
  this.abortAll(state);
423
593
  state.entries.clear();
424
594
  state.failedKeys.clear();
425
595
  state.wantScale = null;
426
596
  state.wantWidth = null;
427
597
  state.planCache = null;
598
+ this.pump();
428
599
  }
429
- return EMPTY_TILE_PLAN;
600
+ return require_contract.EMPTY_TILE_PLAN;
430
601
  }
431
602
  let wantWidth;
432
603
  let wantScale;
@@ -445,7 +616,7 @@ var TileManager = class {
445
616
  console.warn(`[render] stage-less tile demand of ${Math.round(demand.desiredDeviceWidth)} device px would tile the whole page — clamped to ${maxStagelessWidth}. Supply a visibleRect (host the page in a Stage) for sharp deep zoom.`);
446
617
  }
447
618
  if (strategy.pyramid) {
448
- const fitting = strategy.pyramid.filter((s) => Math.round(s * page.width) <= maxStagelessWidth);
619
+ const fitting = strategy.pyramid.filter((scale) => Math.round(scale * page.width) <= maxStagelessWidth);
449
620
  wantScale = fitting.length ? fitting[fitting.length - 1] : strategy.pyramid[0];
450
621
  wantWidth = Math.round(wantScale * page.width);
451
622
  } else {
@@ -462,12 +633,12 @@ var TileManager = class {
462
633
  height: page.height
463
634
  };
464
635
  state.lastVisible = visible;
465
- const p0 = tilesInRect(grid, page, visible);
636
+ const visibleCoords = tilesInRect(grid, page, visible);
466
637
  const ring = inflateRect(visible, options.tiles.prefetchMargin, options.tiles.velocityBias ? demand.velocity : void 0);
467
- const p0Keys = new Set(p0.map((c) => coordKey(c)));
468
- const p1 = tilesInRect(grid, page, ring).filter((c) => !p0Keys.has(coordKey(c)));
469
- this.schedule(pon, state, page, grid, wantScale, wantWidth, includeAnnotations, epoch, p0, p1, visible);
470
- if (this.releaseCovered(pon, state, page, includeAnnotations)) {
638
+ const visibleKeys = new Set(visibleCoords.map((coord) => coordKey(coord)));
639
+ const prefetchCoords = tilesInRect(grid, page, ring).filter((coord) => !visibleKeys.has(coordKey(coord)));
640
+ this.schedule(pageObjectNumber, state, page, grid, wantScale, wantWidth, layers, epoch, visibleCoords, prefetchCoords, visible);
641
+ if (this.releaseCovered(pageObjectNumber, state, page, layers)) {
471
642
  state.version += 1;
472
643
  state.planCache = null;
473
644
  }
@@ -476,11 +647,11 @@ var TileManager = class {
476
647
  const paint = [];
477
648
  const fetching = [];
478
649
  const stale = [];
479
- const bleedPx = this.deps.options.tiles.bleedPx;
650
+ const bleedPx = this.deps.getOptions().tiles.bleedPx;
480
651
  for (const entry of state.entries.values()) {
481
- const visIntersect = (0, _embedpdf_core_geometry.intersectRects)(entry.rect, visible);
652
+ const visiblePart = (0, _embedpdf_core_geometry.intersectRects)(entry.rect, visible);
482
653
  if (entry.resolved) {
483
- if (visIntersect.width > 0 && visIntersect.height > 0) {
654
+ if (visiblePart.width > 0 && visiblePart.height > 0) {
484
655
  const handle = this.deps.store.peek(entry.key);
485
656
  if (!handle) {
486
657
  stale.push(entry.key);
@@ -498,9 +669,10 @@ var TileManager = class {
498
669
  }
499
670
  for (const key of stale) state.entries.delete(key);
500
671
  const rank = /* @__PURE__ */ new Map();
501
- for (const s of [...new Set(paint.map((p) => p.scale))].sort((a, b) => a - b)) rank.set(s, rank.size);
502
- for (const p of paint) p.z = rank.get(p.scale);
503
- paint.sort((a, b) => a.z - b.z || a.key.localeCompare(b.key));
672
+ const scales = [...new Set(paint.map((source) => source.scale))];
673
+ for (const scale of scales.sort((left, right) => left - right)) rank.set(scale, rank.size);
674
+ for (const source of paint) source.z = rank.get(source.scale);
675
+ paint.sort((left, right) => left.z - right.z || left.key.localeCompare(right.key));
504
676
  const plan = {
505
677
  engaged: true,
506
678
  paint,
@@ -515,41 +687,46 @@ var TileManager = class {
515
687
  return plan;
516
688
  }
517
689
  /** The layer's painted report: this key's pixels had a presentation opportunity. */
518
- sourcePainted(view, pon, key) {
519
- const state = this.pages.get(stateKey(view, pon));
690
+ sourcePainted(view, pageObjectNumber, key) {
691
+ const state = this.pages.get(stateKey(view, pageObjectNumber));
520
692
  const entry = state?.entries.get(key);
521
693
  if (!state || !entry || entry.painted) return;
522
694
  entry.painted = true;
523
- const page = this.deps.getPageSize(pon);
524
- if (page) this.releaseCovered(pon, state, page, annotationsOf(key));
695
+ const page = this.deps.getPageSize(pageObjectNumber);
696
+ if (page) this.releaseCovered(pageObjectNumber, state, page, layersOfKey(key));
525
697
  state.version += 1;
526
698
  state.planCache = null;
527
- this.deps.onAdvance(pon);
699
+ this.deps.onAdvance(pageObjectNumber);
528
700
  }
529
701
  /**
530
702
  * The inverse report: this key's <img> left the DOM (pan-away, plan drop),
531
- * so its pixels are NOT currently compositable. Without this, a tile that
703
+ * so its pixels are not currently compositable. Without this, a tile that
532
704
  * unmounts and later remounts is still counted as painted while its new
533
705
  * <img> re-decodes — and an adjacent fresh `sourcePainted` could release
534
706
  * retained coarse coverage over a region that momentarily has no sharp
535
- * pixels. Painted is a statement about the SCREEN, so it follows the DOM.
707
+ * pixels. Painted is a statement about the screen, so it follows the DOM.
536
708
  */
537
- sourceUnpainted(view, pon, key) {
538
- const entry = this.pages.get(stateKey(view, pon))?.entries.get(key);
709
+ sourceUnpainted(view, pageObjectNumber, key) {
710
+ const entry = this.pages.get(stateKey(view, pageObjectNumber))?.entries.get(key);
539
711
  if (entry) entry.painted = false;
540
712
  }
541
- /** A lens unmounted its tile plane: stop fetching, drop bookkeeping.
542
- * Resolved bytes stay in the RasterStore for a re-mount. */
543
- releasePage(view, pon) {
544
- const state = this.pages.get(stateKey(view, pon));
713
+ /** A lens stopped painting tiles for the page (it left the view, or its
714
+ * layer paints none): stop fetching, drop bookkeeping. Resolved bytes stay
715
+ * in the RasterStore for a re-mount. */
716
+ releasePage(view, pageObjectNumber) {
717
+ const state = this.pages.get(stateKey(view, pageObjectNumber));
545
718
  if (!state) return;
546
719
  this.abortAll(state);
547
- this.pages.delete(stateKey(view, pon));
720
+ this.pages.delete(stateKey(view, pageObjectNumber));
721
+ this.pump();
548
722
  }
549
- pageState(pon, view) {
550
- let state = this.pages.get(stateKey(view, pon));
723
+ pageState(pageObjectNumber, view) {
724
+ let state = this.pages.get(stateKey(view, pageObjectNumber));
551
725
  if (!state) {
552
726
  state = {
727
+ view,
728
+ pageObjectNumber,
729
+ queue: null,
553
730
  epoch: -1,
554
731
  wantScale: null,
555
732
  wantWidth: null,
@@ -557,22 +734,23 @@ var TileManager = class {
557
734
  failedKeys: /* @__PURE__ */ new Set(),
558
735
  version: 0,
559
736
  planCache: null,
560
- settleTimer: null,
737
+ cancelSettle: null,
561
738
  pendingLevel: null,
562
739
  lastVisible: null
563
740
  };
564
- this.pages.set(stateKey(view, pon), state);
741
+ this.pages.set(stateKey(view, pageObjectNumber), state);
565
742
  }
566
743
  return state;
567
744
  }
568
- schedule(pon, state, page, grid, wantScale, wantWidth, includeAnnotations, epoch, p0, p1, visible) {
569
- const { options } = this.deps;
745
+ schedule(pageObjectNumber, state, page, grid, wantScale, wantWidth, layers, epoch, visibleCoords, prefetchCoords, visible) {
746
+ const options = this.deps.getOptions();
570
747
  const firstEngage = state.wantWidth === null;
571
748
  const levelChanged = state.wantWidth !== null && state.wantWidth !== wantWidth;
572
749
  if (levelChanged) state.failedKeys.clear();
573
750
  state.wantScale = wantScale;
574
751
  state.wantWidth = wantWidth;
575
- const wanted = new Set([...p0, ...p1].map((c) => this.tileKey(pon, wantWidth, c, includeAnnotations, epoch)));
752
+ const keyOf = (coord) => this.tileKey(pageObjectNumber, wantWidth, coord, layers, epoch);
753
+ const wanted = new Set([...visibleCoords, ...prefetchCoords].map(keyOf));
576
754
  for (const [key, entry] of state.entries) {
577
755
  if (wanted.has(key)) continue;
578
756
  if (!entry.resolved) {
@@ -581,110 +759,154 @@ var TileManager = class {
581
759
  state.entries.delete(key);
582
760
  } else if (entry.scale === wantScale) state.entries.delete(key);
583
761
  }
584
- const cx = visible.x + visible.width / 2;
585
- const cy = visible.y + visible.height / 2;
762
+ const centerX = visible.x + visible.width / 2;
763
+ const centerY = visible.y + visible.height / 2;
586
764
  const span = grid.tileSize / grid.scale;
587
- const orderedP0 = [...p0].sort((a, b) => {
588
- const da = (a.ix + .5) * span - cx;
589
- const db = (b.ix + .5) * span - cx;
590
- const ea = (a.iy + .5) * span - cy;
591
- const eb = (b.iy + .5) * span - cy;
592
- return da * da + ea * ea - (db * db + eb * eb);
593
- });
765
+ const distanceToCenter = (coord) => {
766
+ const dx = (coord.ix + .5) * span - centerX;
767
+ const dy = (coord.iy + .5) * span - centerY;
768
+ return dx * dx + dy * dy;
769
+ };
770
+ const orderedVisible = [...visibleCoords].sort((left, right) => distanceToCenter(left) - distanceToCenter(right));
594
771
  const kickoff = () => {
595
- if (this.ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, orderedP0)) this.ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, p1);
772
+ state.queue = {
773
+ coords: orderedVisible.every((coord) => {
774
+ const key = keyOf(coord);
775
+ return state.failedKeys.has(key) || state.entries.get(key)?.resolved === true;
776
+ }) ? [...orderedVisible, ...prefetchCoords] : orderedVisible,
777
+ ringFrom: orderedVisible.length,
778
+ next: 0,
779
+ page,
780
+ grid,
781
+ wantWidth,
782
+ layers,
783
+ epoch
784
+ };
785
+ this.pump();
596
786
  };
597
787
  if (levelChanged && options.tiles.settleMs > 0) {
598
- this.deps.debug?.(`arm settle pon=${pon} level=${wantWidth}`);
788
+ this.deps.debug?.(`arm settle page=${pageObjectNumber} level=${wantWidth}`);
789
+ state.queue = null;
599
790
  state.pendingLevel = wantWidth;
600
- if (state.settleTimer !== null) clearTimeout(state.settleTimer);
601
- state.settleTimer = setTimeout(() => {
602
- state.settleTimer = null;
603
- this.deps.debug?.(`settle fired pon=${pon} level=${wantWidth} current=${state.pendingLevel === wantWidth}`);
791
+ state.cancelSettle?.();
792
+ state.cancelSettle = this.deps.after(options.tiles.settleMs, () => {
793
+ state.cancelSettle = null;
794
+ this.deps.debug?.(`settle fired page=${pageObjectNumber} level=${wantWidth} current=${state.pendingLevel === wantWidth}`);
604
795
  if (state.pendingLevel === wantWidth) kickoff();
605
- }, options.tiles.settleMs);
796
+ });
606
797
  return;
607
798
  }
608
- if (state.settleTimer !== null && !levelChanged && !firstEngage) {
609
- clearTimeout(state.settleTimer);
610
- state.settleTimer = null;
799
+ if (state.cancelSettle !== null && !levelChanged && !firstEngage) {
800
+ state.cancelSettle();
801
+ state.cancelSettle = null;
611
802
  state.pendingLevel = null;
612
803
  }
613
804
  kickoff();
614
805
  }
615
- /** Start missing fetches; true when every coord is already resolved. */
616
- ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, coords) {
617
- let allReady = true;
618
- let started = 0;
619
- const bleedPt = this.deps.options.tiles.bleedPx / grid.scale;
620
- for (const coord of coords) {
621
- const key = this.tileKey(pon, wantWidth, coord, includeAnnotations, epoch);
622
- if (state.failedKeys.has(key)) continue;
623
- const existing = state.entries.get(key);
624
- if (existing) {
625
- if (!existing.resolved) allReady = false;
626
- continue;
806
+ /**
807
+ * Starts queued tiles while transit slots are free: the first on-screen
808
+ * tile of any view and page, in the order their queues were set, before
809
+ * any tile of a prefetch ring. Within one view of a page, its queue's order
810
+ * holds. Backpressure bounds raw rasters in transit (render + encode); each
811
+ * resolution hands its slot on through here, so no other pump is needed.
812
+ */
813
+ pump() {
814
+ const started = /* @__PURE__ */ new Map();
815
+ while (this.inFlight < MAX_IN_FLIGHT) {
816
+ let next = null;
817
+ for (const state of this.pages.values()) {
818
+ const queued = this.nextToStart(state);
819
+ if (!queued || next && queued.prefetch) continue;
820
+ next = {
821
+ state,
822
+ coord: queued.coord
823
+ };
824
+ if (!queued.prefetch) break;
627
825
  }
628
- allReady = false;
629
- if (this.inFlight >= MAX_IN_FLIGHT) continue;
630
- this.inFlight += 1;
631
- started += 1;
632
- const abort = new AbortController();
633
- const logical = tilePaintRect(grid, page, coord);
634
- const entry = {
635
- key,
636
- scale: grid.scale,
826
+ if (!next) break;
827
+ this.start(next.state, next.coord);
828
+ started.set(next.state, (started.get(next.state) ?? 0) + 1);
829
+ }
830
+ for (const [state, count] of started) this.deps.debug?.(`fetch page=${state.pageObjectNumber} level=${state.queue?.wantWidth} +${count} tiles`);
831
+ }
832
+ /** The first tile of the state's queue not started yet, nor failed at this level. */
833
+ nextToStart(state) {
834
+ const queue = state.queue;
835
+ if (!queue) return null;
836
+ for (; queue.next < queue.coords.length; queue.next += 1) {
837
+ const coord = queue.coords[queue.next];
838
+ const key = this.tileKey(state.pageObjectNumber, queue.wantWidth, coord, queue.layers, queue.epoch);
839
+ if (state.failedKeys.has(key) || state.entries.has(key)) continue;
840
+ return {
637
841
  coord,
638
- rect: logical,
639
- resolved: false,
640
- painted: false,
641
- charged: true,
642
- abort
842
+ prefetch: queue.next >= queue.ringFrom
643
843
  };
644
- state.entries.set(key, entry);
645
- this.deps.store.acquire(key, (signal) => this.deps.fetchTile(pon, toEngineRect(page, bleedPt > 0 ? bleedRect(logical, bleedPt, page) : logical), grid.scale, includeAnnotations, signal), abort.signal).then(() => {
646
- this.releaseSlot(entry);
647
- if (state.entries.get(key) !== entry) return;
844
+ }
845
+ return null;
846
+ }
847
+ /** Starts one queued tile's fetch, in a transit slot. */
848
+ start(state, coord) {
849
+ const { page, grid, wantWidth, layers, epoch } = state.queue;
850
+ const pageObjectNumber = state.pageObjectNumber;
851
+ const key = this.tileKey(pageObjectNumber, wantWidth, coord, layers, epoch);
852
+ const bleedPt = this.deps.getOptions().tiles.bleedPx / grid.scale;
853
+ this.inFlight += 1;
854
+ const abort = new AbortController();
855
+ const logical = tilePaintRect(grid, page, coord);
856
+ const entry = {
857
+ key,
858
+ scale: grid.scale,
859
+ coord,
860
+ rect: logical,
861
+ resolved: false,
862
+ painted: false,
863
+ charged: true,
864
+ abort
865
+ };
866
+ state.entries.set(key, entry);
867
+ state.planCache = null;
868
+ this.deps.store.acquire(key, (signal) => this.deps.fetchTile(state.view, pageObjectNumber, bleedPt > 0 ? bleedRect(logical, bleedPt, page) : logical, grid.scale, layers, signal), abort.signal).then(() => {
869
+ this.releaseSlot(entry);
870
+ if (state.entries.get(key) === entry) {
648
871
  entry.resolved = true;
649
872
  state.version += 1;
650
873
  state.planCache = null;
651
- this.deps.onAdvance(pon);
652
- }, (err) => {
653
- this.releaseSlot(entry);
654
- if (state.entries.get(key) !== entry) return;
874
+ this.deps.onAdvance(pageObjectNumber);
875
+ }
876
+ this.pump();
877
+ }, (error) => {
878
+ this.releaseSlot(entry);
879
+ if (state.entries.get(key) === entry) {
655
880
  state.entries.delete(key);
656
881
  if (!abort.signal.aborted) {
657
882
  state.failedKeys.add(key);
658
- this.deps.debug?.(`tile failed ${key}: ${String(err)}`);
883
+ this.deps.debug?.(`tile failed ${key}: ${String(error)}`);
659
884
  state.version += 1;
660
885
  state.planCache = null;
661
- this.deps.onAdvance(pon);
886
+ this.deps.onAdvance(pageObjectNumber);
662
887
  }
663
- });
664
- }
665
- if (started > 0) this.deps.debug?.(`fetch pon=${pon} level=${wantWidth} +${started} tiles`);
666
- return allReady;
888
+ }
889
+ this.pump();
890
+ });
667
891
  }
668
892
  /**
669
- * The release rule, evaluated as a PURE FUNCTION of the current painted
670
- * set: retained sources (any non-want level) whose VISIBLE footprint is
671
- * covered by PAINTED want-level tiles leave the paint list. Runs on every
893
+ * The release rule, evaluated as a pure function of the current painted
894
+ * set: retained sources (any non-want level) whose visible footprint is
895
+ * covered by painted want-level tiles leave the paint list. Runs on every
672
896
  * `plan()` and on every painted report — never only "against the tile
673
- * that just painted": release must not depend on paint ORDER (center-out
897
+ * that just painted": release must not depend on paint order (center-out
674
898
  * scheduling means the report that completes a retained tile's coverage
675
899
  * routinely lands far away from it), and re-evaluating from current state
676
900
  * also self-heals any painted-flag transition the reports raced past.
677
- * Coverage is index arithmetic over the WANT grid — generic over whatever
901
+ * Coverage is index arithmetic over the want grid — generic over whatever
678
902
  * mix of retained scales exists (exact levels included).
679
903
  */
680
- releaseCovered(pon, state, page, includeAnnotations) {
904
+ releaseCovered(pageObjectNumber, state, page, layers) {
681
905
  const { wantScale, wantWidth } = state;
682
906
  if (wantScale === null || wantWidth === null) return false;
683
907
  const grid = tileGrid(page, wantScale, this.strategy().tileSize);
684
- const paintedAt = (c) => {
685
- const key = this.tileKey(pon, wantWidth, c, includeAnnotations, state.epoch);
686
- return state.entries.get(key)?.painted === true;
687
- };
908
+ const wantKeyOf = (coord) => this.tileKey(pageObjectNumber, wantWidth, coord, layers, state.epoch);
909
+ const paintedAt = (coord) => state.entries.get(wantKeyOf(coord))?.painted === true;
688
910
  let released = false;
689
911
  for (const [key, entry] of state.entries) {
690
912
  if (entry.scale === wantScale || !entry.resolved) continue;
@@ -692,13 +914,12 @@ var TileManager = class {
692
914
  if (regionCovered(grid, page, region, paintedAt)) {
693
915
  state.entries.delete(key);
694
916
  released = true;
695
- } else if (this.deps.debug) {
696
- const missing = tilesInRect(grid, page, region).filter((c) => !paintedAt(c));
697
- const detail = missing.slice(0, 3).map((c) => {
698
- const k = this.tileKey(pon, wantWidth, c, includeAnnotations, state.epoch);
699
- const e = state.entries.get(k);
700
- const st = !e ? "NO-ENTRY" : !e.resolved ? "PENDING" : "RESOLVED-unpainted";
701
- return `${c.ix},${c.iy}=${st}`;
917
+ } else if (this.deps.debug && this.deps.getOptions().debug) {
918
+ const missing = tilesInRect(grid, page, region).filter((coord) => !paintedAt(coord));
919
+ const detail = missing.slice(0, 3).map((coord) => {
920
+ const missingEntry = state.entries.get(wantKeyOf(coord));
921
+ const status = !missingEntry ? "NO-ENTRY" : !missingEntry.resolved ? "PENDING" : "RESOLVED-unpainted";
922
+ return `${coord.ix},${coord.iy}=${status}`;
702
923
  });
703
924
  this.deps.debug(`retained ${key} blocked by: ${detail.join(" ")} (${missing.length} missing)`);
704
925
  }
@@ -706,23 +927,22 @@ var TileManager = class {
706
927
  return released;
707
928
  }
708
929
  /** Test/diagnostic introspection: bookkeeping size for one page. */
709
- stats(view, pon) {
930
+ stats(view, pageObjectNumber) {
710
931
  return {
711
- entries: this.pages.get(stateKey(view, pon))?.entries.size ?? 0,
932
+ entries: this.pages.get(stateKey(view, pageObjectNumber))?.entries.size ?? 0,
712
933
  inFlight: this.inFlight
713
934
  };
714
935
  }
715
- /** Idempotent transit-slot release — the ONE place inFlight decrements. */
936
+ /** Idempotent transit-slot release — the one place inFlight decrements. */
716
937
  releaseSlot(entry) {
717
938
  if (!entry.charged) return;
718
939
  entry.charged = false;
719
940
  this.inFlight -= 1;
720
941
  }
721
942
  abortAll(state) {
722
- if (state.settleTimer !== null) {
723
- clearTimeout(state.settleTimer);
724
- state.settleTimer = null;
725
- }
943
+ state.queue = null;
944
+ state.cancelSettle?.();
945
+ state.cancelSettle = null;
726
946
  for (const entry of state.entries.values()) if (!entry.resolved) {
727
947
  entry.abort?.abort();
728
948
  this.releaseSlot(entry);
@@ -730,117 +950,306 @@ var TileManager = class {
730
950
  }
731
951
  /** Level identity is the integer device width across the page — integer
732
952
  * and stable for pyramid rungs and exact levels alike. The key also
733
- * carries everything that determines the fetched BITMAP for a coord
953
+ * carries everything that determines the fetched bitmap for a coord
734
954
  * (tile size, bleed, encode format): a raster's identity must include
735
955
  * its geometry, or a cached handle from one configuration could be
736
956
  * stretched into another's rect. Format genuinely varies at runtime —
737
957
  * the cloud policy arrives async after open and can change the resolved
738
958
  * format under a live store. */
739
- tileKey(pon, levelWidth, c, includeAnnotations, epoch) {
740
- const { tiles } = this.deps.options;
959
+ tileKey(pageObjectNumber, levelWidth, coord, layers, epoch) {
960
+ const { tiles } = this.deps.getOptions();
741
961
  const format = this.strategy().format;
742
962
  const geometry = `g${tiles.size}.${tiles.bleedPx}${format ? `.${format}` : ""}`;
743
- return `t:${pon}|w${levelWidth}|${c.ix},${c.iy}|a${includeAnnotations ? 1 : 0}|e${epoch}|${geometry}`;
963
+ return `t:${pageObjectNumber}|w${levelWidth}|${coord.ix},${coord.iy}|${layersKeyOf(layers)}|e${epoch}|${geometry}`;
744
964
  }
745
965
  };
746
- const coordKey = (c) => `${c.ix},${c.iy}`;
747
- const rectKey = (r) => `${Math.round(r.x)},${Math.round(r.y)},${Math.round(r.width)},${Math.round(r.height)}`;
748
- const annotationsOf = (key) => key.includes("|a1|");
966
+ const coordKey = (coord) => `${coord.ix},${coord.iy}`;
967
+ const rectKey = (rect) => `${Math.round(rect.x)},${Math.round(rect.y)},${Math.round(rect.width)},${Math.round(rect.height)}`;
968
+ /** What a tile key's picture draws, as {@link layersKeyOf} wrote it into the key. */
969
+ const layersOfKey = (key) => {
970
+ const drawn = /\|a([01])f([01])\|/.exec(key);
971
+ return {
972
+ includeAnnotations: drawn?.[1] === "1",
973
+ includeFormFields: drawn?.[2] === "1"
974
+ };
975
+ };
749
976
  //#endregion
750
- //#region src/capability.ts
977
+ //#region src/controller.ts
978
+ const RENDER_SCOPE = "doc.render";
979
+ const FORMS_SCOPE = "doc.forms.read";
980
+ const ANNOTATIONS_SCOPE = "doc.annotate.read";
981
+ /** Every answer `getLayerRights()` gives, one object each, so the same rights are the same object. */
982
+ const LAYER_RIGHTS = [false, true].map((annotations) => [false, true].map((formFields) => Object.freeze({
983
+ annotations,
984
+ formFields
985
+ })));
986
+ /** An abort reason the engine task accepts (signals carry `unknown`). */
987
+ const reasonOf = (signal) => typeof signal.reason === "string" ? signal.reason : void 0;
988
+ const DEFAULT_BATCH_CONCURRENCY = 4;
751
989
  /**
752
- * The render capability: the ONE place STRATEGY (plugin options — what the
753
- * viewer spends) composes with POLICY (the engine fact — what the deployment
754
- * serves). The engine never snaps; framework layers never see either;
755
- * everything between — conforming demand to the resolved render points,
756
- * collapsing same-key asks in the raster store, exposing stable source
757
- * keys — happens here.
990
+ * The render controller: the one place strategy (config: what the viewer
991
+ * spends) composes with policy (the engine fact: what the deployment
992
+ * serves). The engine never snaps and layers never see either; everything
993
+ * between happens here: conforming demand to the resolved render points,
994
+ * collapsing same-key asks in the raster store, exposing stable source keys,
995
+ * and turning a view's demand into a retention-safe tile plan.
996
+ *
997
+ * Two doors for rasters: `renderPage` (public, exact size) and
998
+ * `renderSource` (host, conformed). Both share the raster store, so a
999
+ * developer's thumbnail and the rail's base plane never render twice.
1000
+ *
1001
+ * Every render it asks for is the picture of a page itself, so it asks the
1002
+ * engine with `priority: 'high'`, naming the view it's for when it knows it:
1003
+ * the engine runs it before anything else about the same place (annotation
1004
+ * appearances, form fields, text geometry, links).
1005
+ *
1006
+ * Two doors for staleness: confirmed document events (see `connect`) and
1007
+ * the `invalidate` verb. Both go through `publishInvalidation`, the one
1008
+ * place the ledger bumps and `onInvalidated` fires.
1009
+ *
1010
+ * The tile manager and the per-view plans live outside state (they hold
1011
+ * live handles and abort controllers); a re-plan wakes readers with
1012
+ * `ctx.notify()`.
758
1013
  */
759
- function createRenderCapability(ctx, options = {}) {
760
- const resolved = resolveRenderOptions(options);
761
- const canRender = () => ctx.doc?.security.allows("doc.render") ?? false;
1014
+ function createRenderController(ctx) {
1015
+ const settings = ctx.settings();
1016
+ const resolvedOptions = (0, _embedpdf_core.memo)(() => [settings.get()], (current) => require_contract.resolveRenderOptions(current));
762
1017
  const store = new RasterStore();
763
- const policy = () => ctx.document()?.renderPolicy ?? _embedpdf_core.CONTINUOUS_RENDER_POLICY;
764
- let strategyMemo = null;
765
- const strategy = () => {
766
- const p = policy();
767
- if (strategyMemo?.policy !== p) {
768
- strategyMemo = {
769
- policy: p,
770
- strategy: resolveStrategy(p, resolved)
771
- };
772
- warnDroppedFormat(strategyMemo.strategy);
773
- }
774
- return strategyMemo.strategy;
1018
+ const appearances = new AppearanceStore();
1019
+ const recentAppearances = new RecentPictures();
1020
+ const invalidated = ctx.events.source();
1021
+ const renderCompleted = ctx.events.source();
1022
+ const renderFailed = ctx.events.source();
1023
+ const canRender = () => ctx.allows(RENDER_SCOPE);
1024
+ const rights = () => LAYER_RIGHTS[ctx.allows("doc.annotate.read") ? 1 : 0][ctx.allows("doc.forms.read") ? 1 : 0];
1025
+ const layersOf = (options) => {
1026
+ const may = rights();
1027
+ return (0, _embedpdf_core.resolvePageLayers)({
1028
+ ...options.includeAnnotations !== void 0 && (may.annotations || !options.includeAnnotations) ? { includeAnnotations: options.includeAnnotations } : {},
1029
+ ...options.includeFormFields !== void 0 && (may.formFields || !options.includeFormFields) ? { includeFormFields: options.includeFormFields } : {}
1030
+ }, may);
1031
+ };
1032
+ const pictureLayers = (options, operation) => {
1033
+ if (options.includeAnnotations === true) ctx.assertAllowed("doc.annotate.read", operation);
1034
+ if (options.includeFormFields === true) ctx.assertAllowed(FORMS_SCOPE, operation);
1035
+ return layersOf(options);
775
1036
  };
776
1037
  let warnedFormat = false;
777
- const warnDroppedFormat = (s) => {
778
- if (warnedFormat || resolved.format === void 0 || s.format === resolved.format) return;
1038
+ const warnDroppedFormat = (strategy, requested) => {
1039
+ if (warnedFormat || requested === void 0 || strategy.format === requested) return;
779
1040
  warnedFormat = true;
780
- console.warn(`[render] format '${resolved.format}' is not in the deployment's formats — using '${s.format}'.`);
1041
+ console.warn(`[render] format '${requested}' is not in the deployment's formats — using '${strategy.format}'.`);
781
1042
  };
782
1043
  let warnedBudget = false;
783
1044
  const warnPastBudgetNoTiles = (demandWidth, supplied) => {
784
- if (warnedBudget || resolved.tiles.enabled || demandWidth <= supplied * 2) return;
1045
+ if (warnedBudget || resolvedOptions().tiles.enabled || demandWidth <= supplied * 2) return;
785
1046
  warnedBudget = true;
786
1047
  console.warn(`[render] demand is ${(demandWidth / supplied).toFixed(1)}× over the base budget and the tile plane is disabled (tiles: false) — the page rests blurry. Raise fullPage.maxWidth or re-enable tiles.`);
787
1048
  };
788
- const pageOf = (pon) => (ctx.document()?.pages ?? []).find((p) => p.pageObjectNumber === pon);
789
- const pageWidthOf = (pon) => {
790
- const page = pageOf(pon);
791
- if (!page) throw new Error(`render: unknown page object number ${pon}`);
792
- return page.size.width;
1049
+ const renderPolicy = () => ctx.document()?.renderPolicy ?? _embedpdf_core.CONTINUOUS_RENDER_POLICY;
1050
+ const currentStrategy = (0, _embedpdf_core.memo)(() => [renderPolicy(), resolvedOptions()], (policy, options) => {
1051
+ const strategy = resolveStrategy(policy, options);
1052
+ warnDroppedFormat(strategy, options.format);
1053
+ return strategy;
1054
+ });
1055
+ const pageWidthOf = (page) => {
1056
+ const info = ctx.getPage(page);
1057
+ if (!info) ctx.assertPageRef(page);
1058
+ return info.size.width;
793
1059
  };
794
- const conformViewport = (pon, scale) => {
795
- const s = strategy();
796
- const demandWidth = scale * pageWidthOf(pon);
797
- const width = baseAskWidth(s, demandWidth);
1060
+ const allPageObjectNumbers = () => (ctx.document()?.pages ?? []).map((info) => info.ref.objectNumber);
1061
+ const conformViewport = (page, scale) => {
1062
+ const demandWidth = scale * pageWidthOf(page);
1063
+ const width = baseAskWidth(currentStrategy(), demandWidth);
798
1064
  warnPastBudgetNoTiles(demandWidth, width);
799
1065
  return {
800
1066
  kind: "width",
801
1067
  width
802
1068
  };
803
1069
  };
804
- const epochOf = (pon, includeAnnotations) => {
805
- const s = ctx.getState();
806
- const content = s.contentEpochs[pon] ?? 0;
807
- if (!includeAnnotations) return content;
808
- return content + (s.annotatedEpochs[pon] ?? 0);
809
- };
810
- const baseKey = (pon, viewport, annotations) => {
811
- const format = strategy().format;
812
- return `${pon}|w${viewport.kind === "width" ? viewport.width : 0}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}${format ? `|f${format}` : ""}`;
813
- };
1070
+ const epochOf = (pageObjectNumber, layers) => renderEpochOf(ctx.state.get(), pageObjectNumber, layers);
1071
+ const rasterKey = (pageObjectNumber, width, layers, format, quality) => `${pageObjectNumber}|w${width}|${layersKeyOf(layers)}|e${epochOf(pageObjectNumber, layers)}${format ? `|f${format}` : ""}${quality !== void 0 ? `|q${quality}` : ""}`;
1072
+ function acquireRaster(page, key, request, signal) {
1073
+ return store.acquire(key, (storeSignal) => {
1074
+ const task = ctx.doc.with({
1075
+ priority: "high",
1076
+ ...request.view !== void 0 ? { view: request.view } : {}
1077
+ }).page(page).render.image({
1078
+ viewport: request.viewport,
1079
+ ...request.layers,
1080
+ ...request.format !== void 0 ? { format: request.format } : {},
1081
+ ...request.quality !== void 0 ? { quality: request.quality } : {}
1082
+ });
1083
+ if (storeSignal.aborted) task.abort(reasonOf(storeSignal));
1084
+ else storeSignal.addEventListener("abort", () => task.abort(reasonOf(storeSignal)), { once: true });
1085
+ return task;
1086
+ }, signal).then((image) => {
1087
+ renderCompleted.emit({
1088
+ page,
1089
+ key
1090
+ });
1091
+ return image;
1092
+ }, (error) => {
1093
+ const mapped = (0, _embedpdf_core.toPluginError)("render", error);
1094
+ if (!(0, _embedpdf_core.isPluginError)(mapped, "operation-cancelled") && !(0, _embedpdf_core.isPluginError)(mapped, "instance-closed")) renderFailed.emit({
1095
+ page,
1096
+ key,
1097
+ error: (0, _embedpdf_core.toPluginErrorInfo)(mapped)
1098
+ });
1099
+ throw mapped;
1100
+ });
1101
+ }
1102
+ /** The host door: conformed to the render points. Async so refusals reject. */
1103
+ async function renderSource(page, { scale, view, signal, ...options }) {
1104
+ ctx.assertAllowed(RENDER_SCOPE, "render.renderSource");
1105
+ const layers = pictureLayers(options, "render.renderSource");
1106
+ const viewport = conformViewport(page, scale);
1107
+ const strategy = currentStrategy();
1108
+ return acquireRaster(page, rasterKey(page.objectNumber, viewport.kind === "width" ? viewport.width : 0, layers, strategy.format, void 0), {
1109
+ viewport,
1110
+ layers,
1111
+ format: strategy.format,
1112
+ quality: strategy.quality,
1113
+ view
1114
+ }, signal);
1115
+ }
1116
+ /** The public door: the requested size, exactly. Async so refusals reject. */
1117
+ async function renderPage(pageArgument, options = {}) {
1118
+ ctx.assertAllowed(RENDER_SCOPE, "render.renderPage");
1119
+ const { ref: page, size } = ctx.pageOf(pageArgument);
1120
+ const width = Math.max(1, Math.round(options.width ?? (options.scale ?? 1) * size.width));
1121
+ const layers = pictureLayers(options, "render.renderPage");
1122
+ const strategy = currentStrategy();
1123
+ const format = options.format ?? strategy.format;
1124
+ const quality = options.quality ?? strategy.quality;
1125
+ const key = rasterKey(page.objectNumber, width, layers, format, options.quality);
1126
+ return ctx.cancellable(options.signal, acquireRaster(page, key, {
1127
+ viewport: {
1128
+ kind: "width",
1129
+ width
1130
+ },
1131
+ layers,
1132
+ format,
1133
+ quality,
1134
+ view: void 0
1135
+ }, options.signal));
1136
+ }
1137
+ async function renderPages(pages, options = {}) {
1138
+ ctx.assertAllowed(RENDER_SCOPE, "render.renderPages");
1139
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_BATCH_CONCURRENCY);
1140
+ const applied = [];
1141
+ const failed = [];
1142
+ const queue = pages.map((page, index) => ({
1143
+ page,
1144
+ index
1145
+ }));
1146
+ const worker = async () => {
1147
+ for (let next = queue.shift(); next; next = queue.shift()) {
1148
+ const { page, index } = next;
1149
+ try {
1150
+ const image = await renderPage(page, options);
1151
+ applied.push({
1152
+ index,
1153
+ render: {
1154
+ page: ctx.pageOf(page).ref,
1155
+ image
1156
+ }
1157
+ });
1158
+ } catch (error) {
1159
+ failed.push({
1160
+ index,
1161
+ ref: page,
1162
+ error: (0, _embedpdf_core.toPluginErrorInfo)((0, _embedpdf_core.toPluginError)("render", error))
1163
+ });
1164
+ }
1165
+ }
1166
+ };
1167
+ const batch = Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
1168
+ await ctx.cancellable(options.signal, batch);
1169
+ const byInput = (left, right) => left.index - right.index;
1170
+ return {
1171
+ applied: applied.sort(byInput).map(({ render }) => render),
1172
+ skipped: [],
1173
+ failed: failed.sort(byInput).map(({ ref, error }) => ({
1174
+ ref,
1175
+ error
1176
+ }))
1177
+ };
1178
+ }
1179
+ async function renderAppearances(page, { family, scale, items, signal }) {
1180
+ ctx.assertAllowed(family === "widgets" ? FORMS_SCOPE : ANNOTATIONS_SCOPE, "render.renderAppearances");
1181
+ const pageObjectNumber = ctx.pageOf(page).ref.objectNumber;
1182
+ const versions = new Map(items.map((item) => [(0, _embedpdf_core.annotationKey)(item.ref), item.version]));
1183
+ const asked = [...versions].map(([item, version]) => `${item}@${version}`).sort();
1184
+ const group = `${family}|${pageObjectNumber}`;
1185
+ const pictures = appearances.acquire(group, `${group}|s${scale}|${asked.join(",")}`, async (fetchSignal) => {
1186
+ const pageHandle = ctx.doc.page((0, _embedpdf_core.toPageRef)(pageObjectNumber));
1187
+ const options = {
1188
+ viewport: {
1189
+ kind: "scale",
1190
+ scale
1191
+ },
1192
+ modes: ["normal"]
1193
+ };
1194
+ const task = family === "widgets" ? pageHandle.forms.renderAppearances(options) : pageHandle.annotations.renderAppearances(options);
1195
+ if (fetchSignal.aborted) task.abort(reasonOf(fetchSignal));
1196
+ else fetchSignal.addEventListener("abort", () => task.abort(reasonOf(fetchSignal)), { once: true });
1197
+ const drawn = (await task).appearances.flatMap((picture) => {
1198
+ const version = versions.get((0, _embedpdf_core.annotationKey)(picture.ref));
1199
+ return version === void 0 ? [] : [{
1200
+ ...picture,
1201
+ version
1202
+ }];
1203
+ });
1204
+ recentAppearances.remember(family, drawn, (picture) => (0, _embedpdf_core.annotationKey)(picture.ref));
1205
+ return drawn;
1206
+ }, signal);
1207
+ return ctx.cancellable(signal, pictures.catch((error) => {
1208
+ throw (0, _embedpdf_core.toPluginError)("render", error);
1209
+ }));
1210
+ }
1211
+ function publishInvalidation(pageObjectNumbers, scope, origin) {
1212
+ if (pageObjectNumbers.length === 0) return;
1213
+ ctx.state.update(invalidatePages, pageObjectNumbers, scope);
1214
+ for (const pageObjectNumber of pageObjectNumbers) replanPage(pageObjectNumber);
1215
+ invalidated.emit({
1216
+ pages: pageObjectNumbers.map((pageObjectNumber) => (0, _embedpdf_core.toPageRef)(pageObjectNumber)),
1217
+ scope,
1218
+ origin
1219
+ });
1220
+ }
1221
+ function invalidate({ pages, scope = "content" } = {}) {
1222
+ const named = pages?.flatMap((page) => ctx.getPage(page)?.ref.objectNumber ?? []);
1223
+ publishInvalidation(named ?? allPageObjectNumbers(), scope, null);
1224
+ }
814
1225
  const pendingAdvance = /* @__PURE__ */ new Set();
815
1226
  let advanceScheduled = false;
816
- const raf = typeof requestAnimationFrame === "function" ? (cb) => requestAnimationFrame(() => cb()) : (cb) => void setTimeout(cb, 16);
817
- const wake = (pon) => {
818
- pendingAdvance.add(pon);
1227
+ const wake = (pageObjectNumber) => {
1228
+ pendingAdvance.add(pageObjectNumber);
819
1229
  if (advanceScheduled) return;
820
1230
  advanceScheduled = true;
821
- raf(() => {
1231
+ ctx.clock.nextFrame(() => {
822
1232
  advanceScheduled = false;
823
- const pons = [...pendingAdvance];
1233
+ const advanced = [...pendingAdvance];
824
1234
  pendingAdvance.clear();
825
- for (const p of pons) ctx.dispatch({
826
- type: "PAINT_ADVANCED",
827
- pon: p
828
- });
1235
+ for (const advancedPage of advanced) replanPage(advancedPage);
829
1236
  });
830
1237
  };
831
- const viewTiles = /* @__PURE__ */ new Map();
832
1238
  const tiles = new TileManager({
833
1239
  store,
834
- options: resolved,
835
- getPolicy: policy,
836
- getPageSize: (pon) => pageOf(pon)?.size,
1240
+ getOptions: resolvedOptions,
1241
+ getPolicy: renderPolicy,
1242
+ getPageSize: (pageObjectNumber) => ctx.getPage((0, _embedpdf_core.toPageRef)(pageObjectNumber))?.size,
837
1243
  getEpoch: epochOf,
838
- fetchTile: (pon, rect, scale, includeAnnotations, signal) => {
839
- const doc = ctx.doc;
840
- if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
841
- if (!canRender()) return Promise.reject(new _embedpdf_core.PermissionDenied("doc.render", "render.tile"));
842
- const s = strategy();
843
- const task = doc.page(pon).render.image({
1244
+ after: ctx.clock.after,
1245
+ fetchTile: async (view, pageObjectNumber, rect, scale, layers, signal) => {
1246
+ ctx.assertAllowed(RENDER_SCOPE, "render.tile");
1247
+ const page = (0, _embedpdf_core.toPageRef)(pageObjectNumber);
1248
+ const strategy = currentStrategy();
1249
+ const task = ctx.doc.with({
1250
+ priority: "high",
1251
+ view
1252
+ }).page(page).render.image({
844
1253
  target: {
845
1254
  kind: "rect",
846
1255
  rect
@@ -849,184 +1258,151 @@ function createRenderCapability(ctx, options = {}) {
849
1258
  kind: "scale",
850
1259
  scale
851
1260
  },
852
- includeAnnotations,
853
- ...s.format !== void 0 ? { format: s.format } : {},
854
- ...s.quality !== void 0 ? { quality: s.quality } : {}
1261
+ ...layers,
1262
+ ...strategy.format !== void 0 ? { format: strategy.format } : {},
1263
+ ...strategy.quality !== void 0 ? { quality: strategy.quality } : {}
855
1264
  });
856
- if (signal.aborted) task.abort(signal.reason);
857
- else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
1265
+ if (signal.aborted) task.abort(reasonOf(signal));
1266
+ else signal.addEventListener("abort", () => task.abort(reasonOf(signal)), { once: true });
858
1267
  return task;
859
1268
  },
860
1269
  onAdvance: wake,
861
- ...resolved.debug ? { debug: (msg) => console.debug(`[render] ${msg}`) } : {}
1270
+ debug: (message) => {
1271
+ if (resolvedOptions().debug) console.debug(`[render] ${message}`);
1272
+ }
862
1273
  });
863
- return {
864
- canRender,
865
- renderPage(pon, { scale, includeAnnotations, signal }) {
866
- const doc = ctx.doc;
867
- if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
868
- if (!canRender()) return Promise.reject(new _embedpdf_core.PermissionDenied("doc.render", "render.renderPage"));
869
- const annotations = includeAnnotations ?? true;
870
- const viewport = conformViewport(pon, scale);
871
- const key = baseKey(pon, viewport, annotations);
872
- return store.acquire(key, (storeSignal) => {
873
- const s = strategy();
874
- const task = doc.page(pon).render.image({
875
- viewport,
876
- includeAnnotations: annotations,
877
- ...s.format !== void 0 ? { format: s.format } : {},
878
- ...s.quality !== void 0 ? { quality: s.quality } : {}
879
- });
880
- if (storeSignal.aborted) task.abort(storeSignal.reason);
881
- else storeSignal.addEventListener("abort", () => task.abort(storeSignal.reason), { once: true });
882
- return task;
883
- }, signal);
884
- },
885
- renderSourceKey(pon, { scale, includeAnnotations }) {
886
- const annotations = includeAnnotations ?? true;
887
- return baseKey(pon, conformViewport(pon, scale), annotations);
888
- },
889
- conformViewport,
890
- paintSettings() {
891
- return {
892
- fadeMs: resolved.tiles.fadeMs,
893
- tiles: resolved.tiles.enabled
894
- };
895
- },
896
- renderPolicy: policy,
897
- tilesFor(view) {
898
- let handle = viewTiles.get(view);
899
- if (!handle) {
900
- handle = {
901
- plan: (pon, demand, opts) => tiles.plan(view, pon, demand, opts?.includeAnnotations ?? true),
902
- painted: (pon, key) => tiles.sourcePainted(view, pon, key),
903
- unpainted: (pon, key) => tiles.sourceUnpainted(view, pon, key),
904
- release: (pon) => tiles.releasePage(view, pon)
905
- };
906
- viewTiles.set(view, handle);
1274
+ /** One handle per view id, reference-counted; demands and plans per page. */
1275
+ const views = /* @__PURE__ */ new Map();
1276
+ /** Re-plan a page for every view that wants it (schedules the next want
1277
+ * set), and wake readers once when any plan changed. */
1278
+ function replanPage(pageObjectNumber) {
1279
+ let changed = false;
1280
+ for (const [viewId, view] of views) {
1281
+ const entry = view.pages.get(pageObjectNumber);
1282
+ if (!entry) continue;
1283
+ const plan = tiles.plan(viewId, pageObjectNumber, entry.demand, entry.layers);
1284
+ if (plan !== entry.plan) {
1285
+ entry.plan = plan;
1286
+ changed = true;
907
1287
  }
908
- return handle;
909
- },
910
- renderEpoch(pon, includeAnnotations = true) {
911
- return epochOf(pon, includeAnnotations);
912
- },
913
- invalidate({ pons, scope = "content" } = {}) {
914
- const target = pons ?? (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
915
- if (target.length) ctx.dispatch({
916
- type: "INVALIDATE",
917
- scope,
918
- pons: target
919
- });
920
1288
  }
921
- };
922
- }
923
- //#endregion
924
- //#region src/effects.ts
925
- /**
926
- * Which pages' ANNOTATED raster a confirmed mutation repaints — the whole
927
- * event→scope map. Everything the engine emits today is appearance-scoped;
928
- * when it grows content-mutation events (redaction.applied, content.edited),
929
- * they get a case here dispatching scope 'content'. Over-invalidation is
930
- * acceptable (a z-order move that changes nothing repaints one thumb);
931
- * under-invalidation is the bug.
932
- */
933
- function annotatedPons(event, allPons) {
934
- switch (event.type) {
935
- case "annotation.created":
936
- case "annotation.updated":
937
- case "annotation.deleted":
938
- case "annotation.moved": return [event.pageObjectNumber];
939
- case "form.valueChanged":
940
- case "form.effectsApplied": return event.changedWidgets.map((w) => w.pageObjectNumber);
941
- case "form.fieldDeleted": return event.removedWidgets.map((w) => w.pageObjectNumber);
942
- case "form.fieldCreated":
943
- case "form.fieldUpdated":
944
- case "form.widgetAttached":
945
- case "form.widgetDetached": return event.field.widgets.map((w) => w.pageObjectNumber);
946
- case "form.imported":
947
- case "form.repaired": return allPons();
948
- default: return [];
1289
+ if (changed) ctx.notify();
949
1290
  }
950
- }
951
- function registerRenderEffects(ctx) {
952
- const doc = ctx.doc;
953
- if (!doc) return;
954
- const allPons = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
955
- const unsubscribe = doc.events.subscribe((event) => {
956
- if (event.type === "redaction.applied") {
957
- const pons = event.results.filter((r) => r.status === "applied").map((r) => r.pageObjectNumber);
958
- if (pons.length) ctx.dispatch({
959
- type: "INVALIDATE",
960
- scope: "content",
961
- pons
962
- });
963
- return;
1291
+ function createViewDemand(viewId) {
1292
+ const existing = views.get(viewId);
1293
+ if (existing) {
1294
+ existing.refs += 1;
1295
+ return existing.handle;
964
1296
  }
965
- const pons = annotatedPons(event, allPons);
966
- if (pons.length) ctx.dispatch({
967
- type: "INVALIDATE",
968
- scope: "annotations",
969
- pons
1297
+ const pages = /* @__PURE__ */ new Map();
1298
+ const handle = {
1299
+ setDemand: (page, demand, options) => {
1300
+ const pageObjectNumber = page.objectNumber;
1301
+ const layers = layersOf(options ?? {});
1302
+ const previous = pages.get(pageObjectNumber);
1303
+ const plan = tiles.plan(viewId, pageObjectNumber, demand, layers);
1304
+ pages.set(pageObjectNumber, {
1305
+ demand,
1306
+ layers,
1307
+ plan
1308
+ });
1309
+ if (plan !== previous?.plan) ctx.notify();
1310
+ },
1311
+ getPlan: (page) => pages.get(page.objectNumber)?.plan ?? require_contract.EMPTY_TILE_PLAN,
1312
+ markPainted: (page, key) => tiles.sourcePainted(viewId, page.objectNumber, key),
1313
+ markUnpainted: (page, key) => tiles.sourceUnpainted(viewId, page.objectNumber, key),
1314
+ release: (page) => {
1315
+ pages.delete(page.objectNumber);
1316
+ tiles.releasePage(viewId, page.objectNumber);
1317
+ },
1318
+ dispose: () => {
1319
+ const view = views.get(viewId);
1320
+ if (!view || view.refs === 0) return;
1321
+ view.refs -= 1;
1322
+ if (view.refs > 0) return;
1323
+ for (const pageObjectNumber of [...view.pages.keys()]) tiles.releasePage(viewId, pageObjectNumber);
1324
+ view.pages.clear();
1325
+ }
1326
+ };
1327
+ views.set(viewId, {
1328
+ handle,
1329
+ refs: 1,
1330
+ pages
970
1331
  });
1332
+ return handle;
1333
+ }
1334
+ const paintSettings = (0, _embedpdf_core.memo)(() => [resolvedOptions().tiles.enabled], (enabled) => Object.freeze({ tiles: enabled }));
1335
+ ctx.listen(settings.api.onSettingsChanged, () => {
1336
+ const shown = /* @__PURE__ */ new Set();
1337
+ for (const view of views.values()) for (const page of view.pages.keys()) shown.add(page);
1338
+ for (const page of shown) replanPage(page);
971
1339
  });
972
- ctx.cleanup(unsubscribe);
973
- }
974
- //#endregion
975
- //#region src/reducer.ts
976
- const initialRenderState = () => ({
977
- contentEpochs: {},
978
- annotatedEpochs: {},
979
- paintVersions: {}
980
- });
981
- const bump = (epochs, pons) => {
982
- const next = { ...epochs };
983
- for (const pon of pons) next[pon] = (next[pon] ?? 0) + 1;
984
- return next;
985
- };
986
- /** Pure. Two transitions: bump the touched pages' ledger for the fact's
987
- * scope (one action per fact, so a batch result bumps each pon once — a
988
- * 'content' fact bumps ONLY the content ledger; annotated readers sum both,
989
- * which is how content invalidation reaches them too), and the tile
990
- * wake-up counter. The render POLICY is deliberately NOT here — it's a
991
- * document fact on `DocumentMeta`, materialized by the kernel at open. */
992
- const renderReducer = (state, a) => {
993
- if (a.type === "PAINT_ADVANCED") return {
994
- ...state,
995
- paintVersions: bump(state.paintVersions, [a.pon])
996
- };
997
- if (a.type !== "INVALIDATE" || a.pons.length === 0) return state;
998
- return a.scope === "content" ? {
999
- ...state,
1000
- contentEpochs: bump(state.contentEpochs, a.pons)
1001
- } : {
1002
- ...state,
1003
- annotatedEpochs: bump(state.annotatedEpochs, a.pons)
1340
+ return {
1341
+ api: {
1342
+ ...settings.api,
1343
+ canRender,
1344
+ renderPage,
1345
+ renderThumbnail: (page, { maxWidth, ...options }) => renderPage(page, {
1346
+ width: maxWidth,
1347
+ ...options
1348
+ }),
1349
+ renderPages,
1350
+ getRenderPolicy: renderPolicy,
1351
+ getRenderEpoch: (page, layers = {}) => {
1352
+ const info = ctx.getPage(page);
1353
+ return info ? epochOf(info.ref.objectNumber, layersOf(layers)) : 0;
1354
+ },
1355
+ invalidate,
1356
+ onInvalidated: invalidated.on,
1357
+ renderSource,
1358
+ getSourceKey: (page, { scale, view: _view, ...options }) => {
1359
+ const viewport = conformViewport(page, scale);
1360
+ return rasterKey(page.objectNumber, viewport.kind === "width" ? viewport.width : 0, layersOf(options), currentStrategy().format, void 0);
1361
+ },
1362
+ conformViewport,
1363
+ getPaintSettings: paintSettings,
1364
+ getLayerRights: rights,
1365
+ createViewDemand,
1366
+ renderAppearances,
1367
+ keptAppearances: (family, item) => recentAppearances.of(family, (0, _embedpdf_core.annotationKey)(item)),
1368
+ getAppearanceScale: (renderScale) => (0, _embedpdf_core.snapAppearanceScale)(renderPolicy(), renderScale),
1369
+ onRenderCompleted: renderCompleted.on,
1370
+ onRenderFailed: renderFailed.on
1371
+ },
1372
+ connect() {
1373
+ ctx.listen(ctx.doc.events, (event) => {
1374
+ const change = pixelChangeOf(event, allPageObjectNumbers);
1375
+ if (!change) return;
1376
+ publishInvalidation(change.pages, change.scope, "origin" in event ? event.origin : null);
1377
+ });
1378
+ }
1004
1379
  };
1005
- };
1380
+ }
1006
1381
  //#endregion
1007
1382
  //#region src/render.plugin.ts
1008
1383
  /**
1009
- * Document-scoped. The ONE policy consumer in the client stack: the doc-bind
1010
- * effect resolves the engine's advertised render
1011
- * policy; `renderPage` conforms desired scales to it and collapses
1012
- * same-rung asks in the raster store; the tile manager turns host-supplied
1013
- * demand into a retention-safe paint plan over the SAME store. State is the
1014
- * per-page ledger — raster versions (two doors: the document event stream's
1015
- * built-in map and the `invalidate` verb), the policy latch, and the tile
1016
- * wake-up counter. Layers key on `renderSourceKey`/`tilePlan` and refetch
1017
- * exactly when those change.
1384
+ * Document-scoped page rasters. The only render-policy consumer in the
1385
+ * client stack: the kernel materializes the engine's advertised render
1386
+ * policy on the document, the controller conforms desired scales to it and
1387
+ * collapses same-key asks in its raster store, and the tile manager turns
1388
+ * host-supplied demand into a retention-safe paint plan over the same store.
1389
+ * State is the per-page raster-version ledger.
1018
1390
  */
1019
- const renderPlugin = (options = {}) => (0, _embedpdf_core.definePlugin)({
1391
+ const renderPlugin = (config) => (0, _embedpdf_core.definePlugin)({
1020
1392
  id: "render",
1021
1393
  scope: "document",
1022
- token: require_contract.RenderToken,
1023
- initialState: initialRenderState,
1024
- reduce: renderReducer,
1025
- capability: (ctx) => createRenderCapability(ctx, options),
1026
- effects: registerRenderEffects
1394
+ token: require_contract_host.RenderToken,
1395
+ state: initialRenderState,
1396
+ settings: {
1397
+ defaults: require_contract.RENDER_DEFAULTS,
1398
+ registered: config
1399
+ },
1400
+ create: createRenderController
1027
1401
  });
1028
1402
  //#endregion
1403
+ exports.RENDER_DEFAULTS = require_contract.RENDER_DEFAULTS;
1029
1404
  exports.RenderToken = require_contract.RenderToken;
1030
1405
  exports.renderPlugin = renderPlugin;
1406
+ exports.samePageViewDemand = require_contract.samePageViewDemand;
1031
1407
 
1032
1408
  //# sourceMappingURL=index.cjs.map