@inkbar/core 0.4.0 → 0.6.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.
- package/README.md +90 -7
- package/dist/dom.d.mts +610 -58
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2387 -929
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +17 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BHYKTwYE.d.mts → signatures-By4B40DP.d.mts} +706 -620
- package/dist/signatures-By4B40DP.d.mts.map +1 -0
- package/dist/{signatures-DF2VmNVa.mjs → signatures-D68fuLlt.mjs} +1333 -325
- package/dist/signatures-D68fuLlt.mjs.map +1 -0
- package/package.json +4 -7
- package/dist/signatures-BHYKTwYE.d.mts.map +0 -1
- package/dist/signatures-DF2VmNVa.mjs.map +0 -1
- package/schema.json +0 -1356
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Circle, Color, FreeText, FreeTextStyle, Fringe, GLOSS_FORMAT, GLOSS_FORMAT as GLOSS_FORMAT$1, GLOSS_MEDIA_TYPE, GLOSS_SCHEMA_ID, GLOSS_VERSION, GLOSS_VERSION as GLOSS_VERSION$1, Highlight, IMAGE_DATA_URL, INTENTS, Ink, LINE_ENDINGS, Line, LineEnding, NOTE_ICONS, Note, NoteIcon, PDF_NAME, PDF_NAME as PDF_NAME$1, Point, Polygon, Polyline, Rect, Redact, Square, Stamp, commonFields } from "gloss-format";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { v7 } from "uuid";
|
|
3
4
|
//#region src/model/errors.ts
|
|
@@ -95,6 +96,15 @@ function quadFromRect(r) {
|
|
|
95
96
|
}
|
|
96
97
|
};
|
|
97
98
|
}
|
|
99
|
+
/** One quad covering `r`, in the corner order of `quadFromRect`. */
|
|
100
|
+
function quadsFromRect(r) {
|
|
101
|
+
return [quadFromRect(r)];
|
|
102
|
+
}
|
|
103
|
+
/** The bounding box of every corner of `quads`. */
|
|
104
|
+
function rectFromQuads(quads) {
|
|
105
|
+
if (quads.length === 0) throw new RangeError("rectFromQuads needs at least one quad");
|
|
106
|
+
return unionRects(quads.map(rectFromQuad));
|
|
107
|
+
}
|
|
98
108
|
function unionRects(rects) {
|
|
99
109
|
if (rects.length === 0) throw new RangeError("unionRects needs at least one rect");
|
|
100
110
|
let x1 = Infinity;
|
|
@@ -250,6 +260,102 @@ function readonly(w) {
|
|
|
250
260
|
};
|
|
251
261
|
}
|
|
252
262
|
//#endregion
|
|
263
|
+
//#region src/model/records.ts
|
|
264
|
+
/**
|
|
265
|
+
* A keyed list of plain records. Records are stored as frozen copies, so a change's `before` and `after` can be
|
|
266
|
+
* kept as history entries without copying.
|
|
267
|
+
*/
|
|
268
|
+
function createRecordStore(key, opts = {}) {
|
|
269
|
+
const list = writable((opts.initial ?? []).map(frozen));
|
|
270
|
+
const emitter = new Emitter();
|
|
271
|
+
let destroyed = false;
|
|
272
|
+
function commit(next, change) {
|
|
273
|
+
if (destroyed) return;
|
|
274
|
+
list.set(next);
|
|
275
|
+
emitter.emit("change", change);
|
|
276
|
+
opts.onChange?.(next, change);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
key,
|
|
280
|
+
list: readonly(list),
|
|
281
|
+
get: (id) => list.get().find((r) => r.id === id),
|
|
282
|
+
add(record) {
|
|
283
|
+
if (list.get().some((r) => r.id === record.id)) throw new InkError("DuplicateId", `A record with id ${record.id} already exists`, {
|
|
284
|
+
key,
|
|
285
|
+
id: record.id
|
|
286
|
+
});
|
|
287
|
+
const stored = frozen(record);
|
|
288
|
+
commit([...list.get(), stored], {
|
|
289
|
+
kind: "add",
|
|
290
|
+
record: stored
|
|
291
|
+
});
|
|
292
|
+
return stored;
|
|
293
|
+
},
|
|
294
|
+
update(id, patch) {
|
|
295
|
+
const before = list.get().find((r) => r.id === id);
|
|
296
|
+
if (destroyed || !before) return void 0;
|
|
297
|
+
const after = Object.freeze({
|
|
298
|
+
...before,
|
|
299
|
+
...patch,
|
|
300
|
+
id: before.id
|
|
301
|
+
});
|
|
302
|
+
commit(list.get().map((r) => r.id === id ? after : r), {
|
|
303
|
+
kind: "update",
|
|
304
|
+
id,
|
|
305
|
+
before,
|
|
306
|
+
after
|
|
307
|
+
});
|
|
308
|
+
return after;
|
|
309
|
+
},
|
|
310
|
+
remove(id) {
|
|
311
|
+
const record = list.get().find((r) => r.id === id);
|
|
312
|
+
if (destroyed || !record) return void 0;
|
|
313
|
+
commit(list.get().filter((r) => r.id !== id), {
|
|
314
|
+
kind: "remove",
|
|
315
|
+
record
|
|
316
|
+
});
|
|
317
|
+
return record;
|
|
318
|
+
},
|
|
319
|
+
replaceAll(incoming, reason) {
|
|
320
|
+
const prev = new Map(list.get().map((r) => [r.id, r]));
|
|
321
|
+
const added = [];
|
|
322
|
+
const updated = [];
|
|
323
|
+
const next = incoming.map((r) => {
|
|
324
|
+
const old = prev.get(r.id);
|
|
325
|
+
if (!old) {
|
|
326
|
+
added.push(r.id);
|
|
327
|
+
return frozen(r);
|
|
328
|
+
}
|
|
329
|
+
if (shallowEqual(old, r)) return old;
|
|
330
|
+
updated.push(r.id);
|
|
331
|
+
return frozen(r);
|
|
332
|
+
});
|
|
333
|
+
const kept = new Set(next.map((r) => r.id));
|
|
334
|
+
commit(next, {
|
|
335
|
+
kind: "replace",
|
|
336
|
+
reason,
|
|
337
|
+
added,
|
|
338
|
+
updated,
|
|
339
|
+
removed: [...prev.keys()].filter((id) => !kept.has(id))
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
on: (event, fn) => emitter.on(event, fn),
|
|
343
|
+
destroy() {
|
|
344
|
+
destroyed = true;
|
|
345
|
+
emitter.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Freezes a copy, so the caller's object stays theirs; an already frozen record is kept. */
|
|
350
|
+
function frozen(record) {
|
|
351
|
+
return Object.isFrozen(record) ? record : Object.freeze({ ...record });
|
|
352
|
+
}
|
|
353
|
+
function shallowEqual(a, b) {
|
|
354
|
+
const ka = Object.keys(a);
|
|
355
|
+
const kb = Object.keys(b);
|
|
356
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && a[k] === b[k]);
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
253
359
|
//#region src/model/selection.ts
|
|
254
360
|
const union = (a, b) => {
|
|
255
361
|
const x = Math.min(a.x, b.x);
|
|
@@ -422,20 +528,10 @@ const COLORS = {
|
|
|
422
528
|
mask: "#000000",
|
|
423
529
|
redact: "#C0392B"
|
|
424
530
|
};
|
|
425
|
-
/**
|
|
426
|
-
* Suggested-redaction colours, strengthening with confidence: amber for low, orange for
|
|
427
|
-
* medium, the redaction red for high.
|
|
428
|
-
*/
|
|
429
|
-
const SUGGESTION_COLORS = {
|
|
430
|
-
low: "#D4A017",
|
|
431
|
-
medium: "#E67E22",
|
|
432
|
-
high: "#C0392B"
|
|
433
|
-
};
|
|
434
531
|
const staticDefaults = {
|
|
435
532
|
color: (type) => COLORS[type],
|
|
436
533
|
fill: () => "#000000",
|
|
437
|
-
opacity: (type) => type === "highlight" ? .4 : 1
|
|
438
|
-
suggestion: (bucket) => SUGGESTION_COLORS[bucket]
|
|
534
|
+
opacity: (type) => type === "highlight" ? .4 : 1
|
|
439
535
|
};
|
|
440
536
|
const VARIABLES = {
|
|
441
537
|
highlight: "--ink-highlight",
|
|
@@ -465,8 +561,7 @@ function cssVariableDefaults(el) {
|
|
|
465
561
|
return {
|
|
466
562
|
color: (type) => read(VARIABLES[type]) ?? staticDefaults.color(type),
|
|
467
563
|
fill: () => read("--ink-redact-fill") ?? staticDefaults.fill(),
|
|
468
|
-
opacity: (type) => staticDefaults.opacity(type)
|
|
469
|
-
suggestion: (bucket) => read(`--ink-suggestion-${bucket}`) ?? staticDefaults.suggestion(bucket)
|
|
564
|
+
opacity: (type) => staticDefaults.opacity(type)
|
|
470
565
|
};
|
|
471
566
|
}
|
|
472
567
|
/** Accepts `#rgb`, `#rrggbb`, `rgb(r, g, b)` and `rgb(r g b)`; returns `#RRGGBB` or undefined. */
|
|
@@ -507,51 +602,12 @@ function parsePdfDate(value, fallback) {
|
|
|
507
602
|
//#endregion
|
|
508
603
|
//#region src/model/annotations/pdf-names.ts
|
|
509
604
|
/**
|
|
510
|
-
* A PDF name: a non-empty run of regular characters, so no whitespace and none of the
|
|
511
|
-
* delimiters `( ) < > [ ] { } / %`.
|
|
512
|
-
*/
|
|
513
|
-
const PDF_NAME = /^[^\s()<>[\]{}/%]+$/;
|
|
514
|
-
/**
|
|
515
605
|
* A set of PDF names Inkbar knows that stays open: any other valid PDF name loads, is kept and
|
|
516
606
|
* is written back unchanged. The output type keeps autocomplete for the known values.
|
|
517
607
|
*/
|
|
518
608
|
function pdfNameSet(known) {
|
|
519
609
|
return z.union([z.enum(known), z.string().regex(PDF_NAME, "expected a PDF name")]).describe(`One of ${known.join(", ")}. Any other PDF name is allowed and kept unchanged.`);
|
|
520
610
|
}
|
|
521
|
-
/** Line ending styles (PDF `/LE`). */
|
|
522
|
-
const LINE_ENDINGS = [
|
|
523
|
-
"None",
|
|
524
|
-
"Square",
|
|
525
|
-
"Circle",
|
|
526
|
-
"Diamond",
|
|
527
|
-
"OpenArrow",
|
|
528
|
-
"ClosedArrow",
|
|
529
|
-
"Butt",
|
|
530
|
-
"ROpenArrow",
|
|
531
|
-
"RClosedArrow",
|
|
532
|
-
"Slash"
|
|
533
|
-
];
|
|
534
|
-
/** Note icons (PDF `/Name` on a text annotation). */
|
|
535
|
-
const NOTE_ICONS = [
|
|
536
|
-
"Comment",
|
|
537
|
-
"Note",
|
|
538
|
-
"Help",
|
|
539
|
-
"Insert",
|
|
540
|
-
"Key",
|
|
541
|
-
"NewParagraph",
|
|
542
|
-
"Paragraph"
|
|
543
|
-
];
|
|
544
|
-
/** Annotation intents (PDF `/IT`). */
|
|
545
|
-
const INTENTS = [
|
|
546
|
-
"FreeText",
|
|
547
|
-
"FreeTextCallout",
|
|
548
|
-
"FreeTextTypeWriter",
|
|
549
|
-
"LineArrow",
|
|
550
|
-
"LineDimension",
|
|
551
|
-
"PolygonCloud",
|
|
552
|
-
"PolygonDimension",
|
|
553
|
-
"PolyLineDimension"
|
|
554
|
-
];
|
|
555
611
|
/** True when `v` is one of the values in `list`. */
|
|
556
612
|
function isKnown(list, v) {
|
|
557
613
|
return list.includes(v);
|
|
@@ -562,12 +618,7 @@ function isKnown(list, v) {
|
|
|
562
618
|
* The insets from a free text's `rect` to its text box, in points (PDF `/RD`, XFDF `fringe`,
|
|
563
619
|
* both in this order).
|
|
564
620
|
*/
|
|
565
|
-
const FringeSchema = z.object(
|
|
566
|
-
left: z.number().nonnegative(),
|
|
567
|
-
top: z.number().nonnegative(),
|
|
568
|
-
right: z.number().nonnegative(),
|
|
569
|
-
bottom: z.number().nonnegative()
|
|
570
|
-
});
|
|
621
|
+
const FringeSchema = z.object(Fringe.shape);
|
|
571
622
|
/** An ending's length, and its full width, as a multiple of the stroke width. */
|
|
572
623
|
const ENDING_SCALE = 3;
|
|
573
624
|
/**
|
|
@@ -640,73 +691,48 @@ function withCalloutBounds(box, callout, width, head) {
|
|
|
640
691
|
}
|
|
641
692
|
//#endregion
|
|
642
693
|
//#region src/model/annotations/types.ts
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
const Hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, "expected #RRGGBB");
|
|
646
|
-
const PointSchema = z.object({
|
|
647
|
-
x: z.number(),
|
|
648
|
-
y: z.number()
|
|
649
|
-
});
|
|
650
|
-
const RectSchema = z.object({
|
|
651
|
-
x: z.number(),
|
|
652
|
-
y: z.number(),
|
|
653
|
-
width: z.number().nonnegative(),
|
|
654
|
-
height: z.number().nonnegative()
|
|
655
|
-
});
|
|
694
|
+
const PointSchema = z.object(Point.shape);
|
|
695
|
+
const RectSchema = z.object(Rect.shape);
|
|
656
696
|
const QuadSchema = z.object({
|
|
657
697
|
p1: PointSchema,
|
|
658
698
|
p2: PointSchema,
|
|
659
699
|
p3: PointSchema,
|
|
660
700
|
p4: PointSchema
|
|
661
701
|
});
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
id: z.string().min(1),
|
|
665
|
-
/** 0-based page index. */
|
|
666
|
-
page: z.number().int().nonnegative(),
|
|
702
|
+
/** What Inkbar adds to every Gloss type: the fields it always fills in, and its viewer state. */
|
|
703
|
+
const Base = {
|
|
667
704
|
/** Bounding box in PDF user space. */
|
|
668
705
|
rect: RectSchema,
|
|
669
706
|
/** Resolved #RRGGBB. Written into the PDF later, so it is always concrete. */
|
|
670
|
-
color:
|
|
671
|
-
opacity:
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
/** Id of the parent note; replies form threads. */
|
|
675
|
-
inReplyTo: z.string().optional(),
|
|
676
|
-
/** ISO 8601 date-time. */
|
|
677
|
-
createdAt: z.iso.datetime({ offset: true }),
|
|
678
|
-
/** ISO 8601 date-time. */
|
|
679
|
-
modifiedAt: z.iso.datetime({ offset: true }),
|
|
707
|
+
color: Color,
|
|
708
|
+
opacity: commonFields.opacity.removeDefault(),
|
|
709
|
+
createdAt: commonFields.createdAt.unwrap(),
|
|
710
|
+
modifiedAt: commonFields.modifiedAt.unwrap(),
|
|
680
711
|
/** Created in this session, or imported from the file. Viewer state, never exported. */
|
|
681
712
|
source: z.enum(["session", "pdf"]),
|
|
682
713
|
/** Embedded annotations are locked until the engine can rewrite the file. Viewer state, never exported. */
|
|
683
|
-
locked: z.boolean().optional()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
custom: z.record(z.string(), z.unknown()).optional()
|
|
690
|
-
});
|
|
691
|
-
const TextMarkupSchema = Base.extend({
|
|
714
|
+
locked: z.boolean().optional()
|
|
715
|
+
};
|
|
716
|
+
const Quads = z.array(QuadSchema).min(1);
|
|
717
|
+
const TextMarkupSchema = z.object({
|
|
718
|
+
...Highlight.shape,
|
|
719
|
+
...Base,
|
|
692
720
|
type: z.enum([
|
|
693
721
|
"highlight",
|
|
694
722
|
"underline",
|
|
695
723
|
"strikeout",
|
|
696
724
|
"squiggly"
|
|
697
725
|
]),
|
|
698
|
-
quads:
|
|
726
|
+
quads: Quads
|
|
699
727
|
});
|
|
700
728
|
/** A note's icon (XFDF `icon`): a PDF name, so a name Inkbar does not know is kept. */
|
|
701
|
-
const NoteIconSchema =
|
|
702
|
-
const NoteSchema =
|
|
703
|
-
|
|
704
|
-
|
|
729
|
+
const NoteIconSchema = NoteIcon;
|
|
730
|
+
const NoteSchema = z.object({
|
|
731
|
+
...Note.shape,
|
|
732
|
+
...Base
|
|
705
733
|
});
|
|
706
734
|
/** A line ending (XFDF `head`/`tail`): a PDF name, so a name Inkbar does not know is kept. */
|
|
707
|
-
const LineEndingSchema =
|
|
708
|
-
/** Dash pattern in points, on then off, repeating. Absent means solid. */
|
|
709
|
-
const DashesSchema = z.array(z.number().nonnegative()).min(1).optional();
|
|
735
|
+
const LineEndingSchema = LineEnding;
|
|
710
736
|
/**
|
|
711
737
|
* A key a type refuses outright rather than dropping like other unknown keys: one an older
|
|
712
738
|
* shape used (so its value is not silently lost), or one that means nothing on the type (so a
|
|
@@ -718,20 +744,9 @@ const Refused = z.never().optional();
|
|
|
718
744
|
* `defaultstyle` string. The older `align` is refused; other unknown keys are dropped.
|
|
719
745
|
*/
|
|
720
746
|
const FreeTextStyleSchema = z.object({
|
|
721
|
-
|
|
722
|
-
/** Points. */
|
|
723
|
-
fontSize: z.number().positive().default(12),
|
|
747
|
+
...FreeTextStyle.shape,
|
|
724
748
|
/** The text colour. */
|
|
725
|
-
color:
|
|
726
|
-
textAlign: z.enum([
|
|
727
|
-
"left",
|
|
728
|
-
"center",
|
|
729
|
-
"right",
|
|
730
|
-
"justify"
|
|
731
|
-
]).default("left"),
|
|
732
|
-
fontWeight: z.literal("bold").optional(),
|
|
733
|
-
fontStyle: z.literal("italic").optional(),
|
|
734
|
-
textDecoration: z.literal("underline").optional(),
|
|
749
|
+
color: Color,
|
|
735
750
|
align: Refused
|
|
736
751
|
});
|
|
737
752
|
/**
|
|
@@ -739,88 +754,56 @@ const FreeTextStyleSchema = z.object({
|
|
|
739
754
|
* `defaultStyle`. A box in the older shape (top-level `fontFamily`, `fontSize`, `align`) is
|
|
740
755
|
* refused rather than silently losing its style; other unknown keys are dropped.
|
|
741
756
|
*/
|
|
742
|
-
const FreeTextSchema =
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
contents: z.string(),
|
|
757
|
+
const FreeTextSchema = z.object({
|
|
758
|
+
...FreeText.shape,
|
|
759
|
+
...Base,
|
|
746
760
|
/** The box's fill (XFDF `color`). Absent means no fill. */
|
|
747
|
-
color:
|
|
748
|
-
/** The
|
|
749
|
-
borderColor:
|
|
750
|
-
/** The border's width in points; 0 draws no border. */
|
|
751
|
-
width: z.number().nonnegative().default(0),
|
|
752
|
-
dashes: DashesSchema,
|
|
753
|
-
/** Points. An extension, not XFDF. */
|
|
754
|
-
cornerRadius: z.number().nonnegative().optional(),
|
|
761
|
+
color: FreeText.shape.color,
|
|
762
|
+
/** The box's border colour. Absent means the text colour. */
|
|
763
|
+
borderColor: Color.optional(),
|
|
755
764
|
defaultStyle: FreeTextStyleSchema,
|
|
756
|
-
/**
|
|
757
|
-
* The insets from `rect` to the text box, in points (XFDF `fringe`, PDF `/RD`). Absent means
|
|
758
|
-
* the box is `rect`.
|
|
759
|
-
*/
|
|
760
765
|
fringe: FringeSchema.optional(),
|
|
761
|
-
/** A callout's leader line: the tip, an optional bend, and where it meets the box (PDF `/CL`). */
|
|
762
766
|
callout: z.array(PointSchema).min(2).max(3).optional(),
|
|
763
|
-
/** The line ending at a callout's tip (XFDF `head`, PDF `/LE`). Absent means `None`. */
|
|
764
|
-
head: LineEndingSchema.optional(),
|
|
765
767
|
fontFamily: Refused,
|
|
766
768
|
fontSize: Refused,
|
|
767
769
|
align: Refused
|
|
768
770
|
});
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
interiorColor: Hex.optional()
|
|
773
|
-
};
|
|
774
|
-
const SquareSchema = Base.extend({
|
|
775
|
-
type: z.literal("square"),
|
|
776
|
-
...ShapeFields,
|
|
777
|
-
/** The corner radius in points. An extension, not XFDF. */
|
|
778
|
-
cornerRadius: z.number().nonnegative().optional()
|
|
771
|
+
const SquareSchema = z.object({
|
|
772
|
+
...Square.shape,
|
|
773
|
+
...Base
|
|
779
774
|
});
|
|
780
|
-
const CircleSchema =
|
|
781
|
-
|
|
782
|
-
...
|
|
775
|
+
const CircleSchema = z.object({
|
|
776
|
+
...Circle.shape,
|
|
777
|
+
...Base,
|
|
783
778
|
cornerRadius: Refused
|
|
784
779
|
});
|
|
785
780
|
const ShapeSchema = z.discriminatedUnion("type", [SquareSchema, CircleSchema]);
|
|
786
|
-
const LineSchema =
|
|
787
|
-
|
|
781
|
+
const LineSchema = z.object({
|
|
782
|
+
...Line.shape,
|
|
783
|
+
...Base,
|
|
788
784
|
start: PointSchema,
|
|
789
|
-
end: PointSchema
|
|
790
|
-
width: z.number().nonnegative().default(1),
|
|
791
|
-
dashes: DashesSchema,
|
|
792
|
-
head: LineEndingSchema.optional(),
|
|
793
|
-
tail: LineEndingSchema.optional()
|
|
785
|
+
end: PointSchema
|
|
794
786
|
});
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
};
|
|
801
|
-
const PolylineSchema = Base.extend({
|
|
802
|
-
type: z.literal("polyline"),
|
|
803
|
-
...PolyFields,
|
|
804
|
-
/** The ending at the first vertex. */
|
|
805
|
-
head: LineEndingSchema.optional(),
|
|
806
|
-
/** The ending at the last vertex. */
|
|
807
|
-
tail: LineEndingSchema.optional()
|
|
787
|
+
const Vertices = z.array(PointSchema).min(2);
|
|
788
|
+
const PolylineSchema = z.object({
|
|
789
|
+
...Polyline.shape,
|
|
790
|
+
...Base,
|
|
791
|
+
vertices: Vertices
|
|
808
792
|
});
|
|
809
|
-
const PolygonSchema =
|
|
810
|
-
|
|
811
|
-
...
|
|
793
|
+
const PolygonSchema = z.object({
|
|
794
|
+
...Polygon.shape,
|
|
795
|
+
...Base,
|
|
796
|
+
vertices: Vertices,
|
|
812
797
|
head: Refused,
|
|
813
798
|
tail: Refused
|
|
814
799
|
});
|
|
815
800
|
const PolySchema = z.discriminatedUnion("type", [PolylineSchema, PolygonSchema]);
|
|
816
|
-
const InkShape =
|
|
817
|
-
|
|
801
|
+
const InkShape = z.object({
|
|
802
|
+
...Ink.shape,
|
|
803
|
+
...Base,
|
|
818
804
|
inklist: z.array(z.array(PointSchema).min(1)).min(1),
|
|
819
|
-
/** Pen pressure
|
|
820
|
-
pressure: z.array(z.array(z.number().min(0).max(1))).optional()
|
|
821
|
-
width: z.number().nonnegative().default(2),
|
|
822
|
-
/** Kept so imported dashed ink round-trips. Inkbar draws ink solid. */
|
|
823
|
-
dashes: DashesSchema
|
|
805
|
+
/** Pen pressure from 0 to 1, one value per point of each stroke. Absent means even pressure. */
|
|
806
|
+
pressure: z.array(z.array(z.number().min(0).max(1))).optional()
|
|
824
807
|
});
|
|
825
808
|
/** Pressure must have one value per point of each stroke. */
|
|
826
809
|
function checkPressure(a, ctx) {
|
|
@@ -832,70 +815,48 @@ function checkPressure(a, ctx) {
|
|
|
832
815
|
});
|
|
833
816
|
}
|
|
834
817
|
const InkSchema = InkShape.superRefine(checkPressure);
|
|
835
|
-
/** A PNG or JPEG image as a base64 `data:` URL, as XFDF carries a stamp's `<imagedata>`. */
|
|
836
|
-
const IMAGE_DATA_URL = /^data:image\/(png|jpeg);base64,/;
|
|
837
818
|
/**
|
|
838
819
|
* An image on the page (XFDF `stamp`), drawn to fill `rect`. Image and typed signatures are
|
|
839
820
|
* stamps with `subject: 'Signature'`.
|
|
840
821
|
*/
|
|
841
|
-
const StampSchema =
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
imagedata: z.string().regex(IMAGE_DATA_URL, "expected a PNG or JPEG data URL"),
|
|
845
|
-
/**
|
|
846
|
-
* Quarter turns, in degrees anticlockwise, the image is turned within `rect` (XFDF
|
|
847
|
-
* `rotation`). Absent means upright in user space.
|
|
848
|
-
*/
|
|
849
|
-
rotation: z.union([
|
|
850
|
-
z.literal(0),
|
|
851
|
-
z.literal(90),
|
|
852
|
-
z.literal(180),
|
|
853
|
-
z.literal(270)
|
|
854
|
-
]).optional()
|
|
822
|
+
const StampSchema = z.object({
|
|
823
|
+
...Stamp.shape,
|
|
824
|
+
...Base
|
|
855
825
|
});
|
|
856
826
|
/**
|
|
857
|
-
* A loupe (
|
|
858
|
-
* under its own centre, magnified. `color` and
|
|
827
|
+
* A loupe (Inkbar's own type, a circle with `custom.inkbar.type: 'loupe'` in a file): a circle
|
|
828
|
+
* inscribed in `rect` that shows the page content under its own centre, magnified. `color` and
|
|
829
|
+
* `width` draw its rim.
|
|
859
830
|
*/
|
|
860
|
-
const LoupeSchema =
|
|
831
|
+
const LoupeSchema = z.object({
|
|
861
832
|
type: z.literal("loupe"),
|
|
833
|
+
...commonFields,
|
|
862
834
|
/** How many times larger the content under the centre is drawn. Above 1. */
|
|
863
835
|
magnification: z.number().gt(1).default(2),
|
|
864
836
|
/** The rim's width in points. */
|
|
865
|
-
width: z.number().nonnegative().default(2)
|
|
837
|
+
width: z.number().nonnegative().default(2),
|
|
838
|
+
...Base
|
|
866
839
|
});
|
|
867
840
|
/**
|
|
868
|
-
* A mask (
|
|
869
|
-
* `rect`, an ellipse inscribed in it or the
|
|
870
|
-
*
|
|
871
|
-
* the first.
|
|
841
|
+
* A mask (Inkbar's own type, a circle or square with `custom.inkbar.type: 'mask'` in a file):
|
|
842
|
+
* dims everything on the page outside `shape` drawn in `rect`, an ellipse inscribed in it or the
|
|
843
|
+
* rectangle itself. `color` is the dimming colour and `dim` how strongly it covers, from 0 to 1.
|
|
844
|
+
* One per page: adding a second replaces the first.
|
|
872
845
|
*/
|
|
873
|
-
const MaskSchema =
|
|
846
|
+
const MaskSchema = z.object({
|
|
874
847
|
type: z.literal("mask"),
|
|
848
|
+
...commonFields,
|
|
875
849
|
shape: z.enum(["rect", "ellipse"]),
|
|
876
|
-
|
|
877
|
-
|
|
850
|
+
dim: z.number().min(0).max(1).default(.5),
|
|
851
|
+
...Base
|
|
878
852
|
});
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
reason: z.string().optional(),
|
|
883
|
-
explanation: z.string().optional(),
|
|
884
|
-
recommendation: z.enum([
|
|
885
|
-
"redact",
|
|
886
|
-
"review",
|
|
887
|
-
"keep"
|
|
888
|
-
]).optional()
|
|
889
|
-
});
|
|
890
|
-
const RedactSchema = Base.extend({
|
|
891
|
-
type: z.literal("redact"),
|
|
853
|
+
const RedactSchema = z.object({
|
|
854
|
+
...Redact.shape,
|
|
855
|
+
...Base,
|
|
892
856
|
/** Present when the mark came from text; absent for an area mark. */
|
|
893
|
-
quads:
|
|
894
|
-
origin: z.enum(["user", "suggested"]).default("user"),
|
|
895
|
-
suggestion: SuggestionSchema.optional(),
|
|
896
|
-
overlayText: z.string().optional(),
|
|
857
|
+
quads: Quads.optional(),
|
|
897
858
|
/** Colour the redaction will be filled with when applied. */
|
|
898
|
-
interiorColor:
|
|
859
|
+
interiorColor: Color
|
|
899
860
|
});
|
|
900
861
|
const AnnotationSchema = z.discriminatedUnion("type", [
|
|
901
862
|
TextMarkupSchema,
|
|
@@ -934,17 +895,12 @@ const ExportedAnnotationSchema = z.discriminatedUnion("type", [
|
|
|
934
895
|
RedactSchema.omit(VIEWER_STATE)
|
|
935
896
|
]);
|
|
936
897
|
const AnnotationDocumentSchema = z.object({
|
|
937
|
-
format: z.literal(
|
|
938
|
-
version: z.literal(
|
|
898
|
+
format: z.literal(GLOSS_FORMAT),
|
|
899
|
+
version: z.literal(GLOSS_VERSION),
|
|
939
900
|
documentKey: z.string().optional(),
|
|
940
901
|
/** Validated item by item on import so unknown types can be preserved. */
|
|
941
902
|
annotations: z.array(z.unknown())
|
|
942
903
|
});
|
|
943
|
-
function confidenceBucket(confidence) {
|
|
944
|
-
if (confidence < .5) return "low";
|
|
945
|
-
if (confidence < .8) return "medium";
|
|
946
|
-
return "high";
|
|
947
|
-
}
|
|
948
904
|
//#endregion
|
|
949
905
|
//#region src/model/annotations/embedded.ts
|
|
950
906
|
const SUPPORTED_SUBTYPES = /* @__PURE__ */ new Set([
|
|
@@ -982,7 +938,7 @@ function rectFromPdf(r) {
|
|
|
982
938
|
}
|
|
983
939
|
/** A PDF name exactly as PDF.js gives it (names are case-sensitive), or undefined if invalid. */
|
|
984
940
|
function pdfName(raw) {
|
|
985
|
-
return raw !== void 0 && PDF_NAME.test(raw) ? raw : void 0;
|
|
941
|
+
return raw !== void 0 && PDF_NAME$1.test(raw) ? raw : void 0;
|
|
986
942
|
}
|
|
987
943
|
/** `head` and `tail` from a Line's or PolyLine's `/LE`, left out when both are `None`. */
|
|
988
944
|
function lineEndingsOf(raw) {
|
|
@@ -1000,18 +956,43 @@ function lineEndingsOf(raw) {
|
|
|
1000
956
|
function importedId(page, pdfjsId) {
|
|
1001
957
|
return `pdf:${page}:${pdfjsId}`;
|
|
1002
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Imports one page's PDF.js annotation data. `targets` is @internal: the annotations whose
|
|
961
|
+
* missing fields the document reads from the file after `ready`.
|
|
962
|
+
*/
|
|
1003
963
|
function importEmbeddedPage(page, raw, importedAt) {
|
|
1004
964
|
const counts = {};
|
|
1005
965
|
const annotations = [];
|
|
966
|
+
const targets = [];
|
|
1006
967
|
for (const data of raw) {
|
|
1007
968
|
counts[data.subtype] = (counts[data.subtype] ?? 0) + 1;
|
|
1008
969
|
if (!SUPPORTED_SUBTYPES.has(data.subtype) || data.hidden || data.noView) continue;
|
|
1009
970
|
const converted = convert(page, data, importedAt);
|
|
1010
|
-
if (converted)
|
|
971
|
+
if (!converted) continue;
|
|
972
|
+
annotations.push(converted);
|
|
973
|
+
const target = targetOf(converted.id, data);
|
|
974
|
+
if (target) targets.push(target);
|
|
1011
975
|
}
|
|
1012
976
|
return {
|
|
1013
977
|
annotations,
|
|
1014
|
-
counts
|
|
978
|
+
counts,
|
|
979
|
+
targets
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function targetOf(id, d) {
|
|
983
|
+
const rect = Array.from(d.rect);
|
|
984
|
+
if (d.subtype === "Text") return d.name === "NoIcon" ? {
|
|
985
|
+
id,
|
|
986
|
+
subtype: "Text",
|
|
987
|
+
rect
|
|
988
|
+
} : void 0;
|
|
989
|
+
if (d.subtype !== "FreeText") return void 0;
|
|
990
|
+
const daFont = d.defaultAppearanceData?.fontName;
|
|
991
|
+
return {
|
|
992
|
+
id,
|
|
993
|
+
subtype: "FreeText",
|
|
994
|
+
rect,
|
|
995
|
+
...daFont ? { daFont } : {}
|
|
1015
996
|
};
|
|
1016
997
|
}
|
|
1017
998
|
function summarise(perPage) {
|
|
@@ -1152,7 +1133,6 @@ function convert(page, d, importedAt) {
|
|
|
1152
1133
|
candidate = {
|
|
1153
1134
|
...base("redact"),
|
|
1154
1135
|
type: "redact",
|
|
1155
|
-
origin: "user",
|
|
1156
1136
|
interiorColor: pdfColorToHex(d.interiorColor, staticDefaults.fill()),
|
|
1157
1137
|
...quads.length > 0 ? { quads } : {}
|
|
1158
1138
|
};
|
|
@@ -1163,6 +1143,749 @@ function convert(page, d, importedAt) {
|
|
|
1163
1143
|
return parsed.success ? parsed.data : void 0;
|
|
1164
1144
|
}
|
|
1165
1145
|
//#endregion
|
|
1146
|
+
//#region src/pdf/lexer.ts
|
|
1147
|
+
/** Thrown when the bytes are not valid PDF object syntax. */
|
|
1148
|
+
var PdfSyntaxError = class extends Error {
|
|
1149
|
+
constructor(message) {
|
|
1150
|
+
super(message);
|
|
1151
|
+
this.name = "PdfSyntaxError";
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
const isWhite$1 = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
1155
|
+
const isDelimiter = (c) => c === 40 || c === 41 || c === 60 || c === 62 || c === 91 || c === 93 || c === 123 || c === 125 || c === 47 || c === 37;
|
|
1156
|
+
const isRegular = (c) => !isWhite$1(c) && !isDelimiter(c);
|
|
1157
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
1158
|
+
const latin1$1 = (bytes, from, to) => {
|
|
1159
|
+
let out = "";
|
|
1160
|
+
for (let i = from; i < to; i++) out += String.fromCharCode(bytes[i]);
|
|
1161
|
+
return out;
|
|
1162
|
+
};
|
|
1163
|
+
function skipSpace(bytes, offset) {
|
|
1164
|
+
let i = offset;
|
|
1165
|
+
while (i < bytes.length) {
|
|
1166
|
+
const c = bytes[i];
|
|
1167
|
+
if (isWhite$1(c)) i++;
|
|
1168
|
+
else if (c === 37) while (i < bytes.length && bytes[i] !== 10 && bytes[i] !== 13) i++;
|
|
1169
|
+
else break;
|
|
1170
|
+
}
|
|
1171
|
+
return i;
|
|
1172
|
+
}
|
|
1173
|
+
function tokenEnd(bytes, offset) {
|
|
1174
|
+
let i = offset;
|
|
1175
|
+
while (i < bytes.length && isRegular(bytes[i])) i++;
|
|
1176
|
+
return i;
|
|
1177
|
+
}
|
|
1178
|
+
const hexValue = (c) => {
|
|
1179
|
+
if (c === void 0) return -1;
|
|
1180
|
+
if (c >= 48 && c <= 57) return c - 48;
|
|
1181
|
+
if (c >= 65 && c <= 70) return c - 65 + 10;
|
|
1182
|
+
if (c >= 97 && c <= 102) return c - 97 + 10;
|
|
1183
|
+
return -1;
|
|
1184
|
+
};
|
|
1185
|
+
function parseName(bytes, offset) {
|
|
1186
|
+
const end = tokenEnd(bytes, offset + 1);
|
|
1187
|
+
let name = "";
|
|
1188
|
+
for (let i = offset + 1; i < end; i++) {
|
|
1189
|
+
const c = bytes[i];
|
|
1190
|
+
const hi = c === 35 ? hexValue(bytes[i + 1]) : -1;
|
|
1191
|
+
const lo = hi >= 0 ? hexValue(bytes[i + 2]) : -1;
|
|
1192
|
+
if (hi >= 0 && lo >= 0 && i + 2 < end) {
|
|
1193
|
+
name += String.fromCharCode(hi * 16 + lo);
|
|
1194
|
+
i += 2;
|
|
1195
|
+
} else name += String.fromCharCode(c);
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
value: {
|
|
1199
|
+
kind: "name",
|
|
1200
|
+
name
|
|
1201
|
+
},
|
|
1202
|
+
end
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function skipLiteralString(bytes, offset) {
|
|
1206
|
+
let depth = 0;
|
|
1207
|
+
for (let i = offset; i < bytes.length; i++) {
|
|
1208
|
+
const c = bytes[i];
|
|
1209
|
+
if (c === 92) i++;
|
|
1210
|
+
else if (c === 40) depth++;
|
|
1211
|
+
else if (c === 41 && --depth === 0) return i + 1;
|
|
1212
|
+
}
|
|
1213
|
+
throw new PdfSyntaxError("Unterminated string");
|
|
1214
|
+
}
|
|
1215
|
+
/** After a non-negative integer ending at `end`, looks for `gen R`. */
|
|
1216
|
+
function tryReference(bytes, num, end) {
|
|
1217
|
+
let i = end;
|
|
1218
|
+
if (i >= bytes.length || !isWhite$1(bytes[i])) return void 0;
|
|
1219
|
+
i = skipSpace(bytes, i);
|
|
1220
|
+
const genStart = i;
|
|
1221
|
+
while (i < bytes.length && isDigit(bytes[i])) i++;
|
|
1222
|
+
if (i === genStart || i >= bytes.length || !isWhite$1(bytes[i])) return void 0;
|
|
1223
|
+
const gen = Number(latin1$1(bytes, genStart, i));
|
|
1224
|
+
i = skipSpace(bytes, i);
|
|
1225
|
+
if (bytes[i] !== 82 || i + 1 < bytes.length && isRegular(bytes[i + 1])) return;
|
|
1226
|
+
return {
|
|
1227
|
+
value: {
|
|
1228
|
+
kind: "ref",
|
|
1229
|
+
num,
|
|
1230
|
+
gen
|
|
1231
|
+
},
|
|
1232
|
+
end: i + 1
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function parseNumberOrKeyword(bytes, offset) {
|
|
1236
|
+
const end = tokenEnd(bytes, offset);
|
|
1237
|
+
const token = latin1$1(bytes, offset, end);
|
|
1238
|
+
if (token === "true") return {
|
|
1239
|
+
value: true,
|
|
1240
|
+
end
|
|
1241
|
+
};
|
|
1242
|
+
if (token === "false") return {
|
|
1243
|
+
value: false,
|
|
1244
|
+
end
|
|
1245
|
+
};
|
|
1246
|
+
if (token === "null") return {
|
|
1247
|
+
value: null,
|
|
1248
|
+
end
|
|
1249
|
+
};
|
|
1250
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(token)) {
|
|
1251
|
+
const n = Number(token);
|
|
1252
|
+
if (/^\d+$/.test(token)) {
|
|
1253
|
+
const ref = tryReference(bytes, n, end);
|
|
1254
|
+
if (ref) return ref;
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
value: n,
|
|
1258
|
+
end
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
throw new PdfSyntaxError(`Unexpected token "${token}" at ${offset}`);
|
|
1262
|
+
}
|
|
1263
|
+
/** Parses one value at `offset`, skipping whitespace and comments first. */
|
|
1264
|
+
function parseValue(bytes, offset) {
|
|
1265
|
+
const i = skipSpace(bytes, offset);
|
|
1266
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unexpected end of data");
|
|
1267
|
+
const c = bytes[i];
|
|
1268
|
+
if (c === 47) return parseName(bytes, i);
|
|
1269
|
+
if (c === 40) return {
|
|
1270
|
+
value: { kind: "string" },
|
|
1271
|
+
end: skipLiteralString(bytes, i)
|
|
1272
|
+
};
|
|
1273
|
+
if (c === 60 && bytes[i + 1] === 60) return parseDict(bytes, i + 2);
|
|
1274
|
+
if (c === 60) {
|
|
1275
|
+
let j = i + 1;
|
|
1276
|
+
while (j < bytes.length && bytes[j] !== 62) j++;
|
|
1277
|
+
if (j >= bytes.length) throw new PdfSyntaxError("Unterminated hex string");
|
|
1278
|
+
return {
|
|
1279
|
+
value: { kind: "string" },
|
|
1280
|
+
end: j + 1
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (c === 91) return parseArray(bytes, i + 1);
|
|
1284
|
+
if (isRegular(c)) return parseNumberOrKeyword(bytes, i);
|
|
1285
|
+
throw new PdfSyntaxError(`Unexpected "${String.fromCharCode(c)}" at ${i}`);
|
|
1286
|
+
}
|
|
1287
|
+
function parseArray(bytes, offset) {
|
|
1288
|
+
const items = [];
|
|
1289
|
+
let i = offset;
|
|
1290
|
+
for (;;) {
|
|
1291
|
+
i = skipSpace(bytes, i);
|
|
1292
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unterminated array");
|
|
1293
|
+
if (bytes[i] === 93) return {
|
|
1294
|
+
value: items,
|
|
1295
|
+
end: i + 1
|
|
1296
|
+
};
|
|
1297
|
+
const item = parseValue(bytes, i);
|
|
1298
|
+
items.push(item.value);
|
|
1299
|
+
i = item.end;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function parseDict(bytes, offset) {
|
|
1303
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1304
|
+
let i = offset;
|
|
1305
|
+
for (;;) {
|
|
1306
|
+
i = skipSpace(bytes, i);
|
|
1307
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unterminated dictionary");
|
|
1308
|
+
if (bytes[i] === 62 && bytes[i + 1] === 62) return {
|
|
1309
|
+
value: {
|
|
1310
|
+
kind: "dict",
|
|
1311
|
+
entries
|
|
1312
|
+
},
|
|
1313
|
+
end: i + 2
|
|
1314
|
+
};
|
|
1315
|
+
if (bytes[i] !== 47) throw new PdfSyntaxError(`Expected a name key at ${i}`);
|
|
1316
|
+
const key = parseName(bytes, i);
|
|
1317
|
+
const val = parseValue(bytes, key.end);
|
|
1318
|
+
entries.set(key.value.name, val.value);
|
|
1319
|
+
i = val.end;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function readInteger(bytes, offset) {
|
|
1323
|
+
const i = skipSpace(bytes, offset);
|
|
1324
|
+
let end = i;
|
|
1325
|
+
while (end < bytes.length && isDigit(bytes[end])) end++;
|
|
1326
|
+
if (end === i) throw new PdfSyntaxError(`Expected an integer at ${i}`);
|
|
1327
|
+
return {
|
|
1328
|
+
n: Number(latin1$1(bytes, i, end)),
|
|
1329
|
+
end
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
/** Parses `num gen obj <value> [stream]` at `offset`. `streamStart` is the first data byte after `stream` and its EOL. */
|
|
1333
|
+
function parseIndirect(bytes, offset) {
|
|
1334
|
+
const num = readInteger(bytes, offset);
|
|
1335
|
+
const gen = readInteger(bytes, num.end);
|
|
1336
|
+
const kw = skipSpace(bytes, gen.end);
|
|
1337
|
+
if (latin1$1(bytes, kw, tokenEnd(bytes, kw)) !== "obj") throw new PdfSyntaxError(`Expected "obj" at ${kw}`);
|
|
1338
|
+
const { value, end } = parseValue(bytes, kw + 3);
|
|
1339
|
+
const result = {
|
|
1340
|
+
num: num.n,
|
|
1341
|
+
gen: gen.n,
|
|
1342
|
+
value
|
|
1343
|
+
};
|
|
1344
|
+
const next = skipSpace(bytes, end);
|
|
1345
|
+
if (latin1$1(bytes, next, next + 6) === "stream" && !isRegular(bytes[next + 6] ?? 32)) {
|
|
1346
|
+
let start = next + 6;
|
|
1347
|
+
if (bytes[start] === 13 && bytes[start + 1] === 10) start += 2;
|
|
1348
|
+
else if (bytes[start] === 10 || bytes[start] === 13) start += 1;
|
|
1349
|
+
result.streamStart = start;
|
|
1350
|
+
}
|
|
1351
|
+
return result;
|
|
1352
|
+
}
|
|
1353
|
+
/** Reads a dictionary entry. */
|
|
1354
|
+
const dictGet = (d, key) => d.entries.get(key);
|
|
1355
|
+
/** True for a name object, optionally one with the given spelling. */
|
|
1356
|
+
const isName = (v, name) => typeof v === "object" && v !== null && v.kind === "name" && (name === void 0 || v.name === name);
|
|
1357
|
+
/** True for a dictionary object. */
|
|
1358
|
+
const isDict = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.kind === "dict";
|
|
1359
|
+
/** True for an indirect reference. */
|
|
1360
|
+
const isRef = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.kind === "ref";
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/pdf/annotation-fields.ts
|
|
1363
|
+
/** `12R` → {num:12,gen:0}, `12R3` → {num:12,gen:3}; anything else undefined. */
|
|
1364
|
+
function refOfPdfjsId(id) {
|
|
1365
|
+
const m = /^(\d+)R(\d+)?$/.exec(id);
|
|
1366
|
+
if (!m) return void 0;
|
|
1367
|
+
return {
|
|
1368
|
+
kind: "ref",
|
|
1369
|
+
num: Number(m[1]),
|
|
1370
|
+
gen: m[2] === void 0 ? 0 : Number(m[2])
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const FAMILIES = [
|
|
1374
|
+
[/^(helvetica|helv)/i, "Helvetica"],
|
|
1375
|
+
[/^(times|tiro)/i, "Times"],
|
|
1376
|
+
[/^cour(ier|$)/i, "Courier"]
|
|
1377
|
+
];
|
|
1378
|
+
/**
|
|
1379
|
+
* BaseFont (or bare resource name) → Inkbar font fields; undefined for Symbol and ZapfDingbats.
|
|
1380
|
+
* A family matches by prefix, so `HelveticaNeue` reads as Helvetica and `CourierNewPS` as
|
|
1381
|
+
* Courier. Any other name keeps its own family, minus a trailing `MT` or `PSMT`.
|
|
1382
|
+
* With `knownOnly`, a name outside Helvetica, Times and Courier gives undefined: a bare
|
|
1383
|
+
* resource id such as `F1` is not a family.
|
|
1384
|
+
*/
|
|
1385
|
+
function fontFromBaseName(name, knownOnly = false) {
|
|
1386
|
+
const bare = name.replace(/^[A-Z]{6}\+/, "");
|
|
1387
|
+
if (/^(symbol|zapfdingbats|zadb)/i.test(bare)) return void 0;
|
|
1388
|
+
const cut = bare.search(/[-,]/);
|
|
1389
|
+
const base = cut < 0 ? bare : bare.slice(0, cut);
|
|
1390
|
+
const style = cut < 0 ? "" : bare.slice(cut + 1);
|
|
1391
|
+
const known = FAMILIES.find(([re]) => re.test(base));
|
|
1392
|
+
if (knownOnly && !known) return void 0;
|
|
1393
|
+
const fontFamily = known ? known[1] : base.replace(/(PS)?MT$/, "");
|
|
1394
|
+
if (!fontFamily) return void 0;
|
|
1395
|
+
const font = { fontFamily };
|
|
1396
|
+
if (/bold|black|heavy/i.test(style)) font.fontWeight = "bold";
|
|
1397
|
+
if (/italic|oblique/i.test(style)) font.fontStyle = "italic";
|
|
1398
|
+
return font;
|
|
1399
|
+
}
|
|
1400
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1401
|
+
async function numbers$1(objs, v, counts) {
|
|
1402
|
+
const arr = await objs.resolve(v);
|
|
1403
|
+
if (!Array.isArray(arr) || !counts.includes(arr.length)) return void 0;
|
|
1404
|
+
const out = await Promise.all(arr.map((n) => objs.resolve(n)));
|
|
1405
|
+
return out.every(finite) ? out : void 0;
|
|
1406
|
+
}
|
|
1407
|
+
async function dictOf(objs, v) {
|
|
1408
|
+
const d = await objs.resolve(v);
|
|
1409
|
+
return isDict(d) ? d : void 0;
|
|
1410
|
+
}
|
|
1411
|
+
/** The font's /BaseFont, looked up in a /DR resource dictionary. */
|
|
1412
|
+
async function baseFontIn(objs, dr, resource) {
|
|
1413
|
+
const res = await dictOf(objs, dr);
|
|
1414
|
+
const fonts = res && await dictOf(objs, dictGet(res, "Font"));
|
|
1415
|
+
const font = fonts && await dictOf(objs, dictGet(fonts, resource));
|
|
1416
|
+
const base = font && await objs.resolve(dictGet(font, "BaseFont"));
|
|
1417
|
+
return isName(base) ? base.name : void 0;
|
|
1418
|
+
}
|
|
1419
|
+
async function readFont(objs, annot, daFont) {
|
|
1420
|
+
const root = await dictOf(objs, dictGet(objs.trailer, "Root"));
|
|
1421
|
+
const acroForm = root && await dictOf(objs, dictGet(root, "AcroForm"));
|
|
1422
|
+
const sources = [dictGet(annot, "DR"), acroForm && dictGet(acroForm, "DR")];
|
|
1423
|
+
for (const dr of sources) {
|
|
1424
|
+
const base = await baseFontIn(objs, dr, daFont);
|
|
1425
|
+
if (base !== void 0) return fontFromBaseName(base);
|
|
1426
|
+
}
|
|
1427
|
+
return fontFromBaseName(daFont, true);
|
|
1428
|
+
}
|
|
1429
|
+
async function readHead(objs, le) {
|
|
1430
|
+
let v = await objs.resolve(le);
|
|
1431
|
+
if (Array.isArray(v)) v = await objs.resolve(v[0]);
|
|
1432
|
+
return isName(v) && PDF_NAME$1.test(v.name) ? v.name : void 0;
|
|
1433
|
+
}
|
|
1434
|
+
/** `rect` is the annotation's [x1 y1 x2 y2], used to reject a fringe that leaves no box. */
|
|
1435
|
+
async function readFreeTextFields(objs, ref, rect, daFont) {
|
|
1436
|
+
const fields = {};
|
|
1437
|
+
try {
|
|
1438
|
+
const annot = await objs.get(ref);
|
|
1439
|
+
if (!isDict(annot)) return fields;
|
|
1440
|
+
const cl = await numbers$1(objs, dictGet(annot, "CL"), [4, 6]);
|
|
1441
|
+
if (cl) {
|
|
1442
|
+
fields.callout = [];
|
|
1443
|
+
for (let i = 0; i < cl.length; i += 2) fields.callout.push({
|
|
1444
|
+
x: cl[i],
|
|
1445
|
+
y: cl[i + 1]
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
const rd = await numbers$1(objs, dictGet(annot, "RD"), [4]);
|
|
1449
|
+
if (rd && rect.length === 4 && rd.every((n) => n >= 0)) {
|
|
1450
|
+
const [left, top, right, bottom] = rd;
|
|
1451
|
+
const width = Math.abs(rect[2] - rect[0]);
|
|
1452
|
+
const height = Math.abs(rect[3] - rect[1]);
|
|
1453
|
+
if (left + right < width && top + bottom < height) fields.fringe = {
|
|
1454
|
+
left,
|
|
1455
|
+
top,
|
|
1456
|
+
right,
|
|
1457
|
+
bottom
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
const head = await readHead(objs, dictGet(annot, "LE"));
|
|
1461
|
+
if (head !== void 0) fields.head = head;
|
|
1462
|
+
if (daFont !== void 0) {
|
|
1463
|
+
const font = await readFont(objs, annot, daFont);
|
|
1464
|
+
if (font) fields.font = font;
|
|
1465
|
+
}
|
|
1466
|
+
} catch {}
|
|
1467
|
+
return fields;
|
|
1468
|
+
}
|
|
1469
|
+
async function readTextIcon(objs, ref) {
|
|
1470
|
+
try {
|
|
1471
|
+
const annot = await objs.get(ref);
|
|
1472
|
+
if (!isDict(annot)) return void 0;
|
|
1473
|
+
const name = await objs.resolve(dictGet(annot, "Name"));
|
|
1474
|
+
return isName(name) && PDF_NAME$1.test(name.name) ? name.name : void 0;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/pdf/stream.ts
|
|
1481
|
+
const ENDSTREAM = Uint8Array.from("endstream", (c) => c.charCodeAt(0));
|
|
1482
|
+
function matchesAt(bytes, at, pattern) {
|
|
1483
|
+
if (at < 0 || at + pattern.length > bytes.length) return false;
|
|
1484
|
+
for (let i = 0; i < pattern.length; i++) if (bytes[at + i] !== pattern[i]) return false;
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
function findEndstream(bytes, from) {
|
|
1488
|
+
for (let i = from; i + ENDSTREAM.length <= bytes.length; i++) if (matchesAt(bytes, i, ENDSTREAM)) return i;
|
|
1489
|
+
return bytes.length;
|
|
1490
|
+
}
|
|
1491
|
+
/** Raw stream bytes: trusts `length` when `endstream` follows it, otherwise scans. */
|
|
1492
|
+
function rawData(bytes, start, length) {
|
|
1493
|
+
if (length !== void 0 && Number.isInteger(length) && length >= 0) {
|
|
1494
|
+
let i = start + length;
|
|
1495
|
+
while (i < bytes.length && (bytes[i] === 10 || bytes[i] === 13 || bytes[i] === 32)) i++;
|
|
1496
|
+
if (matchesAt(bytes, i, ENDSTREAM)) return bytes.subarray(start, start + length);
|
|
1497
|
+
}
|
|
1498
|
+
let end = findEndstream(bytes, start);
|
|
1499
|
+
if (end > start && bytes[end - 1] === 10) end--;
|
|
1500
|
+
if (end > start && bytes[end - 1] === 13) end--;
|
|
1501
|
+
return bytes.subarray(start, end);
|
|
1502
|
+
}
|
|
1503
|
+
/** The single filter parameter dictionary, if any. Anything else that is defined is an error. */
|
|
1504
|
+
function decodeParms(dict) {
|
|
1505
|
+
const parms = dictGet(dict, "DecodeParms") ?? dictGet(dict, "DP");
|
|
1506
|
+
const first = Array.isArray(parms) ? parms[0] : parms;
|
|
1507
|
+
if (first === void 0 || first === null) return void 0;
|
|
1508
|
+
if (!isDict(first)) throw new PdfSyntaxError("Unsupported /DecodeParms");
|
|
1509
|
+
return first;
|
|
1510
|
+
}
|
|
1511
|
+
function numberParam(parms, key, fallback) {
|
|
1512
|
+
const v = parms ? dictGet(parms, key) : void 0;
|
|
1513
|
+
return typeof v === "number" ? v : fallback;
|
|
1514
|
+
}
|
|
1515
|
+
async function inflate(data) {
|
|
1516
|
+
const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate"));
|
|
1517
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
1518
|
+
}
|
|
1519
|
+
function paeth(a, b, c) {
|
|
1520
|
+
const p = a + b - c;
|
|
1521
|
+
const pa = Math.abs(p - a);
|
|
1522
|
+
const pb = Math.abs(p - b);
|
|
1523
|
+
const pc = Math.abs(p - c);
|
|
1524
|
+
if (pa <= pb && pa <= pc) return a;
|
|
1525
|
+
return pb <= pc ? b : c;
|
|
1526
|
+
}
|
|
1527
|
+
function undoPngPredictor(data, columns, colors, bits) {
|
|
1528
|
+
const bpp = Math.max(1, Math.ceil(colors * bits / 8));
|
|
1529
|
+
const rowBytes = Math.ceil(columns * colors * bits / 8);
|
|
1530
|
+
const rows = Math.floor(data.length / (rowBytes + 1));
|
|
1531
|
+
const out = new Uint8Array(rows * rowBytes);
|
|
1532
|
+
for (let r = 0; r < rows; r++) {
|
|
1533
|
+
const type = data[r * (rowBytes + 1)];
|
|
1534
|
+
const src = r * (rowBytes + 1) + 1;
|
|
1535
|
+
const dst = r * rowBytes;
|
|
1536
|
+
for (let x = 0; x < rowBytes; x++) {
|
|
1537
|
+
const raw = data[src + x];
|
|
1538
|
+
const left = x >= bpp ? out[dst + x - bpp] : 0;
|
|
1539
|
+
const up = r > 0 ? out[dst - rowBytes + x] : 0;
|
|
1540
|
+
const upLeft = r > 0 && x >= bpp ? out[dst - rowBytes + x - bpp] : 0;
|
|
1541
|
+
let value;
|
|
1542
|
+
switch (type) {
|
|
1543
|
+
case 0:
|
|
1544
|
+
value = raw;
|
|
1545
|
+
break;
|
|
1546
|
+
case 1:
|
|
1547
|
+
value = raw + left;
|
|
1548
|
+
break;
|
|
1549
|
+
case 2:
|
|
1550
|
+
value = raw + up;
|
|
1551
|
+
break;
|
|
1552
|
+
case 3:
|
|
1553
|
+
value = raw + (left + up >> 1);
|
|
1554
|
+
break;
|
|
1555
|
+
case 4:
|
|
1556
|
+
value = raw + paeth(left, up, upLeft);
|
|
1557
|
+
break;
|
|
1558
|
+
default: throw new PdfSyntaxError(`Unknown PNG filter type ${type}`);
|
|
1559
|
+
}
|
|
1560
|
+
out[dst + x] = value & 255;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
/** The decoded data of the stream whose dictionary is `dict` and whose data starts at `start`. `length` is `/Length`, already resolved. */
|
|
1566
|
+
async function decodeStream(bytes, dict, start, length) {
|
|
1567
|
+
const filter = dictGet(dict, "Filter") ?? dictGet(dict, "F");
|
|
1568
|
+
const filters = Array.isArray(filter) ? filter : filter === void 0 ? [] : [filter];
|
|
1569
|
+
if (filters.length === 0) return rawData(bytes, start, length);
|
|
1570
|
+
if (filters.length > 1 || !isName(filters[0], "FlateDecode")) throw new PdfSyntaxError("Unsupported stream filter");
|
|
1571
|
+
const inflated = await inflate(rawData(bytes, start, length));
|
|
1572
|
+
const parms = decodeParms(dict);
|
|
1573
|
+
const predictor = numberParam(parms, "Predictor", 1);
|
|
1574
|
+
if (predictor === 1) return inflated;
|
|
1575
|
+
if (predictor < 10) throw new PdfSyntaxError(`Unsupported predictor ${predictor}`);
|
|
1576
|
+
return undoPngPredictor(inflated, numberParam(parms, "Columns", 1), numberParam(parms, "Colors", 1), numberParam(parms, "BitsPerComponent", 8));
|
|
1577
|
+
}
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region src/pdf/xref.ts
|
|
1580
|
+
const isWhite = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
1581
|
+
const latin1 = (bytes, from, to) => {
|
|
1582
|
+
let out = "";
|
|
1583
|
+
for (let i = from; i < to; i++) out += String.fromCharCode(bytes[i]);
|
|
1584
|
+
return out;
|
|
1585
|
+
};
|
|
1586
|
+
function readToken(bytes, offset) {
|
|
1587
|
+
let i = offset;
|
|
1588
|
+
while (i < bytes.length && isWhite(bytes[i])) i++;
|
|
1589
|
+
const start = i;
|
|
1590
|
+
while (i < bytes.length && !isWhite(bytes[i])) i++;
|
|
1591
|
+
return {
|
|
1592
|
+
token: latin1(bytes, start, i),
|
|
1593
|
+
end: i
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function readInt(bytes, offset) {
|
|
1597
|
+
const { token, end } = readToken(bytes, offset);
|
|
1598
|
+
if (!/^\d+$/.test(token)) throw new Error(`Expected an integer at ${offset}`);
|
|
1599
|
+
return {
|
|
1600
|
+
n: Number(token),
|
|
1601
|
+
end
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function findStartxref(bytes) {
|
|
1605
|
+
const tail = latin1(bytes, Math.max(0, bytes.length - 1024), bytes.length);
|
|
1606
|
+
const at = tail.lastIndexOf("startxref");
|
|
1607
|
+
if (at < 0) throw new Error("No startxref");
|
|
1608
|
+
return readInt(bytes, bytes.length - tail.length + at + 9).n;
|
|
1609
|
+
}
|
|
1610
|
+
function readTable(bytes, offset) {
|
|
1611
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1612
|
+
let i = readToken(bytes, offset).end;
|
|
1613
|
+
for (;;) {
|
|
1614
|
+
const head = readToken(bytes, i);
|
|
1615
|
+
if (head.token.startsWith("trailer")) {
|
|
1616
|
+
const trailer = parseValue(bytes, head.end - (head.token.length - 7)).value;
|
|
1617
|
+
if (!isDict(trailer)) throw new Error("Trailer is not a dictionary");
|
|
1618
|
+
return {
|
|
1619
|
+
entries,
|
|
1620
|
+
trailer
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const first = readInt(bytes, i);
|
|
1624
|
+
const count = readInt(bytes, first.end);
|
|
1625
|
+
i = count.end;
|
|
1626
|
+
for (let k = 0; k < count.n; k++) {
|
|
1627
|
+
const off = readInt(bytes, i);
|
|
1628
|
+
const gen = readInt(bytes, off.end);
|
|
1629
|
+
const kind = readToken(bytes, gen.end);
|
|
1630
|
+
i = kind.end;
|
|
1631
|
+
if (kind.token === "n") entries.set(first.n + k, {
|
|
1632
|
+
type: "offset",
|
|
1633
|
+
offset: off.n,
|
|
1634
|
+
gen: gen.n
|
|
1635
|
+
});
|
|
1636
|
+
else if (kind.token === "f") entries.set(first.n + k, { type: "free" });
|
|
1637
|
+
else throw new Error(`Bad xref entry type "${kind.token}"`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const numbers = (v) => Array.isArray(v) && v.every((x) => typeof x === "number" && x >= 0) ? v : void 0;
|
|
1642
|
+
async function readStream(bytes, offset) {
|
|
1643
|
+
const obj = parseIndirect(bytes, offset);
|
|
1644
|
+
const dict = obj.value;
|
|
1645
|
+
if (!isDict(dict) || obj.streamStart === void 0) throw new Error("Xref stream expected");
|
|
1646
|
+
const length = dict.entries.get("Length");
|
|
1647
|
+
const data = await decodeStream(bytes, dict, obj.streamStart, typeof length === "number" ? length : void 0);
|
|
1648
|
+
const w = numbers(dict.entries.get("W"));
|
|
1649
|
+
if (!w || w.length !== 3 || w.some((x) => x > 8)) throw new Error("Bad /W");
|
|
1650
|
+
const size = dict.entries.get("Size");
|
|
1651
|
+
const index = numbers(dict.entries.get("Index")) ?? [0, typeof size === "number" ? size : 0];
|
|
1652
|
+
const [w0, w1, w2] = w;
|
|
1653
|
+
const rowLength = w0 + w1 + w2;
|
|
1654
|
+
if (rowLength === 0) throw new Error("Empty xref rows");
|
|
1655
|
+
const field = (at, width) => {
|
|
1656
|
+
let n = 0;
|
|
1657
|
+
for (let k = 0; k < width; k++) n = n * 256 + data[at + k];
|
|
1658
|
+
return n;
|
|
1659
|
+
};
|
|
1660
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1661
|
+
let row = 0;
|
|
1662
|
+
for (let s = 0; s + 1 < index.length; s += 2) for (let k = 0; k < index[s + 1]; k++, row++) {
|
|
1663
|
+
const at = row * rowLength;
|
|
1664
|
+
if (at + rowLength > data.length) return {
|
|
1665
|
+
entries,
|
|
1666
|
+
trailer: dict
|
|
1667
|
+
};
|
|
1668
|
+
const type = w0 === 0 ? 1 : field(at, w0);
|
|
1669
|
+
const a = field(at + w0, w1);
|
|
1670
|
+
const b = field(at + w0 + w1, w2);
|
|
1671
|
+
const num = index[s] + k;
|
|
1672
|
+
if (type === 0) entries.set(num, { type: "free" });
|
|
1673
|
+
else if (type === 1) entries.set(num, {
|
|
1674
|
+
type: "offset",
|
|
1675
|
+
offset: a,
|
|
1676
|
+
gen: b
|
|
1677
|
+
});
|
|
1678
|
+
else if (type === 2) entries.set(num, {
|
|
1679
|
+
type: "compressed",
|
|
1680
|
+
stream: a,
|
|
1681
|
+
index: b
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
entries,
|
|
1686
|
+
trailer: dict
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
async function readSection(bytes, offset) {
|
|
1690
|
+
if (!Number.isInteger(offset) || offset < 0 || offset >= bytes.length) throw new Error("Xref offset out of range");
|
|
1691
|
+
return readToken(bytes, offset).token === "xref" ? readTable(bytes, offset) : readStream(bytes, offset);
|
|
1692
|
+
}
|
|
1693
|
+
/** Newest entry wins along the /Prev chain (and a hybrid file's /XRefStm). Undefined when startxref or a section cannot be read. */
|
|
1694
|
+
async function readXref(bytes) {
|
|
1695
|
+
try {
|
|
1696
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1697
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1698
|
+
let trailer;
|
|
1699
|
+
let next = findStartxref(bytes);
|
|
1700
|
+
while (next !== void 0 && !visited.has(next)) {
|
|
1701
|
+
visited.add(next);
|
|
1702
|
+
const section = await readSection(bytes, next);
|
|
1703
|
+
trailer ??= section.trailer;
|
|
1704
|
+
const hybrid = section.trailer.entries.get("XRefStm");
|
|
1705
|
+
if (typeof hybrid === "number" && !visited.has(hybrid)) {
|
|
1706
|
+
visited.add(hybrid);
|
|
1707
|
+
const extra = await readSection(bytes, hybrid);
|
|
1708
|
+
for (const [num, e] of extra.entries) if (!entries.has(num)) entries.set(num, e);
|
|
1709
|
+
}
|
|
1710
|
+
for (const [num, e] of section.entries) if (!entries.has(num)) entries.set(num, e);
|
|
1711
|
+
const prev = section.trailer.entries.get("Prev");
|
|
1712
|
+
next = typeof prev === "number" ? prev : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
return trailer ? {
|
|
1715
|
+
entries,
|
|
1716
|
+
trailer
|
|
1717
|
+
} : void 0;
|
|
1718
|
+
} catch {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region src/pdf/objects.ts
|
|
1724
|
+
var Reader = class {
|
|
1725
|
+
bytes;
|
|
1726
|
+
xref;
|
|
1727
|
+
trailer;
|
|
1728
|
+
objectStreams = /* @__PURE__ */ new Map();
|
|
1729
|
+
constructor(bytes, xref) {
|
|
1730
|
+
this.bytes = bytes;
|
|
1731
|
+
this.xref = xref;
|
|
1732
|
+
this.trailer = xref.trailer;
|
|
1733
|
+
}
|
|
1734
|
+
async get(ref) {
|
|
1735
|
+
try {
|
|
1736
|
+
return await this.load(ref, true);
|
|
1737
|
+
} catch {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async resolve(v) {
|
|
1742
|
+
return isRef(v) ? this.get(v) : v;
|
|
1743
|
+
}
|
|
1744
|
+
/** `followLength` is false while resolving a /Length, so a self-referencing length cannot recurse. */
|
|
1745
|
+
async load(ref, followLength) {
|
|
1746
|
+
const entry = this.xref.entries.get(ref.num);
|
|
1747
|
+
if (!entry || entry.type === "free") return void 0;
|
|
1748
|
+
if (entry.type === "offset") {
|
|
1749
|
+
if (entry.gen !== ref.gen) return void 0;
|
|
1750
|
+
const obj = parseIndirect(this.bytes, entry.offset);
|
|
1751
|
+
return obj.num === ref.num && obj.gen === ref.gen ? obj.value : void 0;
|
|
1752
|
+
}
|
|
1753
|
+
if (ref.gen !== 0) return void 0;
|
|
1754
|
+
const container = await this.objectStream(entry.stream, followLength);
|
|
1755
|
+
if (!container) return void 0;
|
|
1756
|
+
let pair = container.pairs[entry.index];
|
|
1757
|
+
if (pair?.num !== ref.num) pair = container.pairs.find((p) => p.num === ref.num);
|
|
1758
|
+
if (!pair) return void 0;
|
|
1759
|
+
return parseValue(container.decoded, container.first + pair.offset).value;
|
|
1760
|
+
}
|
|
1761
|
+
objectStream(num, followLength) {
|
|
1762
|
+
let cached = this.objectStreams.get(num);
|
|
1763
|
+
if (!cached) {
|
|
1764
|
+
cached = this.readObjectStream(num, followLength).catch(() => void 0);
|
|
1765
|
+
this.objectStreams.set(num, cached);
|
|
1766
|
+
}
|
|
1767
|
+
return cached;
|
|
1768
|
+
}
|
|
1769
|
+
async readObjectStream(num, followLength) {
|
|
1770
|
+
const entry = this.xref.entries.get(num);
|
|
1771
|
+
if (entry?.type !== "offset") return void 0;
|
|
1772
|
+
const obj = parseIndirect(this.bytes, entry.offset);
|
|
1773
|
+
const dict = obj.value;
|
|
1774
|
+
if (obj.num !== num || !isDict(dict) || obj.streamStart === void 0) return void 0;
|
|
1775
|
+
const n = dict.entries.get("N");
|
|
1776
|
+
const first = dict.entries.get("First");
|
|
1777
|
+
if (typeof n !== "number" || typeof first !== "number") return void 0;
|
|
1778
|
+
let length = dict.entries.get("Length");
|
|
1779
|
+
if (isRef(length)) length = followLength ? await this.loadLength(length) : void 0;
|
|
1780
|
+
const decoded = await decodeStream(this.bytes, dict, obj.streamStart, typeof length === "number" ? length : void 0);
|
|
1781
|
+
const pairs = [];
|
|
1782
|
+
let at = 0;
|
|
1783
|
+
for (let i = 0; i < n; i++) {
|
|
1784
|
+
const a = parseValue(decoded, at);
|
|
1785
|
+
const b = parseValue(decoded, a.end);
|
|
1786
|
+
if (typeof a.value !== "number" || typeof b.value !== "number") return void 0;
|
|
1787
|
+
pairs.push({
|
|
1788
|
+
num: a.value,
|
|
1789
|
+
offset: b.value
|
|
1790
|
+
});
|
|
1791
|
+
at = b.end;
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
decoded,
|
|
1795
|
+
first,
|
|
1796
|
+
pairs
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
async loadLength(ref) {
|
|
1800
|
+
try {
|
|
1801
|
+
return await this.load(ref, false);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
/** Undefined when the file is encrypted (`/Encrypt` in the trailer) or its xref cannot be read. */
|
|
1808
|
+
async function openPdfObjects(bytes) {
|
|
1809
|
+
try {
|
|
1810
|
+
const xref = await readXref(bytes);
|
|
1811
|
+
if (!xref || xref.trailer.entries.has("Encrypt")) return void 0;
|
|
1812
|
+
return new Reader(bytes, xref);
|
|
1813
|
+
} catch {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region src/model/annotations/embedded-fields.ts
|
|
1819
|
+
/**
|
|
1820
|
+
* `a` with `fields` merged in: `callout`, `fringe`, `head` and the font keys of `defaultStyle`
|
|
1821
|
+
* on a FreeText, `icon` on a note. Undefined when `a` is not embedded, the fields do not fit its
|
|
1822
|
+
* type, or the result fails the schema.
|
|
1823
|
+
*/
|
|
1824
|
+
function withEmbeddedFields(a, fields) {
|
|
1825
|
+
if (a.source !== "pdf") return void 0;
|
|
1826
|
+
const { callout, fringe, head, icon, font } = fields;
|
|
1827
|
+
let candidate;
|
|
1828
|
+
if (a.type === "text") {
|
|
1829
|
+
if (icon === void 0 || callout || fringe || head !== void 0 || font) return void 0;
|
|
1830
|
+
candidate = {
|
|
1831
|
+
...a,
|
|
1832
|
+
icon
|
|
1833
|
+
};
|
|
1834
|
+
} else if (a.type === "freetext") {
|
|
1835
|
+
if (icon !== void 0) return void 0;
|
|
1836
|
+
candidate = {
|
|
1837
|
+
...a,
|
|
1838
|
+
...callout ? { callout } : {},
|
|
1839
|
+
...fringe ? { fringe } : {},
|
|
1840
|
+
...head !== void 0 ? { head } : {},
|
|
1841
|
+
defaultStyle: {
|
|
1842
|
+
...a.defaultStyle,
|
|
1843
|
+
...font
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
} else return;
|
|
1847
|
+
const parsed = AnnotationSchema.safeParse(candidate);
|
|
1848
|
+
return parsed.success ? parsed.data : void 0;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Reads every target once, calling `getData` only if a target names an annotation in
|
|
1852
|
+
* `annotations` by an object reference, and returns the fields read, by id. A target the file
|
|
1853
|
+
* adds nothing to, or whose fields would fail the schema, is left out. Never throws: any
|
|
1854
|
+
* failure leaves the annotations as they are.
|
|
1855
|
+
*/
|
|
1856
|
+
async function readEmbeddedFields(annotations, targets, getData) {
|
|
1857
|
+
const read = /* @__PURE__ */ new Map();
|
|
1858
|
+
if (targets.length === 0) return read;
|
|
1859
|
+
try {
|
|
1860
|
+
const byId = new Map(annotations.map((a) => [a.id, a]));
|
|
1861
|
+
const work = targets.flatMap((target) => {
|
|
1862
|
+
const current = byId.get(target.id);
|
|
1863
|
+
const ref = refOfPdfjsId(target.id.split(":")[2] ?? "");
|
|
1864
|
+
return current && ref ? [{
|
|
1865
|
+
target,
|
|
1866
|
+
current,
|
|
1867
|
+
ref
|
|
1868
|
+
}] : [];
|
|
1869
|
+
});
|
|
1870
|
+
if (work.length === 0) return read;
|
|
1871
|
+
const objs = await openPdfObjects(await getData());
|
|
1872
|
+
if (!objs) return read;
|
|
1873
|
+
for (const { target, current, ref } of work) try {
|
|
1874
|
+
const fields = await readFields(objs, target, ref);
|
|
1875
|
+
if (fields && withEmbeddedFields(current, fields)) read.set(current.id, fields);
|
|
1876
|
+
} catch {}
|
|
1877
|
+
} catch {}
|
|
1878
|
+
return read;
|
|
1879
|
+
}
|
|
1880
|
+
async function readFields(objs, target, ref) {
|
|
1881
|
+
if (target.subtype === "Text") {
|
|
1882
|
+
const icon = await readTextIcon(objs, ref);
|
|
1883
|
+
return icon === void 0 ? void 0 : { icon };
|
|
1884
|
+
}
|
|
1885
|
+
const fields = await readFreeTextFields(objs, ref, target.rect, target.daFont);
|
|
1886
|
+
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
1887
|
+
}
|
|
1888
|
+
//#endregion
|
|
1166
1889
|
//#region src/model/env.ts
|
|
1167
1890
|
/** True only when the build or runtime says production; browsers without `process` count as dev. */
|
|
1168
1891
|
function isProduction() {
|
|
@@ -1174,6 +1897,7 @@ function isProduction() {
|
|
|
1174
1897
|
}
|
|
1175
1898
|
//#endregion
|
|
1176
1899
|
//#region src/model/annotations/history.ts
|
|
1900
|
+
const noop = () => {};
|
|
1177
1901
|
/**
|
|
1178
1902
|
* Snapshot-based undo and redo over an uncontrolled store.
|
|
1179
1903
|
*
|
|
@@ -1193,6 +1917,8 @@ function createHistory(store, opts = {}) {
|
|
|
1193
1917
|
batch(fn) {
|
|
1194
1918
|
fn();
|
|
1195
1919
|
},
|
|
1920
|
+
track: () => noop,
|
|
1921
|
+
react: () => noop,
|
|
1196
1922
|
destroy() {}
|
|
1197
1923
|
};
|
|
1198
1924
|
const limit = opts.limit ?? 200;
|
|
@@ -1200,14 +1926,84 @@ function createHistory(store, opts = {}) {
|
|
|
1200
1926
|
const redoStack = [];
|
|
1201
1927
|
let previous = store.list.get();
|
|
1202
1928
|
let replaying = false;
|
|
1203
|
-
/**
|
|
1929
|
+
/**
|
|
1930
|
+
* Open step depth (batches, and an edit while its reactions run); while above zero, changes
|
|
1931
|
+
* update the baselines but push no entry.
|
|
1932
|
+
*/
|
|
1204
1933
|
let depth = 0;
|
|
1205
|
-
/** The
|
|
1206
|
-
let
|
|
1934
|
+
/** The lists when the outermost step opened (or were restarted by a replace). */
|
|
1935
|
+
let start = null;
|
|
1936
|
+
/** Each tracked store with its list as of the last change history saw. */
|
|
1937
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
1938
|
+
const reactions = /* @__PURE__ */ new Set();
|
|
1207
1939
|
const sync = () => {
|
|
1208
1940
|
canUndo.set(undoStack.length > 0);
|
|
1209
1941
|
canRedo.set(redoStack.length > 0);
|
|
1210
1942
|
};
|
|
1943
|
+
const push = (entry) => {
|
|
1944
|
+
undoStack.push(entry);
|
|
1945
|
+
if (undoStack.length > limit) undoStack.shift();
|
|
1946
|
+
redoStack.length = 0;
|
|
1947
|
+
sync();
|
|
1948
|
+
};
|
|
1949
|
+
/**
|
|
1950
|
+
* Forgets `recordStore`'s records in every entry, so undo and redo leave it alone. An entry
|
|
1951
|
+
* left with nothing to replay (no annotation change, no other store) is dropped.
|
|
1952
|
+
*/
|
|
1953
|
+
const dropEntries = (recordStore) => {
|
|
1954
|
+
for (const stack of [undoStack, redoStack]) {
|
|
1955
|
+
const kept = stack.filter((entry) => {
|
|
1956
|
+
if (!entry.records.delete(recordStore)) return true;
|
|
1957
|
+
const { before, after } = entry.annotations;
|
|
1958
|
+
return entry.records.size > 0 || !sameList(before, after);
|
|
1959
|
+
});
|
|
1960
|
+
stack.splice(0, stack.length, ...kept);
|
|
1961
|
+
}
|
|
1962
|
+
sync();
|
|
1963
|
+
};
|
|
1964
|
+
const resetStacks = () => {
|
|
1965
|
+
undoStack.length = 0;
|
|
1966
|
+
redoStack.length = 0;
|
|
1967
|
+
sync();
|
|
1968
|
+
};
|
|
1969
|
+
/** Opens the outermost step from `annotations` and every tracked store's baseline. */
|
|
1970
|
+
const open = (annotations) => {
|
|
1971
|
+
const records = /* @__PURE__ */ new Map();
|
|
1972
|
+
for (const [s, t] of tracked) records.set(s, t.previous);
|
|
1973
|
+
start = {
|
|
1974
|
+
annotations,
|
|
1975
|
+
records
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
/**
|
|
1979
|
+
* Closes the outermost step: one entry for everything that changed since it opened. A step
|
|
1980
|
+
* whose lists end where they began (the same objects in the same order) records nothing.
|
|
1981
|
+
*/
|
|
1982
|
+
const close = () => {
|
|
1983
|
+
const from = start;
|
|
1984
|
+
start = null;
|
|
1985
|
+
if (!from) return;
|
|
1986
|
+
const annotations = {
|
|
1987
|
+
before: from.annotations,
|
|
1988
|
+
after: store.list.get()
|
|
1989
|
+
};
|
|
1990
|
+
previous = annotations.after;
|
|
1991
|
+
const records = /* @__PURE__ */ new Map();
|
|
1992
|
+
for (const [s, t] of tracked) {
|
|
1993
|
+
const before = from.records.get(s) ?? t.previous;
|
|
1994
|
+
const after = s.list.get();
|
|
1995
|
+
t.previous = after;
|
|
1996
|
+
if (!sameList(before, after)) records.set(s, {
|
|
1997
|
+
before,
|
|
1998
|
+
after
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
if (records.size === 0 && sameList(annotations.before, annotations.after)) return;
|
|
2002
|
+
push({
|
|
2003
|
+
annotations,
|
|
2004
|
+
records
|
|
2005
|
+
});
|
|
2006
|
+
};
|
|
1211
2007
|
let destroyed = false;
|
|
1212
2008
|
const unsubscribe = store.on("change", (change) => {
|
|
1213
2009
|
const current = store.list.get();
|
|
@@ -1215,31 +2011,45 @@ function createHistory(store, opts = {}) {
|
|
|
1215
2011
|
previous = current;
|
|
1216
2012
|
return;
|
|
1217
2013
|
}
|
|
2014
|
+
if (change.kind === "replace" && change.reason === "embedded") {
|
|
2015
|
+
const fill = (list) => list.map((a) => {
|
|
2016
|
+
const f = change.fields.get(a.id);
|
|
2017
|
+
return f && withEmbeddedFields(a, f) || a;
|
|
2018
|
+
});
|
|
2019
|
+
for (const { annotations } of [...undoStack, ...redoStack]) {
|
|
2020
|
+
annotations.before = fill(annotations.before);
|
|
2021
|
+
annotations.after = fill(annotations.after);
|
|
2022
|
+
}
|
|
2023
|
+
if (start) start.annotations = fill(start.annotations);
|
|
2024
|
+
previous = current;
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
1218
2027
|
if (change.kind === "replace") {
|
|
1219
|
-
undoStack.length = 0;
|
|
1220
|
-
redoStack.length = 0;
|
|
1221
2028
|
previous = current;
|
|
1222
|
-
if (depth > 0)
|
|
1223
|
-
|
|
2029
|
+
if (depth > 0) open(current);
|
|
2030
|
+
resetStacks();
|
|
1224
2031
|
return;
|
|
1225
2032
|
}
|
|
1226
2033
|
if (depth > 0) {
|
|
1227
2034
|
previous = current;
|
|
2035
|
+
for (const fn of [...reactions]) fn(change);
|
|
1228
2036
|
return;
|
|
1229
2037
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
sync();
|
|
2038
|
+
open(previous);
|
|
2039
|
+
depth++;
|
|
2040
|
+
try {
|
|
2041
|
+
for (const fn of [...reactions]) fn(change);
|
|
2042
|
+
} finally {
|
|
2043
|
+
if (--depth === 0 && !destroyed) close();
|
|
2044
|
+
}
|
|
1238
2045
|
});
|
|
1239
|
-
|
|
2046
|
+
/** Replays a step's lists under the `replaying` guard, skipping stores no longer tracked. */
|
|
2047
|
+
const apply = (entry, side) => {
|
|
1240
2048
|
replaying = true;
|
|
1241
2049
|
try {
|
|
1242
|
-
|
|
2050
|
+
const { annotations } = entry;
|
|
2051
|
+
if (!sameList(annotations.before, annotations.after)) store.replaceAll([...annotations[side]], "history");
|
|
2052
|
+
for (const [s, snapshot] of entry.records) if (tracked.has(s)) s.replaceAll([...snapshot[side]], "history");
|
|
1243
2053
|
} finally {
|
|
1244
2054
|
replaying = false;
|
|
1245
2055
|
}
|
|
@@ -1251,14 +2061,14 @@ function createHistory(store, opts = {}) {
|
|
|
1251
2061
|
const entry = undoStack.pop();
|
|
1252
2062
|
if (destroyed || !entry) return;
|
|
1253
2063
|
redoStack.push(entry);
|
|
1254
|
-
apply(entry
|
|
2064
|
+
apply(entry, "before");
|
|
1255
2065
|
sync();
|
|
1256
2066
|
},
|
|
1257
2067
|
redo() {
|
|
1258
2068
|
const entry = redoStack.pop();
|
|
1259
2069
|
if (destroyed || !entry) return;
|
|
1260
2070
|
undoStack.push(entry);
|
|
1261
|
-
apply(entry
|
|
2071
|
+
apply(entry, "after");
|
|
1262
2072
|
sync();
|
|
1263
2073
|
},
|
|
1264
2074
|
batch(fn) {
|
|
@@ -1266,50 +2076,202 @@ function createHistory(store, opts = {}) {
|
|
|
1266
2076
|
fn();
|
|
1267
2077
|
return;
|
|
1268
2078
|
}
|
|
1269
|
-
if (depth++ === 0)
|
|
2079
|
+
if (depth++ === 0) open(store.list.get());
|
|
1270
2080
|
try {
|
|
1271
2081
|
const result = fn();
|
|
1272
2082
|
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.");
|
|
1273
2083
|
} finally {
|
|
1274
|
-
if (--depth === 0 && !destroyed)
|
|
1275
|
-
const before = batchStart;
|
|
1276
|
-
batchStart = null;
|
|
1277
|
-
const after = store.list.get();
|
|
1278
|
-
if (before && !sameAnnotations(before, after)) {
|
|
1279
|
-
undoStack.push({
|
|
1280
|
-
before,
|
|
1281
|
-
after
|
|
1282
|
-
});
|
|
1283
|
-
if (undoStack.length > limit) undoStack.shift();
|
|
1284
|
-
redoStack.length = 0;
|
|
1285
|
-
sync();
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
2084
|
+
if (--depth === 0 && !destroyed) close();
|
|
1288
2085
|
}
|
|
1289
2086
|
},
|
|
2087
|
+
track(recordStore) {
|
|
2088
|
+
if (destroyed || tracked.has(recordStore)) return noop;
|
|
2089
|
+
const unsubscribeStore = recordStore.on("change", (change) => {
|
|
2090
|
+
const t = tracked.get(recordStore);
|
|
2091
|
+
if (!t) return;
|
|
2092
|
+
const before = t.previous;
|
|
2093
|
+
const current = recordStore.list.get();
|
|
2094
|
+
t.previous = current;
|
|
2095
|
+
if (replaying || change.kind === "replace" && change.reason === "history") return;
|
|
2096
|
+
if (change.kind === "replace") {
|
|
2097
|
+
if (depth > 0 && start) start.records.set(recordStore, current);
|
|
2098
|
+
dropEntries(recordStore);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (depth > 0) return;
|
|
2102
|
+
push({
|
|
2103
|
+
annotations: {
|
|
2104
|
+
before: previous,
|
|
2105
|
+
after: previous
|
|
2106
|
+
},
|
|
2107
|
+
records: /* @__PURE__ */ new Map([[recordStore, {
|
|
2108
|
+
before,
|
|
2109
|
+
after: current
|
|
2110
|
+
}]])
|
|
2111
|
+
});
|
|
2112
|
+
});
|
|
2113
|
+
tracked.set(recordStore, {
|
|
2114
|
+
previous: recordStore.list.get(),
|
|
2115
|
+
unsubscribe: unsubscribeStore
|
|
2116
|
+
});
|
|
2117
|
+
if (start) start.records.set(recordStore, recordStore.list.get());
|
|
2118
|
+
return () => {
|
|
2119
|
+
if (tracked.get(recordStore)?.unsubscribe !== unsubscribeStore) return;
|
|
2120
|
+
unsubscribeStore();
|
|
2121
|
+
tracked.delete(recordStore);
|
|
2122
|
+
start?.records.delete(recordStore);
|
|
2123
|
+
};
|
|
2124
|
+
},
|
|
2125
|
+
react(fn) {
|
|
2126
|
+
if (destroyed) return noop;
|
|
2127
|
+
const run = (edit) => fn(edit);
|
|
2128
|
+
reactions.add(run);
|
|
2129
|
+
return () => {
|
|
2130
|
+
reactions.delete(run);
|
|
2131
|
+
};
|
|
2132
|
+
},
|
|
1290
2133
|
destroy() {
|
|
1291
2134
|
destroyed = true;
|
|
1292
|
-
|
|
2135
|
+
start = null;
|
|
1293
2136
|
unsubscribe();
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
2137
|
+
for (const t of tracked.values()) t.unsubscribe();
|
|
2138
|
+
tracked.clear();
|
|
2139
|
+
reactions.clear();
|
|
2140
|
+
resetStacks();
|
|
1297
2141
|
},
|
|
1298
2142
|
clear() {
|
|
1299
|
-
undoStack.length = 0;
|
|
1300
|
-
redoStack.length = 0;
|
|
1301
2143
|
previous = store.list.get();
|
|
1302
|
-
|
|
2144
|
+
for (const [s, t] of tracked) t.previous = s.list.get();
|
|
2145
|
+
resetStacks();
|
|
1303
2146
|
}
|
|
1304
2147
|
};
|
|
1305
2148
|
}
|
|
1306
|
-
/** The same
|
|
1307
|
-
function
|
|
2149
|
+
/** The same objects in the same order. */
|
|
2150
|
+
function sameList(a, b) {
|
|
1308
2151
|
return a === b || a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1309
2152
|
}
|
|
1310
2153
|
//#endregion
|
|
2154
|
+
//#region src/model/annotations/gloss-mapping.ts
|
|
2155
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2156
|
+
/** `rest` with `own` merged into a copy of its `custom.inkbar`; `own` wins. */
|
|
2157
|
+
function withCustom(rest, own) {
|
|
2158
|
+
const custom = isObject(rest.custom) ? rest.custom : {};
|
|
2159
|
+
const inkbar = isObject(custom.inkbar) ? custom.inkbar : {};
|
|
2160
|
+
return {
|
|
2161
|
+
...rest,
|
|
2162
|
+
custom: {
|
|
2163
|
+
...custom,
|
|
2164
|
+
inkbar: {
|
|
2165
|
+
...inkbar,
|
|
2166
|
+
...own
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* An annotation as a Gloss file writes it. Gloss has no loupe or mask and no pen pressure or
|
|
2173
|
+
* free-text border colour, so these go under `custom.inkbar`, with loupes and masks written as
|
|
2174
|
+
* the circle or square they draw.
|
|
2175
|
+
*/
|
|
2176
|
+
function toFile(annotation) {
|
|
2177
|
+
switch (annotation.type) {
|
|
2178
|
+
case "ink": {
|
|
2179
|
+
const { pressure, ...rest } = annotation;
|
|
2180
|
+
return pressure ? withCustom(rest, { pressure }) : rest;
|
|
2181
|
+
}
|
|
2182
|
+
case "freetext": {
|
|
2183
|
+
const { borderColor, ...rest } = annotation;
|
|
2184
|
+
return borderColor ? withCustom(rest, { borderColor }) : rest;
|
|
2185
|
+
}
|
|
2186
|
+
case "loupe": {
|
|
2187
|
+
const { magnification, ...rest } = annotation;
|
|
2188
|
+
return withCustom({
|
|
2189
|
+
...rest,
|
|
2190
|
+
type: "circle"
|
|
2191
|
+
}, {
|
|
2192
|
+
type: "loupe",
|
|
2193
|
+
magnification
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
case "mask": {
|
|
2197
|
+
const { shape, dim, ...rest } = annotation;
|
|
2198
|
+
return withCustom({
|
|
2199
|
+
...rest,
|
|
2200
|
+
type: shape === "ellipse" ? "circle" : "square"
|
|
2201
|
+
}, {
|
|
2202
|
+
type: "mask",
|
|
2203
|
+
dim
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
default: return annotation;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Moves `keys` from `custom.inkbar` to the top level of `raw`, replacing any top-level value
|
|
2211
|
+
* (an absent key deletes it), then drops `custom.inkbar`, and `custom`, if that leaves them
|
|
2212
|
+
* empty. Everything else under `custom` stays as it was.
|
|
2213
|
+
*/
|
|
2214
|
+
function lift(raw, inkbar, keys, extra = {}) {
|
|
2215
|
+
const item = {
|
|
2216
|
+
...raw,
|
|
2217
|
+
...extra
|
|
2218
|
+
};
|
|
2219
|
+
const left = { ...inkbar };
|
|
2220
|
+
let consumed = false;
|
|
2221
|
+
for (const key of keys) {
|
|
2222
|
+
delete item[key];
|
|
2223
|
+
if (key in left) {
|
|
2224
|
+
item[key] = left[key];
|
|
2225
|
+
delete left[key];
|
|
2226
|
+
consumed = true;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (!consumed || !isObject(raw.custom)) return item;
|
|
2230
|
+
const { inkbar: _inkbar, ...others } = raw.custom;
|
|
2231
|
+
const custom = Object.keys(left).length > 0 ? {
|
|
2232
|
+
...others,
|
|
2233
|
+
inkbar: left
|
|
2234
|
+
} : others;
|
|
2235
|
+
if (Object.keys(custom).length > 0) item.custom = custom;
|
|
2236
|
+
else delete item.custom;
|
|
2237
|
+
return item;
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* A raw file item as Inkbar parses it: Inkbar's own data moves out of `custom.inkbar`. An item
|
|
2241
|
+
* it doesn't recognise passes through unchanged.
|
|
2242
|
+
*/
|
|
2243
|
+
function fromFile(raw) {
|
|
2244
|
+
const inkbar = isObject(raw.custom) && isObject(raw.custom.inkbar) ? raw.custom.inkbar : {};
|
|
2245
|
+
const from = (keys, extra) => ({
|
|
2246
|
+
item: lift(raw, inkbar, keys, extra),
|
|
2247
|
+
lifted: keys
|
|
2248
|
+
});
|
|
2249
|
+
const unchanged = {
|
|
2250
|
+
item: raw,
|
|
2251
|
+
lifted: []
|
|
2252
|
+
};
|
|
2253
|
+
switch (raw.type) {
|
|
2254
|
+
case "ink": return from(["pressure"]);
|
|
2255
|
+
case "freetext": return from(["borderColor"]);
|
|
2256
|
+
case "circle":
|
|
2257
|
+
if (inkbar.type === "loupe") return from(["type", "magnification"]);
|
|
2258
|
+
if (inkbar.type === "mask") return from(["type", "dim"], { shape: "ellipse" });
|
|
2259
|
+
return unchanged;
|
|
2260
|
+
case "square":
|
|
2261
|
+
if (inkbar.type === "mask") return from(["type", "dim"], { shape: "rect" });
|
|
2262
|
+
return unchanged;
|
|
2263
|
+
default: return unchanged;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
//#endregion
|
|
1311
2267
|
//#region src/model/annotations/json.ts
|
|
1312
|
-
|
|
2268
|
+
/**
|
|
2269
|
+
* @internal The Gloss types this core parses, taken from its own schemas rather than Gloss's
|
|
2270
|
+
* list, so a type a later Gloss adds is still treated as foreign here. Loupes and masks are
|
|
2271
|
+
* Inkbar's own types: a file writes them as circles and squares, so a `loupe` or `mask` item is
|
|
2272
|
+
* foreign too.
|
|
2273
|
+
*/
|
|
2274
|
+
const KNOWN_TYPES = new Set(ExportedAnnotationSchema.options.flatMap((o) => [...o.shape.type._zod.values]).filter((type) => type !== "loupe" && type !== "mask"));
|
|
1313
2275
|
/**
|
|
1314
2276
|
* Session annotations plus any preserved foreign items. Embedded (`source: 'pdf'`) ones stay in
|
|
1315
2277
|
* the file. `source` and `locked` are viewer state and are left out.
|
|
@@ -1318,21 +2280,24 @@ const KNOWN_TYPES = new Set(ExportedAnnotationSchema.options.flatMap((o) => [...
|
|
|
1318
2280
|
* export would carry: editing it never touches the store, and values JSON cannot hold, such as
|
|
1319
2281
|
* functions or `undefined` inside `custom`, are left out exactly as `JSON.stringify` would.
|
|
1320
2282
|
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
2283
|
+
* Gloss has no loupe or mask, pen pressure or free-text border colour, so Inkbar writes these
|
|
2284
|
+
* under `custom.inkbar`, with loupes and masks as the circle or square they draw. Every other key
|
|
2285
|
+
* under `custom` goes out as it is.
|
|
2286
|
+
*
|
|
2287
|
+
* The result validates against the Gloss schema (`gloss-format/schema.json`). Foreign items
|
|
2288
|
+
* validate as unknown annotations while their type is one Gloss does not define.
|
|
1324
2289
|
*/
|
|
1325
2290
|
function exportAnnotations(store, preserved = [], documentKey) {
|
|
1326
|
-
const session = store.list.get().filter((a) => a.source === "session").map(({ source: _source, locked: _locked, ...rest }) => rest);
|
|
2291
|
+
const session = store.list.get().filter((a) => a.source === "session").map(({ source: _source, locked: _locked, ...rest }) => toFile(rest));
|
|
1327
2292
|
return {
|
|
1328
|
-
format:
|
|
1329
|
-
version: 1,
|
|
2293
|
+
format: GLOSS_FORMAT$1,
|
|
2294
|
+
version: GLOSS_VERSION$1,
|
|
1330
2295
|
...documentKey === void 0 ? {} : { documentKey },
|
|
1331
2296
|
annotations: JSON.parse(JSON.stringify([...session, ...preserved]))
|
|
1332
2297
|
};
|
|
1333
2298
|
}
|
|
1334
2299
|
/**
|
|
1335
|
-
* Validates the envelope (throws a TypeError when it is not
|
|
2300
|
+
* Validates the envelope (throws a TypeError when it is not a gloss v1 document),
|
|
1336
2301
|
* then each item on its own. Embedded annotations are never touched; imported items become
|
|
1337
2302
|
* session annotations.
|
|
1338
2303
|
*/
|
|
@@ -1353,7 +2318,7 @@ function importAnnotations(store, data, opts) {
|
|
|
1353
2318
|
function planImport(store, data, opts) {
|
|
1354
2319
|
const raw = typeof data === "string" ? JSON.parse(data) : data;
|
|
1355
2320
|
const envelope = AnnotationDocumentSchema.safeParse(raw);
|
|
1356
|
-
if (!envelope.success) throw new TypeError(`Expected
|
|
2321
|
+
if (!envelope.success) throw new TypeError(`Expected a ${GLOSS_FORMAT$1} v${GLOSS_VERSION$1} document: ${envelope.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
1357
2322
|
const embeddedIds = new Set(store.list.get().filter((a) => a.source === "pdf").map((a) => a.id));
|
|
1358
2323
|
const result = {
|
|
1359
2324
|
imported: 0,
|
|
@@ -1389,21 +2354,28 @@ function planImport(store, data, opts) {
|
|
|
1389
2354
|
accepted.add(id);
|
|
1390
2355
|
return true;
|
|
1391
2356
|
};
|
|
1392
|
-
envelope.data.annotations.forEach((
|
|
1393
|
-
const type =
|
|
2357
|
+
envelope.data.annotations.forEach((raw, index) => {
|
|
2358
|
+
const type = raw?.type;
|
|
1394
2359
|
if (typeof type === "string" && !KNOWN_TYPES.has(type)) {
|
|
1395
|
-
if (!isForeign(
|
|
2360
|
+
if (!isForeign(raw)) result.errors.push({
|
|
1396
2361
|
index,
|
|
1397
2362
|
issues: [`type: unknown type "${type}" needs a string id and a numeric page to be preserved`]
|
|
1398
2363
|
});
|
|
1399
|
-
else if (claim(
|
|
2364
|
+
else if (claim(raw.id, index)) result.unknown.push(raw);
|
|
1400
2365
|
return;
|
|
1401
2366
|
}
|
|
2367
|
+
const { item, lifted } = isRecord(raw) && typeof type === "string" ? fromFile(raw) : {
|
|
2368
|
+
item: raw,
|
|
2369
|
+
lifted: []
|
|
2370
|
+
};
|
|
1402
2371
|
const parsed = ExportedAnnotationSchema.safeParse(item);
|
|
1403
2372
|
if (!parsed.success) {
|
|
1404
2373
|
result.errors.push({
|
|
1405
2374
|
index,
|
|
1406
|
-
issues: parsed.error.issues.map((i) =>
|
|
2375
|
+
issues: parsed.error.issues.map((i) => {
|
|
2376
|
+
const path = i.path.join(".");
|
|
2377
|
+
return `${typeof i.path[0] === "string" && lifted.includes(i.path[0]) ? `custom.inkbar.${path}` : path}: ${i.message}`;
|
|
2378
|
+
})
|
|
1407
2379
|
});
|
|
1408
2380
|
return;
|
|
1409
2381
|
}
|
|
@@ -1435,8 +2407,11 @@ function planImport(store, data, opts) {
|
|
|
1435
2407
|
}
|
|
1436
2408
|
};
|
|
1437
2409
|
}
|
|
2410
|
+
function isRecord(item) {
|
|
2411
|
+
return typeof item === "object" && item !== null && !Array.isArray(item);
|
|
2412
|
+
}
|
|
1438
2413
|
function isForeign(item) {
|
|
1439
|
-
if (
|
|
2414
|
+
if (!isRecord(item)) return false;
|
|
1440
2415
|
const { type, id, page } = item;
|
|
1441
2416
|
return typeof type === "string" && typeof id === "string" && typeof page === "number";
|
|
1442
2417
|
}
|
|
@@ -1600,7 +2575,6 @@ function createAnnotationStore(opts) {
|
|
|
1600
2575
|
}
|
|
1601
2576
|
if (input.type === "redact") {
|
|
1602
2577
|
if (input.interiorColor === void 0) candidate.interiorColor = opts.defaults.fill();
|
|
1603
|
-
if (input.origin === "suggested" && input.suggestion && input.color === void 0) candidate.color = opts.defaults.suggestion(confidenceBucket(input.suggestion.confidence));
|
|
1604
2578
|
}
|
|
1605
2579
|
return AnnotationSchema.parse(candidate);
|
|
1606
2580
|
}
|
|
@@ -1615,10 +2589,12 @@ function createAnnotationStore(opts) {
|
|
|
1615
2589
|
if (!entry) return;
|
|
1616
2590
|
pending.delete(key);
|
|
1617
2591
|
if (pending.size === 0) proposed = void 0;
|
|
2592
|
+
pruneSelection(basis());
|
|
1618
2593
|
if (isApplied(entry.change, list.get()) || isProduction()) return;
|
|
1619
2594
|
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.`);
|
|
1620
2595
|
}
|
|
1621
2596
|
function commit(next, change) {
|
|
2597
|
+
if (change.kind !== "replace") emitter.emit("edit", change);
|
|
1622
2598
|
if (controlled) {
|
|
1623
2599
|
if (change.kind !== "replace") {
|
|
1624
2600
|
const key = changeKey(change);
|
|
@@ -1717,6 +2693,7 @@ function createAnnotationStore(opts) {
|
|
|
1717
2693
|
after
|
|
1718
2694
|
};
|
|
1719
2695
|
if (controlled && before.source === "pdf") {
|
|
2696
|
+
emitter.emit("edit", change);
|
|
1720
2697
|
list.set(swap(list.get()));
|
|
1721
2698
|
if (proposed) proposed = next;
|
|
1722
2699
|
emitter.emit("change", change);
|
|
@@ -1785,6 +2762,26 @@ function createAnnotationStore(opts) {
|
|
|
1785
2762
|
reason: "import"
|
|
1786
2763
|
});
|
|
1787
2764
|
},
|
|
2765
|
+
_patchEmbedded(fields) {
|
|
2766
|
+
if (destroyed) return;
|
|
2767
|
+
const applied = /* @__PURE__ */ new Map();
|
|
2768
|
+
const merge = (items) => items.map((a) => {
|
|
2769
|
+
const f = a.source === "pdf" ? fields.get(a.id) : void 0;
|
|
2770
|
+
const merged = f && withEmbeddedFields(a, f);
|
|
2771
|
+
if (!merged) return a;
|
|
2772
|
+
applied.set(a.id, f);
|
|
2773
|
+
return merged;
|
|
2774
|
+
});
|
|
2775
|
+
const next = merge(list.get());
|
|
2776
|
+
if (applied.size === 0) return;
|
|
2777
|
+
list.set(next);
|
|
2778
|
+
if (proposed) proposed = merge(proposed);
|
|
2779
|
+
emitter.emit("change", {
|
|
2780
|
+
kind: "replace",
|
|
2781
|
+
reason: "embedded",
|
|
2782
|
+
fields: applied
|
|
2783
|
+
});
|
|
2784
|
+
},
|
|
1788
2785
|
on: (event, fn) => emitter.on(event, fn),
|
|
1789
2786
|
destroy() {
|
|
1790
2787
|
destroyed = true;
|
|
@@ -2019,6 +3016,8 @@ async function openDocument(source, options) {
|
|
|
2019
3016
|
});
|
|
2020
3017
|
const embedded = [];
|
|
2021
3018
|
let preserved = [];
|
|
3019
|
+
/** Embedded annotations whose PDF.js data lacks fields the file holds; read after `ready`. */
|
|
3020
|
+
const targets = [];
|
|
2022
3021
|
const destroyedError = () => new InkError("DocumentDestroyed", "The document has been destroyed");
|
|
2023
3022
|
const ready = (async () => {
|
|
2024
3023
|
const pdfjs = await loadPdfjs(options.assets);
|
|
@@ -2059,6 +3058,7 @@ async function openDocument(source, options) {
|
|
|
2059
3058
|
});
|
|
2060
3059
|
embedded.push(...summarise(perPage.map((r) => r.counts)));
|
|
2061
3060
|
const fromFile = perPage.flatMap((r) => r.annotations);
|
|
3061
|
+
targets.push(...perPage.flatMap((r) => r.targets));
|
|
2062
3062
|
if (fromFile.length > 0) store._setEmbedded(fromFile);
|
|
2063
3063
|
})().then(() => {
|
|
2064
3064
|
status.set({ state: "ready" });
|
|
@@ -2075,7 +3075,14 @@ async function openDocument(source, options) {
|
|
|
2075
3075
|
const historyOption = options.history ?? true;
|
|
2076
3076
|
const historyLimit = historyOption === false ? 0 : historyOption === true ? void 0 : historyOption.limit;
|
|
2077
3077
|
const history = createHistory(store, historyLimit === void 0 ? {} : { limit: historyLimit });
|
|
2078
|
-
|
|
3078
|
+
ready.then(() => {
|
|
3079
|
+
if (destroyed || !handles) return;
|
|
3080
|
+
const { proxy } = handles;
|
|
3081
|
+
readEmbeddedFields(store.list.get(), targets, () => proxy.getData()).then((patched) => {
|
|
3082
|
+
if (!destroyed && patched.size > 0) store._patchEmbedded(patched);
|
|
3083
|
+
}).catch(() => void 0);
|
|
3084
|
+
}, () => void 0);
|
|
3085
|
+
const doc = {
|
|
2079
3086
|
key: resolved.key,
|
|
2080
3087
|
status: readonly(status),
|
|
2081
3088
|
ready,
|
|
@@ -2086,6 +3093,9 @@ async function openDocument(source, options) {
|
|
|
2086
3093
|
if (!handles) throw statusError(status.get());
|
|
2087
3094
|
return textContentFromPdfjs(page, await (await handles.getPage(page)).getTextContent());
|
|
2088
3095
|
},
|
|
3096
|
+
async pageText(page) {
|
|
3097
|
+
return (await doc.text(page)).items.map((item) => item.str).join(" ");
|
|
3098
|
+
},
|
|
2089
3099
|
annotations: store,
|
|
2090
3100
|
embedded,
|
|
2091
3101
|
history,
|
|
@@ -2115,6 +3125,7 @@ async function openDocument(source, options) {
|
|
|
2115
3125
|
return handles;
|
|
2116
3126
|
}
|
|
2117
3127
|
};
|
|
3128
|
+
return doc;
|
|
2118
3129
|
}
|
|
2119
3130
|
const EMBEDDED_CONCURRENCY = 8;
|
|
2120
3131
|
/** Runs `task(0..count-1)` with at most `limit` in flight; results come back in index order. */
|
|
@@ -2835,23 +3846,6 @@ function createToolStyle(deps) {
|
|
|
2835
3846
|
};
|
|
2836
3847
|
}
|
|
2837
3848
|
//#endregion
|
|
2838
|
-
//#region src/model/annotations/suggestions.ts
|
|
2839
|
-
/**
|
|
2840
|
-
* Keeps a suggested redaction as the user's own: `origin` becomes `'user'` and the colour
|
|
2841
|
-
* goes from the confidence colour to the store's default redaction colour, so an accepted
|
|
2842
|
-
* mark looks like any other. The suggestion details stay on it. Returns the updated
|
|
2843
|
-
* annotation, or undefined (changing nothing) when `id` is not a suggested redaction or is
|
|
2844
|
-
* locked.
|
|
2845
|
-
*/
|
|
2846
|
-
function acceptSuggestion(store, id) {
|
|
2847
|
-
const a = store._getProposed(id);
|
|
2848
|
-
if (a?.type !== "redact" || a.origin !== "suggested" || a.locked) return void 0;
|
|
2849
|
-
return store.update(id, {
|
|
2850
|
-
origin: "user",
|
|
2851
|
-
color: store.defaults.color("redact")
|
|
2852
|
-
});
|
|
2853
|
-
}
|
|
2854
|
-
//#endregion
|
|
2855
3849
|
//#region src/model/annotations/threads.ts
|
|
2856
3850
|
function isThreadRoot(a) {
|
|
2857
3851
|
return a.type === "text" || isTextMarkup(a);
|
|
@@ -2968,24 +3962,41 @@ function rectOf(a, b) {
|
|
|
2968
3962
|
height: Math.abs(b.y - a.y)
|
|
2969
3963
|
};
|
|
2970
3964
|
}
|
|
2971
|
-
/**
|
|
3965
|
+
/**
|
|
3966
|
+
* Vertices round a circle, the first at the top centre, clockwise on the page, stretched so their
|
|
3967
|
+
* bounds fill `rect`.
|
|
3968
|
+
*/
|
|
2972
3969
|
function ring(rect, count, radius) {
|
|
2973
|
-
const
|
|
2974
|
-
const cy = rect.y + rect.height / 2;
|
|
2975
|
-
return Array.from({ length: count }, (_, i) => {
|
|
3970
|
+
const unit = Array.from({ length: count }, (_, i) => {
|
|
2976
3971
|
const angle = i * 2 * Math.PI / count;
|
|
2977
3972
|
const r = radius(i);
|
|
2978
3973
|
return {
|
|
2979
|
-
x:
|
|
2980
|
-
y:
|
|
3974
|
+
x: r * Math.sin(angle),
|
|
3975
|
+
y: r * Math.cos(angle)
|
|
2981
3976
|
};
|
|
2982
3977
|
});
|
|
3978
|
+
const xs = unit.map((p) => p.x);
|
|
3979
|
+
const ys = unit.map((p) => p.y);
|
|
3980
|
+
const fit = (v, min, max, start, size) => clean(max > min ? start + (v - min) / (max - min) * size : start + size / 2);
|
|
3981
|
+
const [minX, maxX, minY, maxY] = [
|
|
3982
|
+
Math.min(...xs),
|
|
3983
|
+
Math.max(...xs),
|
|
3984
|
+
Math.min(...ys),
|
|
3985
|
+
Math.max(...ys)
|
|
3986
|
+
];
|
|
3987
|
+
return unit.map((p) => ({
|
|
3988
|
+
x: fit(p.x, minX, maxX, rect.x, rect.width),
|
|
3989
|
+
y: fit(p.y, minY, maxY, rect.y, rect.height)
|
|
3990
|
+
}));
|
|
2983
3991
|
}
|
|
2984
|
-
/**
|
|
3992
|
+
/**
|
|
3993
|
+
* A star's `2 * points` vertices, outer and inner alternating, stretched to fill `rect`, the first
|
|
3994
|
+
* at the top centre.
|
|
3995
|
+
*/
|
|
2985
3996
|
function starVertices(rect, points, innerRatio = .5) {
|
|
2986
3997
|
return ring(rect, Math.max(2, Math.round(points)) * 2, (i) => i % 2 === 0 ? 1 : innerRatio);
|
|
2987
3998
|
}
|
|
2988
|
-
/** A regular polygon's vertices
|
|
3999
|
+
/** A regular polygon's vertices, stretched to fill `rect`, the first at the top centre. */
|
|
2989
4000
|
function polygonVertices(rect, sides) {
|
|
2990
4001
|
return ring(rect, Math.max(3, Math.round(sides)), () => 1);
|
|
2991
4002
|
}
|
|
@@ -3085,16 +4096,13 @@ function shapeInput(kind, page, start, end, opts) {
|
|
|
3085
4096
|
type: "line",
|
|
3086
4097
|
page,
|
|
3087
4098
|
start,
|
|
3088
|
-
end
|
|
3089
|
-
head: "None",
|
|
3090
|
-
tail: "None"
|
|
4099
|
+
end
|
|
3091
4100
|
};
|
|
3092
4101
|
case "arrow": return {
|
|
3093
4102
|
type: "line",
|
|
3094
4103
|
page,
|
|
3095
4104
|
start,
|
|
3096
4105
|
end,
|
|
3097
|
-
tail: "OpenArrow",
|
|
3098
4106
|
intent: "LineArrow"
|
|
3099
4107
|
};
|
|
3100
4108
|
case "rectangle": return {
|
|
@@ -3917,6 +4925,6 @@ function createSignatureLibrary(store) {
|
|
|
3917
4925
|
};
|
|
3918
4926
|
}
|
|
3919
4927
|
//#endregion
|
|
3920
|
-
export {
|
|
4928
|
+
export { importAnnotations as $, derived as $t, styleGroupOf as A, RectSchema as At, isTextMarkup as B, withCalloutBounds as Bt, repliesTo as C, NoteIconSchema as Ct, STYLE_GROUPS as D, PolygonSchema as Dt, withoutReplies as E, PolySchema as Et, typesInGroup as F, TextMarkupSchema as Ft, resizedRect as G, isKnown as Gt, movedEndpoint as H, LINE_ENDINGS as Ht, hasCallout as I, FringeSchema as It, withBox as J, cssVariableDefaults as Jt, scaled as K, pdfNameSet as Kt, hasCornerHandles as L, calloutStroke as Lt, styleGroupsOf as M, ShapeSchema as Mt, supportedProps as N, SquareSchema as Nt, createToolStyle as O, PolylineSchema as Ot, toFields as P, StampSchema as Pt, exportAnnotations as Q, createRecordStore as Qt, hasRectGeometry as R, freeTextBox as Rt, isThreadRoot as S, MaskSchema as St, threadRoot as T, PointSchema as Tt, movedTip as U, NOTE_ICONS as Ut, keepsAspect as V, INTENTS as Vt, resizedBox as W, PDF_NAME$1 as Wt, createAnnotationStore as X, staticDefaults as Xt, openDocument as Y, normalizeColor as Yt, deriveRect as Z, mergeLineRects as Zt, defaultEnds as _, unionRects as _n, IMAGE_DATA_URL as _t, createLocalSignatureStore as a, multiplyMatrix as an, pdfColorToHex as at, isAbandonedNote as b, isInkError as bn, LineSchema as bt, normaliseInk as c, quadsFromFlat as cn, AnnotationSchema as ct, recogniseStroke as d, rectFromPoints as dn, FreeTextSchema as dt, readonly as en, createHistory as et, DEFAULT_CORNER_RADIUS as f, rectFromQuad as fn, FreeTextStyleSchema as ft, constrain as g, translateRect as gn, GLOSS_VERSION$1 as gt, SHAPE_KINDS as h, translateQuad as hn, GLOSS_SCHEMA_ID as ht, createInkTemplate as i, invertMatrix as in, importedId as it, styleGroupOfType as j, RedactSchema as jt, fromAnnotation as k, QuadSchema as kt, placeSignature as l, quadsFromRect as ln, CircleSchema as lt, DEFAULT_SHAPE_SIZE as m, rectsIntersect as mn, GLOSS_MEDIA_TYPE as mt, SIGNATURE_INK_WIDTH as n, applyMatrix as nn, SUPPORTED_SUBTYPES as nt, createMemorySignatureStore as o, pointsFromFlat as on, summarise as ot, DEFAULT_SHAPE_OPTIONS as p, rectFromQuads as pn, GLOSS_FORMAT$1 as pt, translated as q, parsePdfDate as qt, createImageTemplate as r, approxEqual as rn, importEmbeddedPage as rt, createSignatureLibrary as s, quadFromRect as sn, AnnotationDocumentSchema as st, DEFAULT_SIGNATURE_WIDTH as t, writable as tn, isProduction as tt, RECOGNISE_THRESHOLD as u, rectContainsPoint as un, ExportedAnnotationSchema as ut, isPointerOnlyKind as v, Emitter as vn, InkSchema as vt, threadOf as w, NoteSchema as wt, isReply as x, LoupeSchema as xt, shapeInput as y, InkError as yn, LineEndingSchema as yt, isBubble as z, isStandardCallout as zt };
|
|
3921
4929
|
|
|
3922
|
-
//# sourceMappingURL=signatures-
|
|
4930
|
+
//# sourceMappingURL=signatures-D68fuLlt.mjs.map
|