@inkbar/core 0.2.0 → 0.4.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.
@@ -1,1878 +0,0 @@
1
- import { z } from "zod";
2
- import { v7 } from "uuid";
3
- //#region src/model/errors.ts
4
- var InkError = class extends Error {
5
- name = "InkError";
6
- code;
7
- details;
8
- constructor(code, message, details = {}, cause) {
9
- super(message, cause === void 0 ? void 0 : { cause });
10
- this.code = code;
11
- this.details = details;
12
- }
13
- };
14
- function isInkError(value) {
15
- return value instanceof InkError || typeof value === "object" && value !== null && value.name === "InkError";
16
- }
17
- //#endregion
18
- //#region src/model/events.ts
19
- var Emitter = class {
20
- listeners = /* @__PURE__ */ new Map();
21
- on(event, fn) {
22
- let set = this.listeners.get(event);
23
- if (!set) {
24
- set = /* @__PURE__ */ new Set();
25
- this.listeners.set(event, set);
26
- }
27
- set.add(fn);
28
- return () => {
29
- set?.delete(fn);
30
- };
31
- }
32
- emit(event, payload) {
33
- const set = this.listeners.get(event);
34
- if (!set) return;
35
- for (const fn of [...set]) fn(payload);
36
- }
37
- clear() {
38
- this.listeners.clear();
39
- }
40
- };
41
- //#endregion
42
- //#region src/model/geometry.ts
43
- function approxEqual(a, b, eps = .01) {
44
- return Math.abs(a - b) <= eps;
45
- }
46
- function rectFromPoints(a, b) {
47
- return {
48
- x: Math.min(a.x, b.x),
49
- y: Math.min(a.y, b.y),
50
- width: Math.abs(a.x - b.x),
51
- height: Math.abs(a.y - b.y)
52
- };
53
- }
54
- function rectFromQuad(q) {
55
- const xs = [
56
- q.p1.x,
57
- q.p2.x,
58
- q.p3.x,
59
- q.p4.x
60
- ];
61
- const ys = [
62
- q.p1.y,
63
- q.p2.y,
64
- q.p3.y,
65
- q.p4.y
66
- ];
67
- const x = Math.min(...xs);
68
- const y = Math.min(...ys);
69
- return {
70
- x,
71
- y,
72
- width: Math.max(...xs) - x,
73
- height: Math.max(...ys) - y
74
- };
75
- }
76
- function quadFromRect(r) {
77
- const top = r.y + r.height;
78
- const right = r.x + r.width;
79
- return {
80
- p1: {
81
- x: r.x,
82
- y: top
83
- },
84
- p2: {
85
- x: right,
86
- y: top
87
- },
88
- p3: {
89
- x: r.x,
90
- y: r.y
91
- },
92
- p4: {
93
- x: right,
94
- y: r.y
95
- }
96
- };
97
- }
98
- function unionRects(rects) {
99
- if (rects.length === 0) throw new RangeError("unionRects needs at least one rect");
100
- let x1 = Infinity;
101
- let y1 = Infinity;
102
- let x2 = -Infinity;
103
- let y2 = -Infinity;
104
- for (const r of rects) {
105
- x1 = Math.min(x1, r.x);
106
- y1 = Math.min(y1, r.y);
107
- x2 = Math.max(x2, r.x + r.width);
108
- y2 = Math.max(y2, r.y + r.height);
109
- }
110
- return {
111
- x: x1,
112
- y: y1,
113
- width: x2 - x1,
114
- height: y2 - y1
115
- };
116
- }
117
- function rectsIntersect(a, b) {
118
- return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
119
- }
120
- function rectContainsPoint(r, p) {
121
- return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
122
- }
123
- function translateRect(r, dx, dy) {
124
- return {
125
- x: r.x + dx,
126
- y: r.y + dy,
127
- width: r.width,
128
- height: r.height
129
- };
130
- }
131
- function translateQuad(q, dx, dy) {
132
- const t = (p) => ({
133
- x: p.x + dx,
134
- y: p.y + dy
135
- });
136
- return {
137
- p1: t(q.p1),
138
- p2: t(q.p2),
139
- p3: t(q.p3),
140
- p4: t(q.p4)
141
- };
142
- }
143
- function applyMatrix(m, p) {
144
- return {
145
- x: m[0] * p.x + m[2] * p.y + m[4],
146
- y: m[1] * p.x + m[3] * p.y + m[5]
147
- };
148
- }
149
- function invertMatrix(m) {
150
- const det = m[0] * m[3] - m[1] * m[2];
151
- if (det === 0) throw new RangeError("matrix is not invertible");
152
- return [
153
- m[3] / det,
154
- -m[1] / det,
155
- -m[2] / det,
156
- m[0] / det,
157
- (m[2] * m[5] - m[3] * m[4]) / det,
158
- (m[1] * m[4] - m[0] * m[5]) / det
159
- ];
160
- }
161
- /** Returns the matrix that applies `a` first, then `b`. */
162
- function multiplyMatrix(a, b) {
163
- return [
164
- a[0] * b[0] + a[1] * b[2],
165
- a[0] * b[1] + a[1] * b[3],
166
- a[2] * b[0] + a[3] * b[2],
167
- a[2] * b[1] + a[3] * b[3],
168
- a[4] * b[0] + a[5] * b[2] + b[4],
169
- a[4] * b[1] + a[5] * b[3] + b[5]
170
- ];
171
- }
172
- function pointsFromFlat(flat) {
173
- const out = [];
174
- for (let i = 0; i + 1 < flat.length; i += 2) out.push({
175
- x: flat[i],
176
- y: flat[i + 1]
177
- });
178
- return out;
179
- }
180
- /** Eight numbers per quad, in PDF.js's normalised order. Trailing partial quads are dropped. */
181
- function quadsFromFlat(flat) {
182
- const pts = pointsFromFlat(flat);
183
- const out = [];
184
- for (let i = 0; i + 3 < pts.length; i += 4) out.push({
185
- p1: pts[i],
186
- p2: pts[i + 1],
187
- p3: pts[i + 2],
188
- p4: pts[i + 3]
189
- });
190
- return out;
191
- }
192
- //#endregion
193
- //#region src/model/readable.ts
194
- function writable(initial) {
195
- let value = initial;
196
- const subs = /* @__PURE__ */ new Set();
197
- const set = (next) => {
198
- if (Object.is(next, value)) return;
199
- value = next;
200
- for (const fn of [...subs]) fn(value);
201
- };
202
- return {
203
- get: () => value,
204
- set,
205
- update(fn) {
206
- set(fn(value));
207
- },
208
- subscribe(fn) {
209
- subs.add(fn);
210
- return () => {
211
- subs.delete(fn);
212
- };
213
- }
214
- };
215
- }
216
- /**
217
- * A readable computed from `source`. It subscribes to the source only while it has
218
- * subscribers of its own, so an unobserved derived value holds no reference to the source.
219
- */
220
- function derived(source, fn) {
221
- const subs = /* @__PURE__ */ new Set();
222
- let cached;
223
- let stopSource;
224
- const onSource = (v) => {
225
- const next = fn(v);
226
- if (Object.is(next, cached)) return;
227
- cached = next;
228
- for (const sub of [...subs]) sub(next);
229
- };
230
- return {
231
- get: () => stopSource ? cached : fn(source.get()),
232
- subscribe(sub) {
233
- if (!stopSource) {
234
- cached = fn(source.get());
235
- stopSource = source.subscribe(onSource);
236
- }
237
- subs.add(sub);
238
- return () => {
239
- if (!subs.delete(sub) || subs.size > 0) return;
240
- stopSource?.();
241
- stopSource = void 0;
242
- };
243
- }
244
- };
245
- }
246
- function readonly(w) {
247
- return {
248
- get: () => w.get(),
249
- subscribe: (fn) => w.subscribe(fn)
250
- };
251
- }
252
- //#endregion
253
- //#region src/model/selection.ts
254
- const union = (a, b) => {
255
- const x = Math.min(a.x, b.x);
256
- const y = Math.min(a.y, b.y);
257
- return {
258
- x,
259
- y,
260
- width: Math.max(a.x + a.width, b.x + b.width) - x,
261
- height: Math.max(a.y + a.height, b.y + b.height) - y
262
- };
263
- };
264
- const centreY = (r) => r.y + r.height / 2;
265
- function median(values) {
266
- const sorted = [...values].sort((a, b) => a - b);
267
- const mid = sorted.length >> 1;
268
- return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
269
- }
270
- /**
271
- * Merges glyph boxes that share a line into one quad per run, ordered top to bottom (by
272
- * vertical centre) then left to right. Boxes whose vertical centres are within `tolerance`
273
- * points count as the same line; boxes with no area are dropped. Within a line, a horizontal
274
- * gap wider than `gap` points (default 1.5 × the line's median box height) starts a new run,
275
- * so text selected across a column gutter yields one quad per column. Pure user-space maths.
276
- */
277
- function mergeLineRects(rects, tolerance = 2, gap) {
278
- const lines = [];
279
- for (const r of rects) {
280
- if (!(r.width > 0 && r.height > 0)) continue;
281
- const line = lines.find((l) => Math.abs(centreY(l.bounds) - centreY(r)) <= tolerance);
282
- if (!line) {
283
- lines.push({
284
- bounds: { ...r },
285
- boxes: [r]
286
- });
287
- continue;
288
- }
289
- line.bounds = union(line.bounds, r);
290
- line.boxes.push(r);
291
- }
292
- const runs = [];
293
- for (const line of lines) {
294
- const maxGap = gap ?? 1.5 * median(line.boxes.map((b) => b.height));
295
- const boxes = [...line.boxes].sort((a, b) => a.x - b.x);
296
- let run = { ...boxes[0] };
297
- for (const b of boxes.slice(1)) if (b.x - (run.x + run.width) > maxGap) {
298
- runs.push(run);
299
- run = { ...b };
300
- } else run = union(run, b);
301
- runs.push(run);
302
- }
303
- runs.sort((a, b) => centreY(b) - centreY(a) || a.x - b.x);
304
- return runs.map(quadFromRect);
305
- }
306
- //#endregion
307
- //#region src/pdfjs/load.ts
308
- const REQUIRED = [
309
- "worker",
310
- "cMaps",
311
- "standardFonts",
312
- "wasm"
313
- ];
314
- function assertAssets(value) {
315
- const record = typeof value === "object" && value !== null ? value : {};
316
- const missing = REQUIRED.filter((k) => typeof record[k] !== "string" || record[k] === "");
317
- if (value === void 0 || value === null || missing.length > 0) throw new InkError("AssetConfigMissing", "openDocument needs an `assets` option naming the PDF.js worker, cMaps, standardFonts and wasm locations. Vite users: add the @inkbar/core/vite plugin and pass `assets` from \"virtual:inkbar-assets\".", { missing: value == null ? [...REQUIRED] : missing });
318
- }
319
- let modulePromise;
320
- let configuredWorker;
321
- /**
322
- * Loads PDF.js once. `#pdfjs` resolves to the legacy build in Node and the
323
- * modern build everywhere else (see package.json "imports").
324
- */
325
- async function loadPdfjs(assets) {
326
- assertAssets(assets);
327
- modulePromise ??= import("#pdfjs").catch((cause) => {
328
- modulePromise = void 0;
329
- throw new InkError("AssetLoadFailed", "PDF.js could not be imported", { url: assets.worker }, cause);
330
- });
331
- const pdfjs = await modulePromise;
332
- if (configuredWorker === void 0) {
333
- pdfjs.GlobalWorkerOptions.workerSrc = assets.worker;
334
- configuredWorker = assets.worker;
335
- } else if (configuredWorker !== assets.worker) throw new InkError("AssetConfigMissing", "PDF.js worker URL cannot change after the first document is opened", {
336
- configured: configuredWorker,
337
- requested: assets.worker
338
- });
339
- return pdfjs;
340
- }
341
- async function openProxy(pdfjs, bytes, options) {
342
- const { assets, password, verbosity } = options;
343
- const task = pdfjs.getDocument({
344
- data: bytes,
345
- ...password !== void 0 ? { password } : {},
346
- ...verbosity !== void 0 ? { verbosity } : {},
347
- cMapUrl: assets.cMaps,
348
- cMapPacked: true,
349
- standardFontDataUrl: assets.standardFonts,
350
- wasmUrl: assets.wasm,
351
- ...assets.icc !== void 0 ? { iccUrl: assets.icc } : {}
352
- });
353
- try {
354
- return {
355
- task,
356
- proxy: await task.promise
357
- };
358
- } catch (cause) {
359
- try {
360
- await task.destroy();
361
- } catch {}
362
- throw mapLoadError(cause, {
363
- assets,
364
- passwordSupplied: password !== void 0
365
- });
366
- }
367
- }
368
- /** `PasswordResponses` values from PDF.js (`NEED_PASSWORD = 1`, `INCORRECT_PASSWORD = 2`). */
369
- const INCORRECT_PASSWORD = 2;
370
- /**
371
- * PDF.js raises these from `task.promise` when the worker script cannot be
372
- * imported (in browsers after its real Worker fails and the fake-worker
373
- * fallback also fails to import `workerSrc`).
374
- */
375
- const WORKER_FAILURE = /Setting up fake worker failed|No "GlobalWorkerOptions\.workerSrc" specified/i;
376
- /**
377
- * Only the worker is mapped here. CMap, standard-font and wasm failures are
378
- * PDF.js warnings with fallbacks that never reject the open promise, so they
379
- * cannot be detected at open time.
380
- *
381
- * PDF.js 6 exports only some exception classes (`PasswordException`,
382
- * `InvalidPDFException`, `ResponseException`, `AbortException`), and errors
383
- * that cross the worker boundary are rebuilt by name, so match on `name`.
384
- * Inputs here are always in-memory bytes, so there is no fetch to fail:
385
- * every non-password failure means the bytes are not a usable PDF.
386
- */
387
- function mapLoadError(cause, context) {
388
- const { assets, passwordSupplied = false } = context;
389
- const name = cause?.name;
390
- const rawMessage = cause?.message;
391
- const message = typeof rawMessage === "string" && rawMessage !== "" ? rawMessage : "Unknown PDF.js error";
392
- if (WORKER_FAILURE.test(message)) return new InkError("AssetLoadFailed", "The PDF.js worker could not be loaded. Check the `assets.worker` URL.", { url: assets.worker }, cause);
393
- if (name === "PasswordException") {
394
- const wrong = cause.code === INCORRECT_PASSWORD;
395
- return new InkError("PasswordRequired", wrong ? "The password is incorrect" : "This document is encrypted and needs a password", {
396
- passwordSupplied,
397
- incorrect: wrong
398
- }, cause);
399
- }
400
- return new InkError("InvalidPdf", message, {
401
- pdfjsErrorName: typeof name === "string" ? name : void 0,
402
- pdfjsMessage: message
403
- }, cause);
404
- }
405
- //#endregion
406
- //#region src/model/annotations/defaults.ts
407
- const COLORS = {
408
- highlight: "#F2C230",
409
- underline: "#2B5A9E",
410
- strikeout: "#C0392B",
411
- squiggly: "#1F7A4D",
412
- note: "#F2C230",
413
- freeText: "#151820",
414
- square: "#C0392B",
415
- circle: "#C0392B",
416
- line: "#151820",
417
- polyline: "#151820",
418
- polygon: "#151820",
419
- ink: "#151820",
420
- redact: "#C0392B"
421
- };
422
- /**
423
- * Suggested-redaction colours, strengthening with confidence: amber for low, orange for
424
- * medium, the redaction red for high.
425
- */
426
- const SUGGESTION_COLORS = {
427
- low: "#D4A017",
428
- medium: "#E67E22",
429
- high: "#C0392B"
430
- };
431
- const staticDefaults = {
432
- color: (type) => COLORS[type],
433
- fill: () => "#000000",
434
- opacity: (type) => type === "highlight" ? .4 : 1,
435
- suggestion: (bucket) => SUGGESTION_COLORS[bucket]
436
- };
437
- const VARIABLES = {
438
- highlight: "--ink-highlight",
439
- underline: "--ink-underline",
440
- strikeout: "--ink-strikeout",
441
- squiggly: "--ink-squiggly",
442
- note: "--ink-note",
443
- freeText: "--ink-free-text",
444
- square: "--ink-shape",
445
- circle: "--ink-shape",
446
- line: "--ink-shape",
447
- polyline: "--ink-shape",
448
- polygon: "--ink-shape",
449
- ink: "--ink-ink",
450
- redact: "--ink-redact"
451
- };
452
- /** Resolves default colours from CSS custom properties on `el`, falling back to `staticDefaults`. */
453
- function cssVariableDefaults(el) {
454
- const read = (name) => {
455
- const view = el.ownerDocument.defaultView;
456
- if (!view) return void 0;
457
- return normalizeColor(view.getComputedStyle(el).getPropertyValue(name));
458
- };
459
- return {
460
- color: (type) => read(VARIABLES[type]) ?? staticDefaults.color(type),
461
- fill: () => read("--ink-redact-fill") ?? staticDefaults.fill(),
462
- opacity: (type) => staticDefaults.opacity(type),
463
- suggestion: (bucket) => read(`--ink-suggestion-${bucket}`) ?? staticDefaults.suggestion(bucket)
464
- };
465
- }
466
- /** Accepts `#rgb`, `#rrggbb`, `rgb(r, g, b)` and `rgb(r g b)`; returns `#RRGGBB` or undefined. */
467
- function normalizeColor(value) {
468
- const v = value.trim();
469
- const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(v);
470
- if (short) {
471
- const [, r = "", g = "", b = ""] = short;
472
- return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
473
- }
474
- if (/^#[0-9a-f]{6}$/i.test(v)) return v.toUpperCase();
475
- const rgb = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/i.exec(v);
476
- if (rgb) return `#${[
477
- rgb[1],
478
- rgb[2],
479
- rgb[3]
480
- ].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
481
- }
482
- //#endregion
483
- //#region src/model/pdf-date.ts
484
- /** Parses a PDF date string (`D:YYYYMMDDHHmmSSOHH'mm'`) into ISO 8601. Returns `fallback` on anything unparseable. */
485
- function parsePdfDate(value, fallback) {
486
- if (!value) return fallback;
487
- const m = /^(?:D:)?(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}(?:'\d{2}')?)?/.exec(value.trim());
488
- if (!m) return fallback;
489
- const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
490
- let offsetMinutes = 0;
491
- if (zone && zone !== "Z") {
492
- const sign = zone.startsWith("-") ? -1 : 1;
493
- const zh = Number(zone.slice(1, 3));
494
- const zm = Number(zone.slice(4, 6) || 0);
495
- offsetMinutes = sign * (zh * 60 + zm);
496
- }
497
- const utc = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s)) - offsetMinutes * 6e4;
498
- if (Number.isNaN(utc)) return fallback;
499
- return new Date(utc).toISOString();
500
- }
501
- //#endregion
502
- //#region src/model/annotations/types.ts
503
- const ANNOTATION_FORMAT = "inkbar/annotations";
504
- const ANNOTATION_VERSION = 1;
505
- const Hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, "expected #RRGGBB");
506
- const PointSchema = z.object({
507
- x: z.number(),
508
- y: z.number()
509
- });
510
- const RectSchema = z.object({
511
- x: z.number(),
512
- y: z.number(),
513
- width: z.number().nonnegative(),
514
- height: z.number().nonnegative()
515
- });
516
- const QuadSchema = z.object({
517
- p1: PointSchema,
518
- p2: PointSchema,
519
- p3: PointSchema,
520
- p4: PointSchema
521
- });
522
- const Base = z.object({
523
- /** Client id, UUIDv7, assigned at creation and never reassigned. Embedded annotations use `pdf:<ref>`. */
524
- id: z.string().min(1),
525
- /** 0-based page index. */
526
- page: z.number().int().nonnegative(),
527
- /** Bounding box in PDF user space. */
528
- rect: RectSchema,
529
- /** Resolved #RRGGBB. Written into the PDF later, so it is always concrete. */
530
- color: Hex,
531
- opacity: z.number().min(0).max(1),
532
- author: z.string().optional(),
533
- contents: z.string().optional(),
534
- /** Id of the parent note; replies form threads. */
535
- inReplyTo: z.string().optional(),
536
- createdAt: z.string(),
537
- modifiedAt: z.string(),
538
- /** Created in this session, or imported from the file. */
539
- source: z.enum(["session", "pdf"]),
540
- /** Embedded annotations are locked until the engine can rewrite the file. */
541
- locked: z.boolean().optional(),
542
- /** Host data. Preserved, never interpreted. */
543
- custom: z.record(z.string(), z.unknown()).optional()
544
- });
545
- const TextMarkupSchema = Base.extend({
546
- type: z.enum([
547
- "highlight",
548
- "underline",
549
- "strikeout",
550
- "squiggly"
551
- ]),
552
- quads: z.array(QuadSchema).min(1)
553
- });
554
- const NoteSchema = Base.extend({
555
- type: z.literal("note"),
556
- icon: z.enum([
557
- "comment",
558
- "note",
559
- "help",
560
- "insert",
561
- "key",
562
- "paragraph"
563
- ]).default("comment")
564
- });
565
- const FreeTextSchema = Base.extend({
566
- type: z.literal("freeText"),
567
- text: z.string(),
568
- fontSize: z.number().positive().default(12),
569
- fontFamily: z.string().default("Helvetica"),
570
- align: z.enum([
571
- "left",
572
- "center",
573
- "right"
574
- ]).default("left"),
575
- callout: z.array(PointSchema).min(2).max(3).optional()
576
- });
577
- const ShapeSchema = Base.extend({
578
- type: z.enum(["square", "circle"]),
579
- strokeWidth: z.number().nonnegative().default(1),
580
- fill: Hex.optional()
581
- });
582
- const LineEndingSchema = z.enum([
583
- "none",
584
- "openArrow",
585
- "closedArrow",
586
- "square",
587
- "circle",
588
- "diamond",
589
- "butt",
590
- "slash"
591
- ]);
592
- const LineSchema = Base.extend({
593
- type: z.literal("line"),
594
- start: PointSchema,
595
- end: PointSchema,
596
- strokeWidth: z.number().nonnegative().default(1),
597
- endings: z.tuple([LineEndingSchema, LineEndingSchema]).optional()
598
- });
599
- const PolySchema = Base.extend({
600
- type: z.enum(["polyline", "polygon"]),
601
- vertices: z.array(PointSchema).min(2),
602
- strokeWidth: z.number().nonnegative().default(1),
603
- fill: Hex.optional()
604
- });
605
- const InkSchema = Base.extend({
606
- type: z.literal("ink"),
607
- paths: z.array(z.array(PointSchema).min(1)).min(1),
608
- strokeWidth: z.number().nonnegative().default(2)
609
- });
610
- const SuggestionSchema = z.object({
611
- jobId: z.string().optional(),
612
- confidence: z.number().min(0).max(1),
613
- reason: z.string().optional(),
614
- explanation: z.string().optional(),
615
- recommendation: z.enum([
616
- "redact",
617
- "review",
618
- "keep"
619
- ]).optional()
620
- });
621
- const RedactSchema = Base.extend({
622
- type: z.literal("redact"),
623
- /** Present when the mark came from text; absent for an area mark. */
624
- quads: z.array(QuadSchema).min(1).optional(),
625
- origin: z.enum(["user", "suggested"]).default("user"),
626
- suggestion: SuggestionSchema.optional(),
627
- overlayText: z.string().optional(),
628
- /** Colour the redaction will be filled with when applied. */
629
- fill: Hex
630
- });
631
- const AnnotationSchema = z.discriminatedUnion("type", [
632
- TextMarkupSchema,
633
- NoteSchema,
634
- FreeTextSchema,
635
- ShapeSchema,
636
- LineSchema,
637
- PolySchema,
638
- InkSchema,
639
- RedactSchema
640
- ]);
641
- const AnnotationDocumentSchema = z.object({
642
- format: z.literal(ANNOTATION_FORMAT),
643
- version: z.literal(1),
644
- documentKey: z.string().optional(),
645
- /** Validated item by item on import so unknown types can be preserved. */
646
- annotations: z.array(z.unknown())
647
- });
648
- function confidenceBucket(confidence) {
649
- if (confidence < .5) return "low";
650
- if (confidence < .8) return "medium";
651
- return "high";
652
- }
653
- //#endregion
654
- //#region src/model/annotations/embedded.ts
655
- const SUPPORTED_SUBTYPES = /* @__PURE__ */ new Set([
656
- "Highlight",
657
- "Underline",
658
- "StrikeOut",
659
- "Squiggly",
660
- "Text",
661
- "FreeText",
662
- "Square",
663
- "Circle",
664
- "Line",
665
- "Polygon",
666
- "PolyLine",
667
- "Ink",
668
- "Redact"
669
- ]);
670
- function pdfColorToHex(c, fallback) {
671
- if (!c || c.length < 3) return fallback;
672
- return `#${[
673
- c[0],
674
- c[1],
675
- c[2]
676
- ].map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
677
- }
678
- /** PDF.js hands back `rect` already in user space, but not always ordered; normalise with min/max. */
679
- function rectFromPdf(r) {
680
- return rectFromPoints({
681
- x: r[0],
682
- y: r[1]
683
- }, {
684
- x: r[2],
685
- y: r[3]
686
- });
687
- }
688
- const ICONS = {
689
- comment: "comment",
690
- note: "note",
691
- help: "help",
692
- insert: "insert",
693
- key: "key",
694
- paragraph: "paragraph",
695
- newparagraph: "paragraph"
696
- };
697
- const ENDINGS = {
698
- none: "none",
699
- openarrow: "openArrow",
700
- closedarrow: "closedArrow",
701
- square: "square",
702
- circle: "circle",
703
- diamond: "diamond",
704
- butt: "butt",
705
- slash: "slash",
706
- rslash: "slash"
707
- };
708
- /**
709
- * Imported ids are `pdf:<page>:<pdfjsId>` (page is 0-based, pdfjsId is the annotation's object
710
- * reference such as `12R`). They are stable across opens of the same file, and `inReplyTo`
711
- * uses the same scheme so threads resolve.
712
- */
713
- function importedId(page, pdfjsId) {
714
- return `pdf:${page}:${pdfjsId}`;
715
- }
716
- function importEmbeddedPage(page, raw, importedAt) {
717
- const counts = {};
718
- const annotations = [];
719
- for (const data of raw) {
720
- counts[data.subtype] = (counts[data.subtype] ?? 0) + 1;
721
- if (!SUPPORTED_SUBTYPES.has(data.subtype) || data.hidden || data.noView) continue;
722
- const converted = convert(page, data, importedAt);
723
- if (converted) annotations.push(converted);
724
- }
725
- return {
726
- annotations,
727
- counts
728
- };
729
- }
730
- function summarise(perPage) {
731
- const totals = /* @__PURE__ */ new Map();
732
- for (const counts of perPage) for (const [k, v] of Object.entries(counts)) totals.set(k, (totals.get(k) ?? 0) + v);
733
- return [...totals.entries()].map(([subtype, count]) => ({
734
- subtype,
735
- count,
736
- supported: SUPPORTED_SUBTYPES.has(subtype)
737
- })).sort((a, b) => a.subtype < b.subtype ? -1 : a.subtype > b.subtype ? 1 : 0);
738
- }
739
- function convert(page, d, importedAt) {
740
- const created = parsePdfDate(d.creationDate, parsePdfDate(d.modificationDate, importedAt));
741
- const author = d.titleObj?.str;
742
- const contents = d.contentsObj?.str;
743
- const stroke = d.borderStyle?.width ?? 1;
744
- const quads = d.quadPoints ? quadsFromFlat(d.quadPoints) : [];
745
- const optional = (v) => v !== void 0 && v !== "";
746
- const withColor = (type, source = d.color) => pdfColorToHex(source, staticDefaults.color(type));
747
- const base = (type, color = withColor(type)) => ({
748
- id: importedId(page, d.id),
749
- page,
750
- rect: rectFromPdf(d.rect),
751
- color,
752
- opacity: d.opacity ?? 1,
753
- ...optional(author) ? { author } : {},
754
- ...optional(contents) ? { contents } : {},
755
- ...d.inReplyTo ? { inReplyTo: importedId(page, d.inReplyTo) } : {},
756
- createdAt: created,
757
- modifiedAt: parsePdfDate(d.modificationDate, created),
758
- source: "pdf",
759
- locked: true
760
- });
761
- const fillOf = (c) => c && c.length >= 3 ? { fill: pdfColorToHex(c, "#FFFFFF") } : {};
762
- let candidate;
763
- switch (d.subtype) {
764
- case "Highlight":
765
- case "Underline":
766
- case "StrikeOut":
767
- case "Squiggly": {
768
- if (quads.length === 0) return void 0;
769
- const type = d.subtype === "StrikeOut" ? "strikeout" : d.subtype.toLowerCase();
770
- candidate = {
771
- ...base(type),
772
- type,
773
- quads
774
- };
775
- break;
776
- }
777
- case "Text":
778
- candidate = {
779
- ...base("note"),
780
- type: "note",
781
- icon: ICONS[(d.name ?? "comment").toLowerCase()] ?? "comment"
782
- };
783
- break;
784
- case "FreeText": {
785
- const da = d.defaultAppearanceData;
786
- candidate = {
787
- ...base("freeText", pdfColorToHex(da?.fontColor, staticDefaults.color("freeText"))),
788
- type: "freeText",
789
- text: contents ?? "",
790
- ...da && da.fontSize > 0 ? { fontSize: da.fontSize } : {}
791
- };
792
- break;
793
- }
794
- case "Square":
795
- case "Circle": {
796
- const type = d.subtype.toLowerCase();
797
- candidate = {
798
- ...base(type),
799
- type,
800
- strokeWidth: stroke,
801
- ...fillOf(d.interiorColor)
802
- };
803
- break;
804
- }
805
- case "Line": {
806
- const l = d.lineCoordinates ?? [
807
- d.rect[0],
808
- d.rect[1],
809
- d.rect[2],
810
- d.rect[3]
811
- ];
812
- const endings = d.lineEndings?.map((e) => ENDINGS[e.toLowerCase()] ?? "none");
813
- candidate = {
814
- ...base("line"),
815
- type: "line",
816
- start: {
817
- x: l[0],
818
- y: l[1]
819
- },
820
- end: {
821
- x: l[2],
822
- y: l[3]
823
- },
824
- strokeWidth: stroke,
825
- ...endings && endings.length === 2 && endings.some((e) => e !== "none") ? { endings } : {}
826
- };
827
- break;
828
- }
829
- case "Polygon":
830
- case "PolyLine": {
831
- const type = d.subtype === "Polygon" ? "polygon" : "polyline";
832
- const vertices = d.vertices ? pointsFromFlat(d.vertices) : [];
833
- if (vertices.length < 2) return void 0;
834
- candidate = {
835
- ...base(type),
836
- type,
837
- vertices,
838
- strokeWidth: stroke,
839
- ...fillOf(d.interiorColor)
840
- };
841
- break;
842
- }
843
- case "Ink": {
844
- const paths = (d.inkLists ?? []).map((p) => pointsFromFlat(p)).filter((p) => p.length > 0);
845
- if (paths.length === 0) return void 0;
846
- candidate = {
847
- ...base("ink"),
848
- type: "ink",
849
- paths,
850
- strokeWidth: stroke
851
- };
852
- break;
853
- }
854
- case "Redact":
855
- candidate = {
856
- ...base("redact"),
857
- type: "redact",
858
- origin: "user",
859
- fill: pdfColorToHex(d.interiorColor, staticDefaults.fill()),
860
- ...quads.length > 0 ? { quads } : {}
861
- };
862
- break;
863
- default: return;
864
- }
865
- const parsed = AnnotationSchema.safeParse(candidate);
866
- return parsed.success ? parsed.data : void 0;
867
- }
868
- //#endregion
869
- //#region src/model/env.ts
870
- /** True only when the build or runtime says production; browsers without `process` count as dev. */
871
- function isProduction() {
872
- try {
873
- return process.env.NODE_ENV === "production";
874
- } catch {
875
- return false;
876
- }
877
- }
878
- //#endregion
879
- //#region src/model/annotations/history.ts
880
- /**
881
- * Snapshot-based undo and redo over an uncontrolled store.
882
- *
883
- * In controlled mode the host owns the list, so history is the host's concern (the Vue layer
884
- * offers `useHistory`). There `createHistory` returns an inert History: `canUndo` and `canRedo`
885
- * stay false, `undo` and `redo` do nothing, and nothing is recorded.
886
- */
887
- function createHistory(store, opts = {}) {
888
- const canUndo = writable(false);
889
- const canRedo = writable(false);
890
- if (store.mode === "controlled") return {
891
- canUndo: readonly(canUndo),
892
- canRedo: readonly(canRedo),
893
- undo() {},
894
- redo() {},
895
- clear() {},
896
- batch(fn) {
897
- fn();
898
- },
899
- destroy() {}
900
- };
901
- const limit = opts.limit ?? 200;
902
- const undoStack = [];
903
- const redoStack = [];
904
- let previous = store.list.get();
905
- let replaying = false;
906
- /** Open batch depth; while above zero, changes update `previous` but push no entry. */
907
- let depth = 0;
908
- /** The list when the outermost batch opened (or was restarted by a replace). */
909
- let batchStart = null;
910
- const sync = () => {
911
- canUndo.set(undoStack.length > 0);
912
- canRedo.set(redoStack.length > 0);
913
- };
914
- let destroyed = false;
915
- const unsubscribe = store.on("change", (change) => {
916
- const current = store.list.get();
917
- if (replaying || change.kind === "replace" && change.reason === "history") {
918
- previous = current;
919
- return;
920
- }
921
- if (change.kind === "replace") {
922
- undoStack.length = 0;
923
- redoStack.length = 0;
924
- previous = current;
925
- if (depth > 0) batchStart = current;
926
- sync();
927
- return;
928
- }
929
- if (depth > 0) {
930
- previous = current;
931
- return;
932
- }
933
- undoStack.push({
934
- before: previous,
935
- after: current
936
- });
937
- if (undoStack.length > limit) undoStack.shift();
938
- redoStack.length = 0;
939
- previous = current;
940
- sync();
941
- });
942
- const apply = (list) => {
943
- replaying = true;
944
- try {
945
- store.replaceAll([...list], "history");
946
- } finally {
947
- replaying = false;
948
- }
949
- };
950
- return {
951
- canUndo: readonly(canUndo),
952
- canRedo: readonly(canRedo),
953
- undo() {
954
- const entry = undoStack.pop();
955
- if (destroyed || !entry) return;
956
- redoStack.push(entry);
957
- apply(entry.before);
958
- sync();
959
- },
960
- redo() {
961
- const entry = redoStack.pop();
962
- if (destroyed || !entry) return;
963
- undoStack.push(entry);
964
- apply(entry.after);
965
- sync();
966
- },
967
- batch(fn) {
968
- if (destroyed) {
969
- fn();
970
- return;
971
- }
972
- if (depth++ === 0) batchStart = store.list.get();
973
- try {
974
- const result = fn();
975
- if (result !== null && typeof result === "object" && typeof result.then === "function" && !isProduction()) console.warn("[inkbar] history.batch(fn): fn returned a promise. Changes after its first await are not part of the batch.");
976
- } finally {
977
- if (--depth === 0 && !destroyed) {
978
- const before = batchStart;
979
- batchStart = null;
980
- const after = store.list.get();
981
- if (before && !sameAnnotations(before, after)) {
982
- undoStack.push({
983
- before,
984
- after
985
- });
986
- if (undoStack.length > limit) undoStack.shift();
987
- redoStack.length = 0;
988
- sync();
989
- }
990
- }
991
- }
992
- },
993
- destroy() {
994
- destroyed = true;
995
- batchStart = null;
996
- unsubscribe();
997
- undoStack.length = 0;
998
- redoStack.length = 0;
999
- sync();
1000
- },
1001
- clear() {
1002
- undoStack.length = 0;
1003
- redoStack.length = 0;
1004
- previous = store.list.get();
1005
- sync();
1006
- }
1007
- };
1008
- }
1009
- /** The same annotation objects in the same order. */
1010
- function sameAnnotations(a, b) {
1011
- return a === b || a.length === b.length && a.every((x, i) => x === b[i]);
1012
- }
1013
- //#endregion
1014
- //#region src/model/annotations/json.ts
1015
- const KNOWN_TYPES = new Set(AnnotationSchema.options.flatMap((o) => [...o.shape.type._zod.values]));
1016
- /**
1017
- * Session annotations plus any preserved foreign items. Embedded (`source: 'pdf'`) ones stay in
1018
- * the file.
1019
- *
1020
- * The result is a JSON copy (`JSON.parse(JSON.stringify(...))`), the same data a serialised
1021
- * export would carry: editing it never touches the store, and values JSON cannot hold, such as
1022
- * functions or `undefined` inside `custom`, are left out exactly as `JSON.stringify` would.
1023
- *
1024
- * Without foreign items the result validates against this version's `schema.json`. With them it
1025
- * validates only against the newer schema that produced them, since this version's schema
1026
- * describes only the types it knows.
1027
- */
1028
- function exportAnnotations(store, preserved = [], documentKey) {
1029
- const session = store.list.get().filter((a) => a.source === "session");
1030
- return {
1031
- format: ANNOTATION_FORMAT,
1032
- version: 1,
1033
- ...documentKey === void 0 ? {} : { documentKey },
1034
- annotations: JSON.parse(JSON.stringify([...session, ...preserved]))
1035
- };
1036
- }
1037
- /**
1038
- * Validates the envelope (throws a TypeError when it is not an inkbar/annotations v1 document),
1039
- * then each item on its own. Embedded annotations are never touched; imported items become
1040
- * session annotations.
1041
- */
1042
- function importAnnotations(store, data, opts) {
1043
- const plan = planImport(store, data, opts);
1044
- plan.apply();
1045
- return plan.result;
1046
- }
1047
- /**
1048
- * @internal Validation half of `importAnnotations`, so the document can record foreign items
1049
- * before the store publishes the change.
1050
- *
1051
- * Ids are unique across the payload: the first item with an id claims it, and later ones (known
1052
- * or foreign) are reported in `errors`, even when the first was itself rejected. An id that
1053
- * matches an embedded annotation is rejected for known and foreign items alike. In `merge` mode an incoming item replaces whatever session
1054
- * annotation has its id, whether the incoming item is known or foreign.
1055
- */
1056
- function planImport(store, data, opts) {
1057
- const raw = typeof data === "string" ? JSON.parse(data) : data;
1058
- const envelope = AnnotationDocumentSchema.safeParse(raw);
1059
- if (!envelope.success) throw new TypeError(`Expected an ${ANNOTATION_FORMAT} v1 document: ${envelope.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
1060
- const embeddedIds = new Set(store.list.get().filter((a) => a.source === "pdf").map((a) => a.id));
1061
- const result = {
1062
- imported: 0,
1063
- unknown: [],
1064
- errors: []
1065
- };
1066
- const incoming = [];
1067
- /** id -> index of the first item that used it, whether that item was accepted or not. */
1068
- const seen = /* @__PURE__ */ new Map();
1069
- /** Ids the import actually brings in. */
1070
- const accepted = /* @__PURE__ */ new Set();
1071
- /**
1072
- * Reserves `id` for the item at `index`. Later items with the same id are duplicates even
1073
- * when this one is rejected for reusing an embedded id.
1074
- */
1075
- const claim = (id, index) => {
1076
- const first = seen.get(id);
1077
- if (first !== void 0) {
1078
- result.errors.push({
1079
- index,
1080
- issues: [`id: duplicate id "${id}", already used by item ${first}`]
1081
- });
1082
- return false;
1083
- }
1084
- seen.set(id, index);
1085
- if (embeddedIds.has(id)) {
1086
- result.errors.push({
1087
- index,
1088
- issues: ["id: matches an annotation embedded in the PDF, which import never replaces"]
1089
- });
1090
- return false;
1091
- }
1092
- accepted.add(id);
1093
- return true;
1094
- };
1095
- envelope.data.annotations.forEach((item, index) => {
1096
- const type = item?.type;
1097
- if (typeof type === "string" && !KNOWN_TYPES.has(type)) {
1098
- if (!isForeign(item)) result.errors.push({
1099
- index,
1100
- issues: [`type: unknown type "${type}" needs a string id and a numeric page to be preserved`]
1101
- });
1102
- else if (claim(item.id, index)) result.unknown.push(item);
1103
- return;
1104
- }
1105
- const parsed = AnnotationSchema.safeParse(item);
1106
- if (!parsed.success) {
1107
- result.errors.push({
1108
- index,
1109
- issues: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
1110
- });
1111
- return;
1112
- }
1113
- if (!claim(parsed.data.id, index)) return;
1114
- incoming.push({
1115
- ...parsed.data,
1116
- source: "session"
1117
- });
1118
- result.imported++;
1119
- });
1120
- if (result.unknown.length > 0 && !isProduction()) {
1121
- const types = [...new Set(result.unknown.map((u) => u.type))];
1122
- console.warn(`[inkbar] importAnnotations kept ${result.unknown.length} annotation(s) of unknown type (${types.join(", ")}). They are not shown, and exportAnnotations writes them back out.`);
1123
- }
1124
- const ids = accepted;
1125
- return {
1126
- result,
1127
- ids,
1128
- apply() {
1129
- const current = store.list.get();
1130
- const embedded = current.filter((a) => a.source === "pdf");
1131
- if (opts.mode === "replace") {
1132
- store.replaceAll([...embedded, ...incoming], "import");
1133
- return;
1134
- }
1135
- const byId = new Map(current.filter((a) => a.source === "session" && !ids.has(a.id)).map((a) => [a.id, a]));
1136
- for (const a of incoming) byId.set(a.id, a);
1137
- store.replaceAll([...embedded, ...byId.values()], "import");
1138
- }
1139
- };
1140
- }
1141
- function isForeign(item) {
1142
- if (typeof item !== "object" || item === null || Array.isArray(item)) return false;
1143
- const { type, id, page } = item;
1144
- return typeof type === "string" && typeof id === "string" && typeof page === "number";
1145
- }
1146
- //#endregion
1147
- //#region src/model/annotations/store.ts
1148
- /** Keys a locked annotation refuses to change. */
1149
- const LOCKED_KEYS = /* @__PURE__ */ new Set([
1150
- "page",
1151
- "rect",
1152
- "quads",
1153
- "paths",
1154
- "start",
1155
- "end",
1156
- "vertices",
1157
- "callout",
1158
- "contents"
1159
- ]);
1160
- function keepKey(target, from, key) {
1161
- if (Object.prototype.hasOwnProperty.call(from, key)) target[key] = from[key];
1162
- else delete target[key];
1163
- }
1164
- /** `proposed` as a replacement for embedded `current`, with what the file owns kept from `current`. */
1165
- function guardEmbedded(current, proposed) {
1166
- if (proposed === current) return current;
1167
- if (proposed.type !== current.type) return current;
1168
- const candidate = {
1169
- ...proposed,
1170
- id: current.id,
1171
- createdAt: current.createdAt,
1172
- source: "pdf"
1173
- };
1174
- keepKey(candidate, current, "locked");
1175
- if (current.locked) for (const key of LOCKED_KEYS) keepKey(candidate, current, key);
1176
- const parsed = AnnotationSchema.safeParse(candidate);
1177
- return parsed.success ? parsed.data : current;
1178
- }
1179
- function deriveRect(input) {
1180
- switch (input.type) {
1181
- case "highlight":
1182
- case "underline":
1183
- case "strikeout":
1184
- case "squiggly": return unionRects(input.quads.map(rectFromQuad));
1185
- case "redact":
1186
- if (input.quads) return unionRects(input.quads.map(rectFromQuad));
1187
- break;
1188
- case "ink": {
1189
- const pad = (input.strokeWidth ?? 2) / 2;
1190
- const r = bounds(input.paths.flat());
1191
- return {
1192
- x: r.x - pad,
1193
- y: r.y - pad,
1194
- width: r.width + pad * 2,
1195
- height: r.height + pad * 2
1196
- };
1197
- }
1198
- case "line": return bounds([input.start, input.end]);
1199
- case "polyline":
1200
- case "polygon": return bounds(input.vertices);
1201
- }
1202
- if (input.rect) return input.rect;
1203
- throw new TypeError(`rect is required for ${input.type}`);
1204
- }
1205
- function bounds(pts) {
1206
- const xs = pts.map((p) => p.x);
1207
- const ys = pts.map((p) => p.y);
1208
- const x = Math.min(...xs);
1209
- const y = Math.min(...ys);
1210
- return {
1211
- x,
1212
- y,
1213
- width: Math.max(...xs) - x,
1214
- height: Math.max(...ys) - y
1215
- };
1216
- }
1217
- /** Pending-map key; only annotation changes are tracked, and the prefix keeps ids apart from any other slot. */
1218
- function changeKey(change) {
1219
- return `annotation:${change.kind === "update" ? change.id : change.annotation.id}`;
1220
- }
1221
- /** Structural equality for plain JSON values, ignoring `modifiedAt` at the top level. */
1222
- function sameIgnoringModifiedAt(a, b) {
1223
- const { modifiedAt: _a, ...restA } = a;
1224
- const { modifiedAt: _b, ...restB } = b;
1225
- return deepEqual(restA, restB);
1226
- }
1227
- function deepEqual(a, b) {
1228
- if (Object.is(a, b)) return true;
1229
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1230
- if (Array.isArray(a) !== Array.isArray(b)) return false;
1231
- const ka = Object.keys(a);
1232
- const kb = Object.keys(b);
1233
- if (ka.length !== kb.length) return false;
1234
- return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
1235
- }
1236
- function isApplied(change, current) {
1237
- switch (change.kind) {
1238
- case "add": return current.some((a) => a.id === change.annotation.id);
1239
- case "remove": return !current.some((a) => a.id === change.annotation.id);
1240
- case "update": {
1241
- const mine = current.find((a) => a.id === change.id);
1242
- if (!mine) return false;
1243
- return mine === change.after || sameIgnoringModifiedAt(mine, change.after);
1244
- }
1245
- case "replace": return true;
1246
- }
1247
- }
1248
- function createAnnotationStore(opts) {
1249
- const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1250
- const nextId = opts.id ?? (() => v7());
1251
- const list = writable([...opts.mode.initial ?? []]);
1252
- const selection = writable([]);
1253
- const emitter = new Emitter();
1254
- const controlled = opts.mode.mode === "controlled" ? opts.mode : void 0;
1255
- const pending = /* @__PURE__ */ new Map();
1256
- /**
1257
- * Controlled mode: the list as the store last proposed it, embedded items included. Later
1258
- * proposals build on it, so several mutations in a row (one highlight per page, a bulk
1259
- * remove, add then select) all reach the host. It resets to the applied list when the host
1260
- * pushes a value or a pending change's check fires without one.
1261
- */
1262
- let proposed;
1263
- let destroyed = false;
1264
- /** What mutations build on: the pending proposal in controlled mode, else the list. */
1265
- const basis = () => proposed ?? list.get();
1266
- const findIn = (items, id) => items.find((a) => a.id === id);
1267
- function completeInput(input) {
1268
- const ts = now();
1269
- const candidate = {
1270
- ...input,
1271
- id: input.id ?? nextId(),
1272
- rect: input.rect ?? deriveRect(input),
1273
- color: input.color ?? opts.defaults.color(input.type),
1274
- opacity: input.opacity ?? opts.defaults.opacity(input.type),
1275
- createdAt: ts,
1276
- modifiedAt: ts,
1277
- source: "session"
1278
- };
1279
- if (input.type === "redact") {
1280
- if (input.fill === void 0) candidate.fill = opts.defaults.fill();
1281
- if (input.origin === "suggested" && input.suggestion && input.color === void 0) candidate.color = opts.defaults.suggestion(confidenceBucket(input.suggestion.confidence));
1282
- }
1283
- return AnnotationSchema.parse(candidate);
1284
- }
1285
- function clearPending(key) {
1286
- const entry = pending.get(key);
1287
- if (!entry) return;
1288
- clearTimeout(entry.timer);
1289
- pending.delete(key);
1290
- }
1291
- function warnIfIgnored(key) {
1292
- const entry = pending.get(key);
1293
- if (!entry) return;
1294
- pending.delete(key);
1295
- if (pending.size === 0) proposed = void 0;
1296
- if (isApplied(entry.change, list.get()) || isProduction()) return;
1297
- console.warn(`[inkbar] ControlledMutationIgnored: the host did not apply a ${entry.change.kind} for annotation ${entry.id}. In controlled mode, pass the list from onChange back into the store.`);
1298
- }
1299
- function commit(next, change) {
1300
- if (controlled) {
1301
- if (change.kind !== "replace") {
1302
- const key = changeKey(change);
1303
- clearPending(key);
1304
- const entry = {
1305
- id: change.kind === "update" ? change.id : change.annotation.id,
1306
- change,
1307
- timer: setTimeout(() => {
1308
- entry.timer = setTimeout(() => {
1309
- warnIfIgnored(key);
1310
- }, 0);
1311
- }, 0)
1312
- };
1313
- pending.set(key, entry);
1314
- }
1315
- proposed = change.kind === "replace" ? [...list.get().filter((a) => a.source === "pdf"), ...next.filter((a) => a.source !== "pdf")] : next;
1316
- controlled.onChange(next.filter((a) => a.source !== "pdf"), change);
1317
- return;
1318
- }
1319
- list.set(next);
1320
- emitter.emit("change", change);
1321
- }
1322
- /**
1323
- * Uncontrolled `replaceAll` cannot drop or forge embedded annotations: every current embedded
1324
- * item stays (taking the list's version of it, guarded like `update`, when the list has one),
1325
- * and items claiming `source: 'pdf'` that the store does not hold are dropped. History
1326
- * snapshots contain the embedded items, so undo and redo of their edits still work.
1327
- */
1328
- function withEmbedded(proposed) {
1329
- const current = new Map(list.get().filter((a) => a.source === "pdf").map((a) => [a.id, a]));
1330
- const seen = /* @__PURE__ */ new Set();
1331
- const out = [];
1332
- for (const a of proposed) {
1333
- const mine = current.get(a.id);
1334
- if (mine) {
1335
- if (!seen.has(a.id)) out.push(guardEmbedded(mine, a));
1336
- seen.add(a.id);
1337
- } else if (a.source !== "pdf") out.push(a);
1338
- }
1339
- return [...[...current.values()].filter((a) => !seen.has(a.id)), ...out];
1340
- }
1341
- function pruneSelection(next) {
1342
- const known = new Set(next.map((a) => a.id));
1343
- const current = selection.get();
1344
- if (current.every((id) => known.has(id))) return;
1345
- selection.set(current.filter((id) => known.has(id)));
1346
- }
1347
- return {
1348
- mode: opts.mode.mode,
1349
- list: readonly(list),
1350
- selection: readonly(selection),
1351
- defaults: opts.defaults,
1352
- get: (id) => list.get().find((a) => a.id === id),
1353
- _getProposed: (id) => findIn(basis(), id),
1354
- byPage: (page) => list.get().filter((a) => a.page === page),
1355
- completeInput,
1356
- add(input) {
1357
- const a = completeInput(input);
1358
- if (findIn(basis(), a.id)) throw new Error(`An annotation with id ${a.id} already exists`);
1359
- if (destroyed) return a;
1360
- commit([...basis(), a], {
1361
- kind: "add",
1362
- annotation: a
1363
- });
1364
- return a;
1365
- },
1366
- update(id, patch) {
1367
- const before = findIn(basis(), id);
1368
- if (destroyed || !before) return void 0;
1369
- if (before.locked && Object.keys(patch).some((k) => LOCKED_KEYS.has(k))) return void 0;
1370
- const candidate = {
1371
- ...before,
1372
- ...patch,
1373
- id: before.id,
1374
- type: before.type,
1375
- createdAt: before.createdAt,
1376
- source: before.source,
1377
- modifiedAt: now()
1378
- };
1379
- if (before.source === "pdf") keepKey(candidate, before, "locked");
1380
- const after = AnnotationSchema.parse(candidate);
1381
- const swap = (items) => items.map((a) => a.id === id ? after : a);
1382
- const next = swap(basis());
1383
- const change = {
1384
- kind: "update",
1385
- id,
1386
- before,
1387
- after
1388
- };
1389
- if (controlled && before.source === "pdf") {
1390
- list.set(swap(list.get()));
1391
- if (proposed) proposed = next;
1392
- emitter.emit("change", change);
1393
- } else commit(next, change);
1394
- return after;
1395
- },
1396
- remove(id) {
1397
- const items = basis();
1398
- const target = findIn(items, id);
1399
- if (destroyed || !target || target.locked) return false;
1400
- const doomed = /* @__PURE__ */ new Set([id]);
1401
- let grew = true;
1402
- while (grew) {
1403
- grew = false;
1404
- for (const a of items) if (a.inReplyTo !== void 0 && doomed.has(a.inReplyTo) && !doomed.has(a.id)) {
1405
- doomed.add(a.id);
1406
- grew = true;
1407
- }
1408
- }
1409
- const next = items.filter((a) => !doomed.has(a.id));
1410
- commit(next, {
1411
- kind: "remove",
1412
- annotation: target,
1413
- removed: [target, ...items.filter((a) => a.id !== id && doomed.has(a.id))]
1414
- });
1415
- if (!controlled) pruneSelection(next);
1416
- return true;
1417
- },
1418
- select(ids) {
1419
- const known = new Set(basis().map((a) => a.id));
1420
- selection.set(ids.filter((id) => known.has(id)));
1421
- },
1422
- replaceAll(list, reason) {
1423
- if (destroyed) return;
1424
- const next = controlled ? [...list] : withEmbedded(list);
1425
- commit(next, {
1426
- kind: "replace",
1427
- reason
1428
- });
1429
- if (!controlled) pruneSelection(next);
1430
- },
1431
- setControlledValue(value) {
1432
- if (!controlled) throw new TypeError("setControlledValue is only valid in controlled mode");
1433
- if (destroyed) return;
1434
- const embedded = list.get().filter((a) => a.source === "pdf");
1435
- const embeddedIds = new Set(embedded.map((a) => a.id));
1436
- const next = [...embedded, ...value.filter((a) => !embeddedIds.has(a.id))];
1437
- list.set(next);
1438
- proposed = void 0;
1439
- for (const [key, entry] of [...pending]) if (isApplied(entry.change, next)) clearPending(key);
1440
- pruneSelection(next);
1441
- emitter.emit("change", {
1442
- kind: "replace",
1443
- reason: "sync"
1444
- });
1445
- },
1446
- _setEmbedded(items) {
1447
- if (destroyed) return;
1448
- const ids = new Set(items.map((a) => a.id));
1449
- const next = [...items, ...list.get().filter((a) => a.source !== "pdf" && !ids.has(a.id))];
1450
- proposed = void 0;
1451
- list.set(next);
1452
- pruneSelection(next);
1453
- emitter.emit("change", {
1454
- kind: "replace",
1455
- reason: "import"
1456
- });
1457
- },
1458
- on: (event, fn) => emitter.on(event, fn),
1459
- destroy() {
1460
- destroyed = true;
1461
- proposed = void 0;
1462
- for (const key of [...pending.keys()]) clearPending(key);
1463
- emitter.clear();
1464
- }
1465
- };
1466
- }
1467
- //#endregion
1468
- //#region src/model/page-cache.ts
1469
- function createPageCache(fetch) {
1470
- const cache = /* @__PURE__ */ new Map();
1471
- return {
1472
- get(index) {
1473
- const cached = cache.get(index);
1474
- if (cached) return cached;
1475
- const fetched = fetch(index);
1476
- cache.set(index, fetched);
1477
- fetched.catch(() => {
1478
- if (cache.get(index) === fetched) cache.delete(index);
1479
- });
1480
- return fetched;
1481
- },
1482
- clear: () => cache.clear()
1483
- };
1484
- }
1485
- //#endregion
1486
- //#region src/model/pages.ts
1487
- function pageInfoFromProxy(page, index) {
1488
- const [x1, y1, x2, y2] = page.view;
1489
- const box = {
1490
- x: Math.min(x1, x2),
1491
- y: Math.min(y1, y2),
1492
- width: Math.abs(x2 - x1),
1493
- height: Math.abs(y2 - y1)
1494
- };
1495
- return {
1496
- index,
1497
- box,
1498
- rotate: normaliseRotation(page.rotate),
1499
- userUnit: page.userUnit > 0 ? page.userUnit : 1,
1500
- width: box.width,
1501
- height: box.height
1502
- };
1503
- }
1504
- /** PDF.js already rejects /Rotate values that are not multiples of 90; this also folds negatives. */
1505
- function normaliseRotation(rotate) {
1506
- return (Math.round(rotate / 90) * 90 % 360 + 360) % 360;
1507
- }
1508
- //#endregion
1509
- //#region src/model/source.ts
1510
- function resolveSource(src, fetchImpl = globalThis.fetch) {
1511
- if (typeof src === "string") return {
1512
- key: src,
1513
- bytes: () => fetchBytes(src, fetchImpl)
1514
- };
1515
- if (src instanceof ArrayBuffer || src instanceof Uint8Array) {
1516
- const data = toBytes(src);
1517
- return {
1518
- key: `bytes:${v7()}`,
1519
- bytes: () => Promise.resolve(data.slice())
1520
- };
1521
- }
1522
- if ("url" in src) {
1523
- const { url } = src;
1524
- return {
1525
- key: src.key,
1526
- bytes: () => fetchBytes(url, fetchImpl)
1527
- };
1528
- }
1529
- if ("data" in src) {
1530
- const data = toBytes(src.data);
1531
- return {
1532
- key: src.key,
1533
- bytes: () => Promise.resolve(data.slice())
1534
- };
1535
- }
1536
- const blob = src;
1537
- return {
1538
- key: `bytes:${v7()}`,
1539
- bytes: async () => new Uint8Array(await blob.arrayBuffer())
1540
- };
1541
- }
1542
- /**
1543
- * A view over the host's bytes. `bytes()` hands PDF.js a copy each time: PDF.js transfers the
1544
- * buffer it is given to its worker, which would empty the host's array and break a reopen
1545
- * (a password retry, for one).
1546
- */
1547
- function toBytes(data) {
1548
- return data instanceof Uint8Array ? data : new Uint8Array(data);
1549
- }
1550
- async function fetchBytes(url, fetchImpl) {
1551
- if (typeof fetchImpl !== "function") throw new InkError("SourceFetchFailed", "No fetch implementation available for URL sources", { url });
1552
- let res;
1553
- try {
1554
- res = await fetchImpl(url, { credentials: "same-origin" });
1555
- } catch (cause) {
1556
- throw new InkError("SourceFetchFailed", `Could not fetch ${url}`, { url }, cause);
1557
- }
1558
- if (!res.ok) throw new InkError("SourceFetchFailed", `Fetching ${url} returned HTTP ${res.status}`, {
1559
- url,
1560
- status: res.status
1561
- });
1562
- try {
1563
- return new Uint8Array(await res.arrayBuffer());
1564
- } catch (cause) {
1565
- throw new InkError("SourceFetchFailed", `Reading the body of ${url} failed`, { url }, cause);
1566
- }
1567
- }
1568
- //#endregion
1569
- //#region src/model/text.ts
1570
- /** Fractions of the font size used when PDF.js has no usable metrics for the font. */
1571
- const FALLBACK_ASCENT = .75;
1572
- const FALLBACK_DESCENT = .25;
1573
- /**
1574
- * Converts `getTextContent()` output to user-space boxes.
1575
- *
1576
- * For an item with transform [a, b, c, d, e, f] (already in user space):
1577
- * - (e, f) is the start of the run on the baseline;
1578
- * - u = (a, b) / |(a, b)| is the baseline direction, along which the run advances `width`;
1579
- * - v = (c, d) / |(c, d)| is the glyph-up direction, and `fontSize` = |(c, d)|;
1580
- * - the box spans from `descent * fontSize` below the baseline to `ascent * fontSize` above it,
1581
- * using the font's ascent/descent from `styles` when PDF.js provides them, else 0.75 / 0.25.
1582
- *
1583
- * So p3 = (e, f) - v * descent * fontSize, p4 = p3 + u * width, p1 = p3 + v * (ascent + descent) * fontSize,
1584
- * p2 = p4 + (p1 - p3). `rect` is the axis-aligned bounding box of the quad.
1585
- */
1586
- function textContentFromPdfjs(page, tc) {
1587
- const items = [];
1588
- for (const raw of tc.items) {
1589
- if (!("str" in raw)) continue;
1590
- const item = raw;
1591
- const [a, b, c, d, e, f] = item.transform;
1592
- const baseLen = Math.hypot(a, b);
1593
- const fontSize = Math.hypot(c, d);
1594
- const u = baseLen === 0 ? {
1595
- x: 1,
1596
- y: 0
1597
- } : {
1598
- x: a / baseLen,
1599
- y: b / baseLen
1600
- };
1601
- const v = fontSize === 0 ? {
1602
- x: -u.y,
1603
- y: u.x
1604
- } : {
1605
- x: c / fontSize,
1606
- y: d / fontSize
1607
- };
1608
- const { ascent, descent } = metrics(tc.styles[item.fontName]);
1609
- const below = descent * fontSize;
1610
- const total = (ascent + descent) * fontSize;
1611
- const w = item.width;
1612
- const p3 = {
1613
- x: e - v.x * below,
1614
- y: f - v.y * below
1615
- };
1616
- const p4 = {
1617
- x: p3.x + u.x * w,
1618
- y: p3.y + u.y * w
1619
- };
1620
- const quad = {
1621
- p1: {
1622
- x: p3.x + v.x * total,
1623
- y: p3.y + v.y * total
1624
- },
1625
- p2: {
1626
- x: p4.x + v.x * total,
1627
- y: p4.y + v.y * total
1628
- },
1629
- p3,
1630
- p4
1631
- };
1632
- items.push({
1633
- str: item.str,
1634
- dir: isDir(item.dir) ? item.dir : "ltr",
1635
- fontName: item.fontName,
1636
- fontSize,
1637
- hasEOL: item.hasEOL,
1638
- quad,
1639
- rect: rectFromQuad(quad)
1640
- });
1641
- }
1642
- return {
1643
- page,
1644
- items
1645
- };
1646
- }
1647
- function metrics(style) {
1648
- if (!style || style.vertical) return {
1649
- ascent: FALLBACK_ASCENT,
1650
- descent: FALLBACK_DESCENT
1651
- };
1652
- const ascent = style.ascent;
1653
- const descent = Math.abs(style.descent);
1654
- if (!(ascent > 0) || !Number.isFinite(descent) || ascent + descent <= 0) return {
1655
- ascent: FALLBACK_ASCENT,
1656
- descent: FALLBACK_DESCENT
1657
- };
1658
- return {
1659
- ascent,
1660
- descent
1661
- };
1662
- }
1663
- function isDir(dir) {
1664
- return dir === "ltr" || dir === "rtl" || dir === "ttb";
1665
- }
1666
- //#endregion
1667
- //#region src/model/document.ts
1668
- /**
1669
- * Opens a document. Resolves once loading has finished, whether it succeeded or not:
1670
- * check `status` or await `ready`, which rejects with the InkError. Only a missing
1671
- * `assets` option (a programmer error) makes this promise itself reject.
1672
- */
1673
- async function openDocument(source, options) {
1674
- assertAssets(options?.assets);
1675
- const resolved = resolveSource(source, options.fetch);
1676
- const status = writable({ state: "loading" });
1677
- const pages = [];
1678
- const info = {
1679
- pageCount: 0,
1680
- isEncrypted: false
1681
- };
1682
- let pageCache;
1683
- let destroyed = false;
1684
- let handles;
1685
- let destroyTask;
1686
- const store = createAnnotationStore({
1687
- mode: options.annotations ?? { mode: "uncontrolled" },
1688
- defaults: options.defaults ?? staticDefaults
1689
- });
1690
- const embedded = [];
1691
- let preserved = [];
1692
- const destroyedError = () => new InkError("DocumentDestroyed", "The document has been destroyed");
1693
- const ready = (async () => {
1694
- const pdfjs = await loadPdfjs(options.assets);
1695
- const { task, proxy } = await openProxy(pdfjs, await resolved.bytes(), {
1696
- assets: options.assets,
1697
- password: options.password,
1698
- verbosity: options.verbosity
1699
- });
1700
- destroyTask = () => task.destroy();
1701
- const cache = createPageCache((index) => proxy.getPage(index + 1));
1702
- pageCache = cache;
1703
- const getPage = (index) => {
1704
- if (destroyed) return Promise.reject(destroyedError());
1705
- if (!Number.isInteger(index) || index < 0 || index >= proxy.numPages) return Promise.reject(new InkError("PageOutOfRange", `Page ${index} does not exist`, {
1706
- index,
1707
- pageCount: proxy.numPages
1708
- }));
1709
- return cache.get(index);
1710
- };
1711
- handles = {
1712
- module: pdfjs,
1713
- proxy,
1714
- getPage
1715
- };
1716
- const docInfo = (await proxy.getMetadata().catch(() => {}))?.info ?? {};
1717
- const title = nonEmptyString(docInfo.Title);
1718
- const author = nonEmptyString(docInfo.Author);
1719
- if (title !== void 0) info.title = title;
1720
- if (author !== void 0) info.author = author;
1721
- info.pageCount = proxy.numPages;
1722
- info.isEncrypted = docInfo.EncryptFilterName !== null && docInfo.EncryptFilterName !== void 0;
1723
- for (let i = 0; i < proxy.numPages; i++) pages.push(pageInfoFromProxy(await getPage(i), i));
1724
- const importedAt = (/* @__PURE__ */ new Date()).toISOString();
1725
- const perPage = await mapInOrder(proxy.numPages, EMBEDDED_CONCURRENCY, async (i) => {
1726
- return importEmbeddedPage(i, await (await getPage(i)).getAnnotations().catch(() => {
1727
- return [];
1728
- }), importedAt);
1729
- });
1730
- embedded.push(...summarise(perPage.map((r) => r.counts)));
1731
- const fromFile = perPage.flatMap((r) => r.annotations);
1732
- if (fromFile.length > 0) store._setEmbedded(fromFile);
1733
- })().then(() => {
1734
- status.set({ state: "ready" });
1735
- }, (cause) => {
1736
- const error = isInkError(cause) ? cause : new InkError("InvalidPdf", errorMessage(cause), {}, cause);
1737
- if (error.code === "PasswordRequired") info.isEncrypted = true;
1738
- status.set({
1739
- state: "error",
1740
- error
1741
- });
1742
- throw error;
1743
- });
1744
- await ready.catch(() => void 0);
1745
- const historyOption = options.history ?? true;
1746
- const historyLimit = historyOption === false ? 0 : historyOption === true ? void 0 : historyOption.limit;
1747
- const history = createHistory(store, historyLimit === void 0 ? {} : { limit: historyLimit });
1748
- return {
1749
- key: resolved.key,
1750
- status: readonly(status),
1751
- ready,
1752
- pages,
1753
- info,
1754
- async text(page) {
1755
- if (destroyed) throw destroyedError();
1756
- if (!handles) throw statusError(status.get());
1757
- return textContentFromPdfjs(page, await (await handles.getPage(page)).getTextContent());
1758
- },
1759
- annotations: store,
1760
- embedded,
1761
- history,
1762
- exportAnnotations(format) {
1763
- if (format !== "json") throw new TypeError(`Unsupported export format ${String(format)}`);
1764
- return exportAnnotations(store, preserved, resolved.key);
1765
- },
1766
- importAnnotations(data, opts = { mode: "replace" }) {
1767
- const plan = planImport(store, data, opts);
1768
- preserved = opts.mode === "replace" ? plan.result.unknown : [...preserved.filter((u) => !plan.ids.has(u.id)), ...plan.result.unknown];
1769
- plan.apply();
1770
- return plan.result;
1771
- },
1772
- save() {
1773
- return Promise.reject(new InkError("EngineUnavailable", "Saving needs @inkbar/engine, which is not registered. Use exportAnnotations(\"json\") to keep annotations without writing the PDF."));
1774
- },
1775
- destroy() {
1776
- if (destroyed) return;
1777
- destroyed = true;
1778
- history.destroy();
1779
- store.destroy();
1780
- pageCache?.clear();
1781
- if (destroyTask) teardown(destroyTask);
1782
- },
1783
- get _pdfjs() {
1784
- if (!handles) throw statusError(status.get());
1785
- return handles;
1786
- }
1787
- };
1788
- }
1789
- const EMBEDDED_CONCURRENCY = 8;
1790
- /** Runs `task(0..count-1)` with at most `limit` in flight; results come back in index order. */
1791
- async function mapInOrder(count, limit, task) {
1792
- const results = new Array(count);
1793
- let next = 0;
1794
- const worker = async () => {
1795
- while (next < count) {
1796
- const i = next++;
1797
- results[i] = await task(i);
1798
- }
1799
- };
1800
- await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
1801
- return results;
1802
- }
1803
- function teardown(destroyTask) {
1804
- destroyTask().catch(() => {});
1805
- }
1806
- function statusError(s) {
1807
- return s.state === "error" ? s.error : new InkError("InvalidPdf", "The document has not finished loading");
1808
- }
1809
- function nonEmptyString(value) {
1810
- return typeof value === "string" && value !== "" ? value : void 0;
1811
- }
1812
- function errorMessage(cause) {
1813
- if (cause instanceof Error) return cause.message;
1814
- return String(cause);
1815
- }
1816
- //#endregion
1817
- //#region src/model/annotations/suggestions.ts
1818
- /**
1819
- * Keeps a suggested redaction as the user's own: `origin` becomes `'user'` and the colour
1820
- * goes from the confidence colour to the store's default redaction colour, so an accepted
1821
- * mark looks like any other. The suggestion details stay on it. Returns the updated
1822
- * annotation, or undefined (changing nothing) when `id` is not a suggested redaction or is
1823
- * locked.
1824
- */
1825
- function acceptSuggestion(store, id) {
1826
- const a = store._getProposed(id);
1827
- if (a?.type !== "redact" || a.origin !== "suggested" || a.locked) return void 0;
1828
- return store.update(id, {
1829
- origin: "user",
1830
- color: store.defaults.color("redact")
1831
- });
1832
- }
1833
- //#endregion
1834
- //#region src/model/annotations/threads.ts
1835
- /** Direct replies to `id`, in creation order. */
1836
- function repliesTo(list, id) {
1837
- return list.filter((a) => a.inReplyTo === id).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1838
- }
1839
- /**
1840
- * The top of a note's thread: follows `inReplyTo` up through notes, stopping at a missing
1841
- * parent, a parent that is not a note, or a cycle.
1842
- */
1843
- function threadRoot(list, start) {
1844
- const seen = /* @__PURE__ */ new Set([start.id]);
1845
- let current = start;
1846
- while (current.inReplyTo) {
1847
- const parent = list.find((a) => a.id === current.inReplyTo);
1848
- if (parent?.type !== "note" || seen.has(parent.id)) break;
1849
- seen.add(parent.id);
1850
- current = parent;
1851
- }
1852
- return current;
1853
- }
1854
- /** The root, then every reply below it, depth first, each level in creation order. */
1855
- function threadOf(list, root) {
1856
- const out = [root];
1857
- const seen = /* @__PURE__ */ new Set([root.id]);
1858
- const visit = (id) => {
1859
- for (const r of repliesTo(list, id)) {
1860
- if (seen.has(r.id)) continue;
1861
- seen.add(r.id);
1862
- out.push(r);
1863
- visit(r.id);
1864
- }
1865
- };
1866
- visit(root.id);
1867
- return out;
1868
- }
1869
- /** An unlocked root note with no text and no replies: nothing a reader would miss. */
1870
- function isAbandonedNote(list, id) {
1871
- const a = list.find((x) => x.id === id);
1872
- if (a?.type !== "note" || a.locked || a.inReplyTo || a.contents?.trim()) return false;
1873
- return repliesTo(list, id).length === 0;
1874
- }
1875
- //#endregion
1876
- export { rectFromQuad as $, RectSchema as A, mergeLineRects as B, InkSchema as C, PointSchema as D, NoteSchema as E, confidenceBucket as F, approxEqual as G, readonly as H, parsePdfDate as I, pointsFromFlat as J, invertMatrix as K, cssVariableDefaults as L, ShapeSchema as M, SuggestionSchema as N, PolySchema as O, TextMarkupSchema as P, rectFromPoints as Q, normalizeColor as R, FreeTextSchema as S, LineSchema as T, writable as U, derived as V, applyMatrix as W, quadsFromFlat as X, quadFromRect as Y, rectContainsPoint as Z, summarise as _, acceptSuggestion as a, InkError as at, AnnotationDocumentSchema as b, deriveRect as c, createHistory as d, rectsIntersect as et, isProduction as f, pdfColorToHex as g, importedId as h, threadRoot as i, Emitter as it, RedactSchema as j, QuadSchema as k, exportAnnotations as l, importEmbeddedPage as m, repliesTo as n, translateRect as nt, openDocument as o, isInkError as ot, SUPPORTED_SUBTYPES as p, multiplyMatrix as q, threadOf as r, unionRects as rt, createAnnotationStore as s, isAbandonedNote as t, translateQuad as tt, importAnnotations as u, ANNOTATION_FORMAT as v, LineEndingSchema as w, AnnotationSchema as x, ANNOTATION_VERSION as y, staticDefaults as z };
1877
-
1878
- //# sourceMappingURL=threads-BtvtrAhL.mjs.map