@inkbar/core 0.4.0 → 0.5.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 +18 -7
- package/dist/dom.d.mts +142 -26
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +1277 -898
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +3 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BHYKTwYE.d.mts → signatures-BwaXi7C8.d.mts} +615 -611
- package/dist/signatures-BwaXi7C8.d.mts.map +1 -0
- package/dist/{signatures-DF2VmNVa.mjs → signatures-CMapTKrC.mjs} +1071 -281
- package/dist/signatures-CMapTKrC.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
|
|
@@ -422,20 +423,10 @@ const COLORS = {
|
|
|
422
423
|
mask: "#000000",
|
|
423
424
|
redact: "#C0392B"
|
|
424
425
|
};
|
|
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
426
|
const staticDefaults = {
|
|
435
427
|
color: (type) => COLORS[type],
|
|
436
428
|
fill: () => "#000000",
|
|
437
|
-
opacity: (type) => type === "highlight" ? .4 : 1
|
|
438
|
-
suggestion: (bucket) => SUGGESTION_COLORS[bucket]
|
|
429
|
+
opacity: (type) => type === "highlight" ? .4 : 1
|
|
439
430
|
};
|
|
440
431
|
const VARIABLES = {
|
|
441
432
|
highlight: "--ink-highlight",
|
|
@@ -465,8 +456,7 @@ function cssVariableDefaults(el) {
|
|
|
465
456
|
return {
|
|
466
457
|
color: (type) => read(VARIABLES[type]) ?? staticDefaults.color(type),
|
|
467
458
|
fill: () => read("--ink-redact-fill") ?? staticDefaults.fill(),
|
|
468
|
-
opacity: (type) => staticDefaults.opacity(type)
|
|
469
|
-
suggestion: (bucket) => read(`--ink-suggestion-${bucket}`) ?? staticDefaults.suggestion(bucket)
|
|
459
|
+
opacity: (type) => staticDefaults.opacity(type)
|
|
470
460
|
};
|
|
471
461
|
}
|
|
472
462
|
/** Accepts `#rgb`, `#rrggbb`, `rgb(r, g, b)` and `rgb(r g b)`; returns `#RRGGBB` or undefined. */
|
|
@@ -507,51 +497,12 @@ function parsePdfDate(value, fallback) {
|
|
|
507
497
|
//#endregion
|
|
508
498
|
//#region src/model/annotations/pdf-names.ts
|
|
509
499
|
/**
|
|
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
500
|
* A set of PDF names Inkbar knows that stays open: any other valid PDF name loads, is kept and
|
|
516
501
|
* is written back unchanged. The output type keeps autocomplete for the known values.
|
|
517
502
|
*/
|
|
518
503
|
function pdfNameSet(known) {
|
|
519
504
|
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
505
|
}
|
|
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
506
|
/** True when `v` is one of the values in `list`. */
|
|
556
507
|
function isKnown(list, v) {
|
|
557
508
|
return list.includes(v);
|
|
@@ -562,12 +513,7 @@ function isKnown(list, v) {
|
|
|
562
513
|
* The insets from a free text's `rect` to its text box, in points (PDF `/RD`, XFDF `fringe`,
|
|
563
514
|
* both in this order).
|
|
564
515
|
*/
|
|
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
|
-
});
|
|
516
|
+
const FringeSchema = z.object(Fringe.shape);
|
|
571
517
|
/** An ending's length, and its full width, as a multiple of the stroke width. */
|
|
572
518
|
const ENDING_SCALE = 3;
|
|
573
519
|
/**
|
|
@@ -640,73 +586,48 @@ function withCalloutBounds(box, callout, width, head) {
|
|
|
640
586
|
}
|
|
641
587
|
//#endregion
|
|
642
588
|
//#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
|
-
});
|
|
589
|
+
const PointSchema = z.object(Point.shape);
|
|
590
|
+
const RectSchema = z.object(Rect.shape);
|
|
656
591
|
const QuadSchema = z.object({
|
|
657
592
|
p1: PointSchema,
|
|
658
593
|
p2: PointSchema,
|
|
659
594
|
p3: PointSchema,
|
|
660
595
|
p4: PointSchema
|
|
661
596
|
});
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
id: z.string().min(1),
|
|
665
|
-
/** 0-based page index. */
|
|
666
|
-
page: z.number().int().nonnegative(),
|
|
597
|
+
/** What Inkbar adds to every Gloss type: the fields it always fills in, and its viewer state. */
|
|
598
|
+
const Base = {
|
|
667
599
|
/** Bounding box in PDF user space. */
|
|
668
600
|
rect: RectSchema,
|
|
669
601
|
/** 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 }),
|
|
602
|
+
color: Color,
|
|
603
|
+
opacity: commonFields.opacity.removeDefault(),
|
|
604
|
+
createdAt: commonFields.createdAt.unwrap(),
|
|
605
|
+
modifiedAt: commonFields.modifiedAt.unwrap(),
|
|
680
606
|
/** Created in this session, or imported from the file. Viewer state, never exported. */
|
|
681
607
|
source: z.enum(["session", "pdf"]),
|
|
682
608
|
/** 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({
|
|
609
|
+
locked: z.boolean().optional()
|
|
610
|
+
};
|
|
611
|
+
const Quads = z.array(QuadSchema).min(1);
|
|
612
|
+
const TextMarkupSchema = z.object({
|
|
613
|
+
...Highlight.shape,
|
|
614
|
+
...Base,
|
|
692
615
|
type: z.enum([
|
|
693
616
|
"highlight",
|
|
694
617
|
"underline",
|
|
695
618
|
"strikeout",
|
|
696
619
|
"squiggly"
|
|
697
620
|
]),
|
|
698
|
-
quads:
|
|
621
|
+
quads: Quads
|
|
699
622
|
});
|
|
700
623
|
/** 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
|
-
|
|
624
|
+
const NoteIconSchema = NoteIcon;
|
|
625
|
+
const NoteSchema = z.object({
|
|
626
|
+
...Note.shape,
|
|
627
|
+
...Base
|
|
705
628
|
});
|
|
706
629
|
/** 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();
|
|
630
|
+
const LineEndingSchema = LineEnding;
|
|
710
631
|
/**
|
|
711
632
|
* A key a type refuses outright rather than dropping like other unknown keys: one an older
|
|
712
633
|
* shape used (so its value is not silently lost), or one that means nothing on the type (so a
|
|
@@ -718,20 +639,9 @@ const Refused = z.never().optional();
|
|
|
718
639
|
* `defaultstyle` string. The older `align` is refused; other unknown keys are dropped.
|
|
719
640
|
*/
|
|
720
641
|
const FreeTextStyleSchema = z.object({
|
|
721
|
-
|
|
722
|
-
/** Points. */
|
|
723
|
-
fontSize: z.number().positive().default(12),
|
|
642
|
+
...FreeTextStyle.shape,
|
|
724
643
|
/** 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(),
|
|
644
|
+
color: Color,
|
|
735
645
|
align: Refused
|
|
736
646
|
});
|
|
737
647
|
/**
|
|
@@ -739,88 +649,56 @@ const FreeTextStyleSchema = z.object({
|
|
|
739
649
|
* `defaultStyle`. A box in the older shape (top-level `fontFamily`, `fontSize`, `align`) is
|
|
740
650
|
* refused rather than silently losing its style; other unknown keys are dropped.
|
|
741
651
|
*/
|
|
742
|
-
const FreeTextSchema =
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
contents: z.string(),
|
|
652
|
+
const FreeTextSchema = z.object({
|
|
653
|
+
...FreeText.shape,
|
|
654
|
+
...Base,
|
|
746
655
|
/** 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(),
|
|
656
|
+
color: FreeText.shape.color,
|
|
657
|
+
/** The box's border colour. Absent means the text colour. */
|
|
658
|
+
borderColor: Color.optional(),
|
|
755
659
|
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
660
|
fringe: FringeSchema.optional(),
|
|
761
|
-
/** A callout's leader line: the tip, an optional bend, and where it meets the box (PDF `/CL`). */
|
|
762
661
|
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
662
|
fontFamily: Refused,
|
|
766
663
|
fontSize: Refused,
|
|
767
664
|
align: Refused
|
|
768
665
|
});
|
|
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()
|
|
666
|
+
const SquareSchema = z.object({
|
|
667
|
+
...Square.shape,
|
|
668
|
+
...Base
|
|
779
669
|
});
|
|
780
|
-
const CircleSchema =
|
|
781
|
-
|
|
782
|
-
...
|
|
670
|
+
const CircleSchema = z.object({
|
|
671
|
+
...Circle.shape,
|
|
672
|
+
...Base,
|
|
783
673
|
cornerRadius: Refused
|
|
784
674
|
});
|
|
785
675
|
const ShapeSchema = z.discriminatedUnion("type", [SquareSchema, CircleSchema]);
|
|
786
|
-
const LineSchema =
|
|
787
|
-
|
|
676
|
+
const LineSchema = z.object({
|
|
677
|
+
...Line.shape,
|
|
678
|
+
...Base,
|
|
788
679
|
start: PointSchema,
|
|
789
|
-
end: PointSchema
|
|
790
|
-
width: z.number().nonnegative().default(1),
|
|
791
|
-
dashes: DashesSchema,
|
|
792
|
-
head: LineEndingSchema.optional(),
|
|
793
|
-
tail: LineEndingSchema.optional()
|
|
680
|
+
end: PointSchema
|
|
794
681
|
});
|
|
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()
|
|
682
|
+
const Vertices = z.array(PointSchema).min(2);
|
|
683
|
+
const PolylineSchema = z.object({
|
|
684
|
+
...Polyline.shape,
|
|
685
|
+
...Base,
|
|
686
|
+
vertices: Vertices
|
|
808
687
|
});
|
|
809
|
-
const PolygonSchema =
|
|
810
|
-
|
|
811
|
-
...
|
|
688
|
+
const PolygonSchema = z.object({
|
|
689
|
+
...Polygon.shape,
|
|
690
|
+
...Base,
|
|
691
|
+
vertices: Vertices,
|
|
812
692
|
head: Refused,
|
|
813
693
|
tail: Refused
|
|
814
694
|
});
|
|
815
695
|
const PolySchema = z.discriminatedUnion("type", [PolylineSchema, PolygonSchema]);
|
|
816
|
-
const InkShape =
|
|
817
|
-
|
|
696
|
+
const InkShape = z.object({
|
|
697
|
+
...Ink.shape,
|
|
698
|
+
...Base,
|
|
818
699
|
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
|
|
700
|
+
/** Pen pressure from 0 to 1, one value per point of each stroke. Absent means even pressure. */
|
|
701
|
+
pressure: z.array(z.array(z.number().min(0).max(1))).optional()
|
|
824
702
|
});
|
|
825
703
|
/** Pressure must have one value per point of each stroke. */
|
|
826
704
|
function checkPressure(a, ctx) {
|
|
@@ -832,70 +710,48 @@ function checkPressure(a, ctx) {
|
|
|
832
710
|
});
|
|
833
711
|
}
|
|
834
712
|
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
713
|
/**
|
|
838
714
|
* An image on the page (XFDF `stamp`), drawn to fill `rect`. Image and typed signatures are
|
|
839
715
|
* stamps with `subject: 'Signature'`.
|
|
840
716
|
*/
|
|
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()
|
|
717
|
+
const StampSchema = z.object({
|
|
718
|
+
...Stamp.shape,
|
|
719
|
+
...Base
|
|
855
720
|
});
|
|
856
721
|
/**
|
|
857
|
-
* A loupe (
|
|
858
|
-
* under its own centre, magnified. `color` and
|
|
722
|
+
* A loupe (Inkbar's own type, a circle with `custom.inkbar.type: 'loupe'` in a file): a circle
|
|
723
|
+
* inscribed in `rect` that shows the page content under its own centre, magnified. `color` and
|
|
724
|
+
* `width` draw its rim.
|
|
859
725
|
*/
|
|
860
|
-
const LoupeSchema =
|
|
726
|
+
const LoupeSchema = z.object({
|
|
861
727
|
type: z.literal("loupe"),
|
|
728
|
+
...commonFields,
|
|
862
729
|
/** How many times larger the content under the centre is drawn. Above 1. */
|
|
863
730
|
magnification: z.number().gt(1).default(2),
|
|
864
731
|
/** The rim's width in points. */
|
|
865
|
-
width: z.number().nonnegative().default(2)
|
|
732
|
+
width: z.number().nonnegative().default(2),
|
|
733
|
+
...Base
|
|
866
734
|
});
|
|
867
735
|
/**
|
|
868
|
-
* A mask (
|
|
869
|
-
* `rect`, an ellipse inscribed in it or the
|
|
870
|
-
*
|
|
871
|
-
* the first.
|
|
736
|
+
* A mask (Inkbar's own type, a circle or square with `custom.inkbar.type: 'mask'` in a file):
|
|
737
|
+
* dims everything on the page outside `shape` drawn in `rect`, an ellipse inscribed in it or the
|
|
738
|
+
* rectangle itself. `color` is the dimming colour and `dim` how strongly it covers, from 0 to 1.
|
|
739
|
+
* One per page: adding a second replaces the first.
|
|
872
740
|
*/
|
|
873
|
-
const MaskSchema =
|
|
741
|
+
const MaskSchema = z.object({
|
|
874
742
|
type: z.literal("mask"),
|
|
743
|
+
...commonFields,
|
|
875
744
|
shape: z.enum(["rect", "ellipse"]),
|
|
876
|
-
|
|
877
|
-
|
|
745
|
+
dim: z.number().min(0).max(1).default(.5),
|
|
746
|
+
...Base
|
|
878
747
|
});
|
|
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"),
|
|
748
|
+
const RedactSchema = z.object({
|
|
749
|
+
...Redact.shape,
|
|
750
|
+
...Base,
|
|
892
751
|
/** 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(),
|
|
752
|
+
quads: Quads.optional(),
|
|
897
753
|
/** Colour the redaction will be filled with when applied. */
|
|
898
|
-
interiorColor:
|
|
754
|
+
interiorColor: Color
|
|
899
755
|
});
|
|
900
756
|
const AnnotationSchema = z.discriminatedUnion("type", [
|
|
901
757
|
TextMarkupSchema,
|
|
@@ -934,17 +790,12 @@ const ExportedAnnotationSchema = z.discriminatedUnion("type", [
|
|
|
934
790
|
RedactSchema.omit(VIEWER_STATE)
|
|
935
791
|
]);
|
|
936
792
|
const AnnotationDocumentSchema = z.object({
|
|
937
|
-
format: z.literal(
|
|
938
|
-
version: z.literal(
|
|
793
|
+
format: z.literal(GLOSS_FORMAT),
|
|
794
|
+
version: z.literal(GLOSS_VERSION),
|
|
939
795
|
documentKey: z.string().optional(),
|
|
940
796
|
/** Validated item by item on import so unknown types can be preserved. */
|
|
941
797
|
annotations: z.array(z.unknown())
|
|
942
798
|
});
|
|
943
|
-
function confidenceBucket(confidence) {
|
|
944
|
-
if (confidence < .5) return "low";
|
|
945
|
-
if (confidence < .8) return "medium";
|
|
946
|
-
return "high";
|
|
947
|
-
}
|
|
948
799
|
//#endregion
|
|
949
800
|
//#region src/model/annotations/embedded.ts
|
|
950
801
|
const SUPPORTED_SUBTYPES = /* @__PURE__ */ new Set([
|
|
@@ -982,7 +833,7 @@ function rectFromPdf(r) {
|
|
|
982
833
|
}
|
|
983
834
|
/** A PDF name exactly as PDF.js gives it (names are case-sensitive), or undefined if invalid. */
|
|
984
835
|
function pdfName(raw) {
|
|
985
|
-
return raw !== void 0 && PDF_NAME.test(raw) ? raw : void 0;
|
|
836
|
+
return raw !== void 0 && PDF_NAME$1.test(raw) ? raw : void 0;
|
|
986
837
|
}
|
|
987
838
|
/** `head` and `tail` from a Line's or PolyLine's `/LE`, left out when both are `None`. */
|
|
988
839
|
function lineEndingsOf(raw) {
|
|
@@ -1000,18 +851,43 @@ function lineEndingsOf(raw) {
|
|
|
1000
851
|
function importedId(page, pdfjsId) {
|
|
1001
852
|
return `pdf:${page}:${pdfjsId}`;
|
|
1002
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Imports one page's PDF.js annotation data. `targets` is @internal: the annotations whose
|
|
856
|
+
* missing fields the document reads from the file after `ready`.
|
|
857
|
+
*/
|
|
1003
858
|
function importEmbeddedPage(page, raw, importedAt) {
|
|
1004
859
|
const counts = {};
|
|
1005
860
|
const annotations = [];
|
|
861
|
+
const targets = [];
|
|
1006
862
|
for (const data of raw) {
|
|
1007
863
|
counts[data.subtype] = (counts[data.subtype] ?? 0) + 1;
|
|
1008
864
|
if (!SUPPORTED_SUBTYPES.has(data.subtype) || data.hidden || data.noView) continue;
|
|
1009
865
|
const converted = convert(page, data, importedAt);
|
|
1010
|
-
if (converted)
|
|
866
|
+
if (!converted) continue;
|
|
867
|
+
annotations.push(converted);
|
|
868
|
+
const target = targetOf(converted.id, data);
|
|
869
|
+
if (target) targets.push(target);
|
|
1011
870
|
}
|
|
1012
871
|
return {
|
|
1013
872
|
annotations,
|
|
1014
|
-
counts
|
|
873
|
+
counts,
|
|
874
|
+
targets
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function targetOf(id, d) {
|
|
878
|
+
const rect = Array.from(d.rect);
|
|
879
|
+
if (d.subtype === "Text") return d.name === "NoIcon" ? {
|
|
880
|
+
id,
|
|
881
|
+
subtype: "Text",
|
|
882
|
+
rect
|
|
883
|
+
} : void 0;
|
|
884
|
+
if (d.subtype !== "FreeText") return void 0;
|
|
885
|
+
const daFont = d.defaultAppearanceData?.fontName;
|
|
886
|
+
return {
|
|
887
|
+
id,
|
|
888
|
+
subtype: "FreeText",
|
|
889
|
+
rect,
|
|
890
|
+
...daFont ? { daFont } : {}
|
|
1015
891
|
};
|
|
1016
892
|
}
|
|
1017
893
|
function summarise(perPage) {
|
|
@@ -1152,7 +1028,6 @@ function convert(page, d, importedAt) {
|
|
|
1152
1028
|
candidate = {
|
|
1153
1029
|
...base("redact"),
|
|
1154
1030
|
type: "redact",
|
|
1155
|
-
origin: "user",
|
|
1156
1031
|
interiorColor: pdfColorToHex(d.interiorColor, staticDefaults.fill()),
|
|
1157
1032
|
...quads.length > 0 ? { quads } : {}
|
|
1158
1033
|
};
|
|
@@ -1163,6 +1038,749 @@ function convert(page, d, importedAt) {
|
|
|
1163
1038
|
return parsed.success ? parsed.data : void 0;
|
|
1164
1039
|
}
|
|
1165
1040
|
//#endregion
|
|
1041
|
+
//#region src/pdf/lexer.ts
|
|
1042
|
+
/** Thrown when the bytes are not valid PDF object syntax. */
|
|
1043
|
+
var PdfSyntaxError = class extends Error {
|
|
1044
|
+
constructor(message) {
|
|
1045
|
+
super(message);
|
|
1046
|
+
this.name = "PdfSyntaxError";
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
const isWhite$1 = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
1050
|
+
const isDelimiter = (c) => c === 40 || c === 41 || c === 60 || c === 62 || c === 91 || c === 93 || c === 123 || c === 125 || c === 47 || c === 37;
|
|
1051
|
+
const isRegular = (c) => !isWhite$1(c) && !isDelimiter(c);
|
|
1052
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
1053
|
+
const latin1$1 = (bytes, from, to) => {
|
|
1054
|
+
let out = "";
|
|
1055
|
+
for (let i = from; i < to; i++) out += String.fromCharCode(bytes[i]);
|
|
1056
|
+
return out;
|
|
1057
|
+
};
|
|
1058
|
+
function skipSpace(bytes, offset) {
|
|
1059
|
+
let i = offset;
|
|
1060
|
+
while (i < bytes.length) {
|
|
1061
|
+
const c = bytes[i];
|
|
1062
|
+
if (isWhite$1(c)) i++;
|
|
1063
|
+
else if (c === 37) while (i < bytes.length && bytes[i] !== 10 && bytes[i] !== 13) i++;
|
|
1064
|
+
else break;
|
|
1065
|
+
}
|
|
1066
|
+
return i;
|
|
1067
|
+
}
|
|
1068
|
+
function tokenEnd(bytes, offset) {
|
|
1069
|
+
let i = offset;
|
|
1070
|
+
while (i < bytes.length && isRegular(bytes[i])) i++;
|
|
1071
|
+
return i;
|
|
1072
|
+
}
|
|
1073
|
+
const hexValue = (c) => {
|
|
1074
|
+
if (c === void 0) return -1;
|
|
1075
|
+
if (c >= 48 && c <= 57) return c - 48;
|
|
1076
|
+
if (c >= 65 && c <= 70) return c - 65 + 10;
|
|
1077
|
+
if (c >= 97 && c <= 102) return c - 97 + 10;
|
|
1078
|
+
return -1;
|
|
1079
|
+
};
|
|
1080
|
+
function parseName(bytes, offset) {
|
|
1081
|
+
const end = tokenEnd(bytes, offset + 1);
|
|
1082
|
+
let name = "";
|
|
1083
|
+
for (let i = offset + 1; i < end; i++) {
|
|
1084
|
+
const c = bytes[i];
|
|
1085
|
+
const hi = c === 35 ? hexValue(bytes[i + 1]) : -1;
|
|
1086
|
+
const lo = hi >= 0 ? hexValue(bytes[i + 2]) : -1;
|
|
1087
|
+
if (hi >= 0 && lo >= 0 && i + 2 < end) {
|
|
1088
|
+
name += String.fromCharCode(hi * 16 + lo);
|
|
1089
|
+
i += 2;
|
|
1090
|
+
} else name += String.fromCharCode(c);
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
value: {
|
|
1094
|
+
kind: "name",
|
|
1095
|
+
name
|
|
1096
|
+
},
|
|
1097
|
+
end
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function skipLiteralString(bytes, offset) {
|
|
1101
|
+
let depth = 0;
|
|
1102
|
+
for (let i = offset; i < bytes.length; i++) {
|
|
1103
|
+
const c = bytes[i];
|
|
1104
|
+
if (c === 92) i++;
|
|
1105
|
+
else if (c === 40) depth++;
|
|
1106
|
+
else if (c === 41 && --depth === 0) return i + 1;
|
|
1107
|
+
}
|
|
1108
|
+
throw new PdfSyntaxError("Unterminated string");
|
|
1109
|
+
}
|
|
1110
|
+
/** After a non-negative integer ending at `end`, looks for `gen R`. */
|
|
1111
|
+
function tryReference(bytes, num, end) {
|
|
1112
|
+
let i = end;
|
|
1113
|
+
if (i >= bytes.length || !isWhite$1(bytes[i])) return void 0;
|
|
1114
|
+
i = skipSpace(bytes, i);
|
|
1115
|
+
const genStart = i;
|
|
1116
|
+
while (i < bytes.length && isDigit(bytes[i])) i++;
|
|
1117
|
+
if (i === genStart || i >= bytes.length || !isWhite$1(bytes[i])) return void 0;
|
|
1118
|
+
const gen = Number(latin1$1(bytes, genStart, i));
|
|
1119
|
+
i = skipSpace(bytes, i);
|
|
1120
|
+
if (bytes[i] !== 82 || i + 1 < bytes.length && isRegular(bytes[i + 1])) return;
|
|
1121
|
+
return {
|
|
1122
|
+
value: {
|
|
1123
|
+
kind: "ref",
|
|
1124
|
+
num,
|
|
1125
|
+
gen
|
|
1126
|
+
},
|
|
1127
|
+
end: i + 1
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function parseNumberOrKeyword(bytes, offset) {
|
|
1131
|
+
const end = tokenEnd(bytes, offset);
|
|
1132
|
+
const token = latin1$1(bytes, offset, end);
|
|
1133
|
+
if (token === "true") return {
|
|
1134
|
+
value: true,
|
|
1135
|
+
end
|
|
1136
|
+
};
|
|
1137
|
+
if (token === "false") return {
|
|
1138
|
+
value: false,
|
|
1139
|
+
end
|
|
1140
|
+
};
|
|
1141
|
+
if (token === "null") return {
|
|
1142
|
+
value: null,
|
|
1143
|
+
end
|
|
1144
|
+
};
|
|
1145
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(token)) {
|
|
1146
|
+
const n = Number(token);
|
|
1147
|
+
if (/^\d+$/.test(token)) {
|
|
1148
|
+
const ref = tryReference(bytes, n, end);
|
|
1149
|
+
if (ref) return ref;
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
value: n,
|
|
1153
|
+
end
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
throw new PdfSyntaxError(`Unexpected token "${token}" at ${offset}`);
|
|
1157
|
+
}
|
|
1158
|
+
/** Parses one value at `offset`, skipping whitespace and comments first. */
|
|
1159
|
+
function parseValue(bytes, offset) {
|
|
1160
|
+
const i = skipSpace(bytes, offset);
|
|
1161
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unexpected end of data");
|
|
1162
|
+
const c = bytes[i];
|
|
1163
|
+
if (c === 47) return parseName(bytes, i);
|
|
1164
|
+
if (c === 40) return {
|
|
1165
|
+
value: { kind: "string" },
|
|
1166
|
+
end: skipLiteralString(bytes, i)
|
|
1167
|
+
};
|
|
1168
|
+
if (c === 60 && bytes[i + 1] === 60) return parseDict(bytes, i + 2);
|
|
1169
|
+
if (c === 60) {
|
|
1170
|
+
let j = i + 1;
|
|
1171
|
+
while (j < bytes.length && bytes[j] !== 62) j++;
|
|
1172
|
+
if (j >= bytes.length) throw new PdfSyntaxError("Unterminated hex string");
|
|
1173
|
+
return {
|
|
1174
|
+
value: { kind: "string" },
|
|
1175
|
+
end: j + 1
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
if (c === 91) return parseArray(bytes, i + 1);
|
|
1179
|
+
if (isRegular(c)) return parseNumberOrKeyword(bytes, i);
|
|
1180
|
+
throw new PdfSyntaxError(`Unexpected "${String.fromCharCode(c)}" at ${i}`);
|
|
1181
|
+
}
|
|
1182
|
+
function parseArray(bytes, offset) {
|
|
1183
|
+
const items = [];
|
|
1184
|
+
let i = offset;
|
|
1185
|
+
for (;;) {
|
|
1186
|
+
i = skipSpace(bytes, i);
|
|
1187
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unterminated array");
|
|
1188
|
+
if (bytes[i] === 93) return {
|
|
1189
|
+
value: items,
|
|
1190
|
+
end: i + 1
|
|
1191
|
+
};
|
|
1192
|
+
const item = parseValue(bytes, i);
|
|
1193
|
+
items.push(item.value);
|
|
1194
|
+
i = item.end;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function parseDict(bytes, offset) {
|
|
1198
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1199
|
+
let i = offset;
|
|
1200
|
+
for (;;) {
|
|
1201
|
+
i = skipSpace(bytes, i);
|
|
1202
|
+
if (i >= bytes.length) throw new PdfSyntaxError("Unterminated dictionary");
|
|
1203
|
+
if (bytes[i] === 62 && bytes[i + 1] === 62) return {
|
|
1204
|
+
value: {
|
|
1205
|
+
kind: "dict",
|
|
1206
|
+
entries
|
|
1207
|
+
},
|
|
1208
|
+
end: i + 2
|
|
1209
|
+
};
|
|
1210
|
+
if (bytes[i] !== 47) throw new PdfSyntaxError(`Expected a name key at ${i}`);
|
|
1211
|
+
const key = parseName(bytes, i);
|
|
1212
|
+
const val = parseValue(bytes, key.end);
|
|
1213
|
+
entries.set(key.value.name, val.value);
|
|
1214
|
+
i = val.end;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function readInteger(bytes, offset) {
|
|
1218
|
+
const i = skipSpace(bytes, offset);
|
|
1219
|
+
let end = i;
|
|
1220
|
+
while (end < bytes.length && isDigit(bytes[end])) end++;
|
|
1221
|
+
if (end === i) throw new PdfSyntaxError(`Expected an integer at ${i}`);
|
|
1222
|
+
return {
|
|
1223
|
+
n: Number(latin1$1(bytes, i, end)),
|
|
1224
|
+
end
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
/** Parses `num gen obj <value> [stream]` at `offset`. `streamStart` is the first data byte after `stream` and its EOL. */
|
|
1228
|
+
function parseIndirect(bytes, offset) {
|
|
1229
|
+
const num = readInteger(bytes, offset);
|
|
1230
|
+
const gen = readInteger(bytes, num.end);
|
|
1231
|
+
const kw = skipSpace(bytes, gen.end);
|
|
1232
|
+
if (latin1$1(bytes, kw, tokenEnd(bytes, kw)) !== "obj") throw new PdfSyntaxError(`Expected "obj" at ${kw}`);
|
|
1233
|
+
const { value, end } = parseValue(bytes, kw + 3);
|
|
1234
|
+
const result = {
|
|
1235
|
+
num: num.n,
|
|
1236
|
+
gen: gen.n,
|
|
1237
|
+
value
|
|
1238
|
+
};
|
|
1239
|
+
const next = skipSpace(bytes, end);
|
|
1240
|
+
if (latin1$1(bytes, next, next + 6) === "stream" && !isRegular(bytes[next + 6] ?? 32)) {
|
|
1241
|
+
let start = next + 6;
|
|
1242
|
+
if (bytes[start] === 13 && bytes[start + 1] === 10) start += 2;
|
|
1243
|
+
else if (bytes[start] === 10 || bytes[start] === 13) start += 1;
|
|
1244
|
+
result.streamStart = start;
|
|
1245
|
+
}
|
|
1246
|
+
return result;
|
|
1247
|
+
}
|
|
1248
|
+
/** Reads a dictionary entry. */
|
|
1249
|
+
const dictGet = (d, key) => d.entries.get(key);
|
|
1250
|
+
/** True for a name object, optionally one with the given spelling. */
|
|
1251
|
+
const isName = (v, name) => typeof v === "object" && v !== null && v.kind === "name" && (name === void 0 || v.name === name);
|
|
1252
|
+
/** True for a dictionary object. */
|
|
1253
|
+
const isDict = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.kind === "dict";
|
|
1254
|
+
/** True for an indirect reference. */
|
|
1255
|
+
const isRef = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && v.kind === "ref";
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/pdf/annotation-fields.ts
|
|
1258
|
+
/** `12R` → {num:12,gen:0}, `12R3` → {num:12,gen:3}; anything else undefined. */
|
|
1259
|
+
function refOfPdfjsId(id) {
|
|
1260
|
+
const m = /^(\d+)R(\d+)?$/.exec(id);
|
|
1261
|
+
if (!m) return void 0;
|
|
1262
|
+
return {
|
|
1263
|
+
kind: "ref",
|
|
1264
|
+
num: Number(m[1]),
|
|
1265
|
+
gen: m[2] === void 0 ? 0 : Number(m[2])
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const FAMILIES = [
|
|
1269
|
+
[/^(helvetica|helv)/i, "Helvetica"],
|
|
1270
|
+
[/^(times|tiro)/i, "Times"],
|
|
1271
|
+
[/^cour(ier|$)/i, "Courier"]
|
|
1272
|
+
];
|
|
1273
|
+
/**
|
|
1274
|
+
* BaseFont (or bare resource name) → Inkbar font fields; undefined for Symbol and ZapfDingbats.
|
|
1275
|
+
* A family matches by prefix, so `HelveticaNeue` reads as Helvetica and `CourierNewPS` as
|
|
1276
|
+
* Courier. Any other name keeps its own family, minus a trailing `MT` or `PSMT`.
|
|
1277
|
+
* With `knownOnly`, a name outside Helvetica, Times and Courier gives undefined: a bare
|
|
1278
|
+
* resource id such as `F1` is not a family.
|
|
1279
|
+
*/
|
|
1280
|
+
function fontFromBaseName(name, knownOnly = false) {
|
|
1281
|
+
const bare = name.replace(/^[A-Z]{6}\+/, "");
|
|
1282
|
+
if (/^(symbol|zapfdingbats|zadb)/i.test(bare)) return void 0;
|
|
1283
|
+
const cut = bare.search(/[-,]/);
|
|
1284
|
+
const base = cut < 0 ? bare : bare.slice(0, cut);
|
|
1285
|
+
const style = cut < 0 ? "" : bare.slice(cut + 1);
|
|
1286
|
+
const known = FAMILIES.find(([re]) => re.test(base));
|
|
1287
|
+
if (knownOnly && !known) return void 0;
|
|
1288
|
+
const fontFamily = known ? known[1] : base.replace(/(PS)?MT$/, "");
|
|
1289
|
+
if (!fontFamily) return void 0;
|
|
1290
|
+
const font = { fontFamily };
|
|
1291
|
+
if (/bold|black|heavy/i.test(style)) font.fontWeight = "bold";
|
|
1292
|
+
if (/italic|oblique/i.test(style)) font.fontStyle = "italic";
|
|
1293
|
+
return font;
|
|
1294
|
+
}
|
|
1295
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1296
|
+
async function numbers$1(objs, v, counts) {
|
|
1297
|
+
const arr = await objs.resolve(v);
|
|
1298
|
+
if (!Array.isArray(arr) || !counts.includes(arr.length)) return void 0;
|
|
1299
|
+
const out = await Promise.all(arr.map((n) => objs.resolve(n)));
|
|
1300
|
+
return out.every(finite) ? out : void 0;
|
|
1301
|
+
}
|
|
1302
|
+
async function dictOf(objs, v) {
|
|
1303
|
+
const d = await objs.resolve(v);
|
|
1304
|
+
return isDict(d) ? d : void 0;
|
|
1305
|
+
}
|
|
1306
|
+
/** The font's /BaseFont, looked up in a /DR resource dictionary. */
|
|
1307
|
+
async function baseFontIn(objs, dr, resource) {
|
|
1308
|
+
const res = await dictOf(objs, dr);
|
|
1309
|
+
const fonts = res && await dictOf(objs, dictGet(res, "Font"));
|
|
1310
|
+
const font = fonts && await dictOf(objs, dictGet(fonts, resource));
|
|
1311
|
+
const base = font && await objs.resolve(dictGet(font, "BaseFont"));
|
|
1312
|
+
return isName(base) ? base.name : void 0;
|
|
1313
|
+
}
|
|
1314
|
+
async function readFont(objs, annot, daFont) {
|
|
1315
|
+
const root = await dictOf(objs, dictGet(objs.trailer, "Root"));
|
|
1316
|
+
const acroForm = root && await dictOf(objs, dictGet(root, "AcroForm"));
|
|
1317
|
+
const sources = [dictGet(annot, "DR"), acroForm && dictGet(acroForm, "DR")];
|
|
1318
|
+
for (const dr of sources) {
|
|
1319
|
+
const base = await baseFontIn(objs, dr, daFont);
|
|
1320
|
+
if (base !== void 0) return fontFromBaseName(base);
|
|
1321
|
+
}
|
|
1322
|
+
return fontFromBaseName(daFont, true);
|
|
1323
|
+
}
|
|
1324
|
+
async function readHead(objs, le) {
|
|
1325
|
+
let v = await objs.resolve(le);
|
|
1326
|
+
if (Array.isArray(v)) v = await objs.resolve(v[0]);
|
|
1327
|
+
return isName(v) && PDF_NAME$1.test(v.name) ? v.name : void 0;
|
|
1328
|
+
}
|
|
1329
|
+
/** `rect` is the annotation's [x1 y1 x2 y2], used to reject a fringe that leaves no box. */
|
|
1330
|
+
async function readFreeTextFields(objs, ref, rect, daFont) {
|
|
1331
|
+
const fields = {};
|
|
1332
|
+
try {
|
|
1333
|
+
const annot = await objs.get(ref);
|
|
1334
|
+
if (!isDict(annot)) return fields;
|
|
1335
|
+
const cl = await numbers$1(objs, dictGet(annot, "CL"), [4, 6]);
|
|
1336
|
+
if (cl) {
|
|
1337
|
+
fields.callout = [];
|
|
1338
|
+
for (let i = 0; i < cl.length; i += 2) fields.callout.push({
|
|
1339
|
+
x: cl[i],
|
|
1340
|
+
y: cl[i + 1]
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const rd = await numbers$1(objs, dictGet(annot, "RD"), [4]);
|
|
1344
|
+
if (rd && rect.length === 4 && rd.every((n) => n >= 0)) {
|
|
1345
|
+
const [left, top, right, bottom] = rd;
|
|
1346
|
+
const width = Math.abs(rect[2] - rect[0]);
|
|
1347
|
+
const height = Math.abs(rect[3] - rect[1]);
|
|
1348
|
+
if (left + right < width && top + bottom < height) fields.fringe = {
|
|
1349
|
+
left,
|
|
1350
|
+
top,
|
|
1351
|
+
right,
|
|
1352
|
+
bottom
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
const head = await readHead(objs, dictGet(annot, "LE"));
|
|
1356
|
+
if (head !== void 0) fields.head = head;
|
|
1357
|
+
if (daFont !== void 0) {
|
|
1358
|
+
const font = await readFont(objs, annot, daFont);
|
|
1359
|
+
if (font) fields.font = font;
|
|
1360
|
+
}
|
|
1361
|
+
} catch {}
|
|
1362
|
+
return fields;
|
|
1363
|
+
}
|
|
1364
|
+
async function readTextIcon(objs, ref) {
|
|
1365
|
+
try {
|
|
1366
|
+
const annot = await objs.get(ref);
|
|
1367
|
+
if (!isDict(annot)) return void 0;
|
|
1368
|
+
const name = await objs.resolve(dictGet(annot, "Name"));
|
|
1369
|
+
return isName(name) && PDF_NAME$1.test(name.name) ? name.name : void 0;
|
|
1370
|
+
} catch {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/pdf/stream.ts
|
|
1376
|
+
const ENDSTREAM = Uint8Array.from("endstream", (c) => c.charCodeAt(0));
|
|
1377
|
+
function matchesAt(bytes, at, pattern) {
|
|
1378
|
+
if (at < 0 || at + pattern.length > bytes.length) return false;
|
|
1379
|
+
for (let i = 0; i < pattern.length; i++) if (bytes[at + i] !== pattern[i]) return false;
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
function findEndstream(bytes, from) {
|
|
1383
|
+
for (let i = from; i + ENDSTREAM.length <= bytes.length; i++) if (matchesAt(bytes, i, ENDSTREAM)) return i;
|
|
1384
|
+
return bytes.length;
|
|
1385
|
+
}
|
|
1386
|
+
/** Raw stream bytes: trusts `length` when `endstream` follows it, otherwise scans. */
|
|
1387
|
+
function rawData(bytes, start, length) {
|
|
1388
|
+
if (length !== void 0 && Number.isInteger(length) && length >= 0) {
|
|
1389
|
+
let i = start + length;
|
|
1390
|
+
while (i < bytes.length && (bytes[i] === 10 || bytes[i] === 13 || bytes[i] === 32)) i++;
|
|
1391
|
+
if (matchesAt(bytes, i, ENDSTREAM)) return bytes.subarray(start, start + length);
|
|
1392
|
+
}
|
|
1393
|
+
let end = findEndstream(bytes, start);
|
|
1394
|
+
if (end > start && bytes[end - 1] === 10) end--;
|
|
1395
|
+
if (end > start && bytes[end - 1] === 13) end--;
|
|
1396
|
+
return bytes.subarray(start, end);
|
|
1397
|
+
}
|
|
1398
|
+
/** The single filter parameter dictionary, if any. Anything else that is defined is an error. */
|
|
1399
|
+
function decodeParms(dict) {
|
|
1400
|
+
const parms = dictGet(dict, "DecodeParms") ?? dictGet(dict, "DP");
|
|
1401
|
+
const first = Array.isArray(parms) ? parms[0] : parms;
|
|
1402
|
+
if (first === void 0 || first === null) return void 0;
|
|
1403
|
+
if (!isDict(first)) throw new PdfSyntaxError("Unsupported /DecodeParms");
|
|
1404
|
+
return first;
|
|
1405
|
+
}
|
|
1406
|
+
function numberParam(parms, key, fallback) {
|
|
1407
|
+
const v = parms ? dictGet(parms, key) : void 0;
|
|
1408
|
+
return typeof v === "number" ? v : fallback;
|
|
1409
|
+
}
|
|
1410
|
+
async function inflate(data) {
|
|
1411
|
+
const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate"));
|
|
1412
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
1413
|
+
}
|
|
1414
|
+
function paeth(a, b, c) {
|
|
1415
|
+
const p = a + b - c;
|
|
1416
|
+
const pa = Math.abs(p - a);
|
|
1417
|
+
const pb = Math.abs(p - b);
|
|
1418
|
+
const pc = Math.abs(p - c);
|
|
1419
|
+
if (pa <= pb && pa <= pc) return a;
|
|
1420
|
+
return pb <= pc ? b : c;
|
|
1421
|
+
}
|
|
1422
|
+
function undoPngPredictor(data, columns, colors, bits) {
|
|
1423
|
+
const bpp = Math.max(1, Math.ceil(colors * bits / 8));
|
|
1424
|
+
const rowBytes = Math.ceil(columns * colors * bits / 8);
|
|
1425
|
+
const rows = Math.floor(data.length / (rowBytes + 1));
|
|
1426
|
+
const out = new Uint8Array(rows * rowBytes);
|
|
1427
|
+
for (let r = 0; r < rows; r++) {
|
|
1428
|
+
const type = data[r * (rowBytes + 1)];
|
|
1429
|
+
const src = r * (rowBytes + 1) + 1;
|
|
1430
|
+
const dst = r * rowBytes;
|
|
1431
|
+
for (let x = 0; x < rowBytes; x++) {
|
|
1432
|
+
const raw = data[src + x];
|
|
1433
|
+
const left = x >= bpp ? out[dst + x - bpp] : 0;
|
|
1434
|
+
const up = r > 0 ? out[dst - rowBytes + x] : 0;
|
|
1435
|
+
const upLeft = r > 0 && x >= bpp ? out[dst - rowBytes + x - bpp] : 0;
|
|
1436
|
+
let value;
|
|
1437
|
+
switch (type) {
|
|
1438
|
+
case 0:
|
|
1439
|
+
value = raw;
|
|
1440
|
+
break;
|
|
1441
|
+
case 1:
|
|
1442
|
+
value = raw + left;
|
|
1443
|
+
break;
|
|
1444
|
+
case 2:
|
|
1445
|
+
value = raw + up;
|
|
1446
|
+
break;
|
|
1447
|
+
case 3:
|
|
1448
|
+
value = raw + (left + up >> 1);
|
|
1449
|
+
break;
|
|
1450
|
+
case 4:
|
|
1451
|
+
value = raw + paeth(left, up, upLeft);
|
|
1452
|
+
break;
|
|
1453
|
+
default: throw new PdfSyntaxError(`Unknown PNG filter type ${type}`);
|
|
1454
|
+
}
|
|
1455
|
+
out[dst + x] = value & 255;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return out;
|
|
1459
|
+
}
|
|
1460
|
+
/** The decoded data of the stream whose dictionary is `dict` and whose data starts at `start`. `length` is `/Length`, already resolved. */
|
|
1461
|
+
async function decodeStream(bytes, dict, start, length) {
|
|
1462
|
+
const filter = dictGet(dict, "Filter") ?? dictGet(dict, "F");
|
|
1463
|
+
const filters = Array.isArray(filter) ? filter : filter === void 0 ? [] : [filter];
|
|
1464
|
+
if (filters.length === 0) return rawData(bytes, start, length);
|
|
1465
|
+
if (filters.length > 1 || !isName(filters[0], "FlateDecode")) throw new PdfSyntaxError("Unsupported stream filter");
|
|
1466
|
+
const inflated = await inflate(rawData(bytes, start, length));
|
|
1467
|
+
const parms = decodeParms(dict);
|
|
1468
|
+
const predictor = numberParam(parms, "Predictor", 1);
|
|
1469
|
+
if (predictor === 1) return inflated;
|
|
1470
|
+
if (predictor < 10) throw new PdfSyntaxError(`Unsupported predictor ${predictor}`);
|
|
1471
|
+
return undoPngPredictor(inflated, numberParam(parms, "Columns", 1), numberParam(parms, "Colors", 1), numberParam(parms, "BitsPerComponent", 8));
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/pdf/xref.ts
|
|
1475
|
+
const isWhite = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
1476
|
+
const latin1 = (bytes, from, to) => {
|
|
1477
|
+
let out = "";
|
|
1478
|
+
for (let i = from; i < to; i++) out += String.fromCharCode(bytes[i]);
|
|
1479
|
+
return out;
|
|
1480
|
+
};
|
|
1481
|
+
function readToken(bytes, offset) {
|
|
1482
|
+
let i = offset;
|
|
1483
|
+
while (i < bytes.length && isWhite(bytes[i])) i++;
|
|
1484
|
+
const start = i;
|
|
1485
|
+
while (i < bytes.length && !isWhite(bytes[i])) i++;
|
|
1486
|
+
return {
|
|
1487
|
+
token: latin1(bytes, start, i),
|
|
1488
|
+
end: i
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function readInt(bytes, offset) {
|
|
1492
|
+
const { token, end } = readToken(bytes, offset);
|
|
1493
|
+
if (!/^\d+$/.test(token)) throw new Error(`Expected an integer at ${offset}`);
|
|
1494
|
+
return {
|
|
1495
|
+
n: Number(token),
|
|
1496
|
+
end
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function findStartxref(bytes) {
|
|
1500
|
+
const tail = latin1(bytes, Math.max(0, bytes.length - 1024), bytes.length);
|
|
1501
|
+
const at = tail.lastIndexOf("startxref");
|
|
1502
|
+
if (at < 0) throw new Error("No startxref");
|
|
1503
|
+
return readInt(bytes, bytes.length - tail.length + at + 9).n;
|
|
1504
|
+
}
|
|
1505
|
+
function readTable(bytes, offset) {
|
|
1506
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1507
|
+
let i = readToken(bytes, offset).end;
|
|
1508
|
+
for (;;) {
|
|
1509
|
+
const head = readToken(bytes, i);
|
|
1510
|
+
if (head.token.startsWith("trailer")) {
|
|
1511
|
+
const trailer = parseValue(bytes, head.end - (head.token.length - 7)).value;
|
|
1512
|
+
if (!isDict(trailer)) throw new Error("Trailer is not a dictionary");
|
|
1513
|
+
return {
|
|
1514
|
+
entries,
|
|
1515
|
+
trailer
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
const first = readInt(bytes, i);
|
|
1519
|
+
const count = readInt(bytes, first.end);
|
|
1520
|
+
i = count.end;
|
|
1521
|
+
for (let k = 0; k < count.n; k++) {
|
|
1522
|
+
const off = readInt(bytes, i);
|
|
1523
|
+
const gen = readInt(bytes, off.end);
|
|
1524
|
+
const kind = readToken(bytes, gen.end);
|
|
1525
|
+
i = kind.end;
|
|
1526
|
+
if (kind.token === "n") entries.set(first.n + k, {
|
|
1527
|
+
type: "offset",
|
|
1528
|
+
offset: off.n,
|
|
1529
|
+
gen: gen.n
|
|
1530
|
+
});
|
|
1531
|
+
else if (kind.token === "f") entries.set(first.n + k, { type: "free" });
|
|
1532
|
+
else throw new Error(`Bad xref entry type "${kind.token}"`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const numbers = (v) => Array.isArray(v) && v.every((x) => typeof x === "number" && x >= 0) ? v : void 0;
|
|
1537
|
+
async function readStream(bytes, offset) {
|
|
1538
|
+
const obj = parseIndirect(bytes, offset);
|
|
1539
|
+
const dict = obj.value;
|
|
1540
|
+
if (!isDict(dict) || obj.streamStart === void 0) throw new Error("Xref stream expected");
|
|
1541
|
+
const length = dict.entries.get("Length");
|
|
1542
|
+
const data = await decodeStream(bytes, dict, obj.streamStart, typeof length === "number" ? length : void 0);
|
|
1543
|
+
const w = numbers(dict.entries.get("W"));
|
|
1544
|
+
if (!w || w.length !== 3 || w.some((x) => x > 8)) throw new Error("Bad /W");
|
|
1545
|
+
const size = dict.entries.get("Size");
|
|
1546
|
+
const index = numbers(dict.entries.get("Index")) ?? [0, typeof size === "number" ? size : 0];
|
|
1547
|
+
const [w0, w1, w2] = w;
|
|
1548
|
+
const rowLength = w0 + w1 + w2;
|
|
1549
|
+
if (rowLength === 0) throw new Error("Empty xref rows");
|
|
1550
|
+
const field = (at, width) => {
|
|
1551
|
+
let n = 0;
|
|
1552
|
+
for (let k = 0; k < width; k++) n = n * 256 + data[at + k];
|
|
1553
|
+
return n;
|
|
1554
|
+
};
|
|
1555
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1556
|
+
let row = 0;
|
|
1557
|
+
for (let s = 0; s + 1 < index.length; s += 2) for (let k = 0; k < index[s + 1]; k++, row++) {
|
|
1558
|
+
const at = row * rowLength;
|
|
1559
|
+
if (at + rowLength > data.length) return {
|
|
1560
|
+
entries,
|
|
1561
|
+
trailer: dict
|
|
1562
|
+
};
|
|
1563
|
+
const type = w0 === 0 ? 1 : field(at, w0);
|
|
1564
|
+
const a = field(at + w0, w1);
|
|
1565
|
+
const b = field(at + w0 + w1, w2);
|
|
1566
|
+
const num = index[s] + k;
|
|
1567
|
+
if (type === 0) entries.set(num, { type: "free" });
|
|
1568
|
+
else if (type === 1) entries.set(num, {
|
|
1569
|
+
type: "offset",
|
|
1570
|
+
offset: a,
|
|
1571
|
+
gen: b
|
|
1572
|
+
});
|
|
1573
|
+
else if (type === 2) entries.set(num, {
|
|
1574
|
+
type: "compressed",
|
|
1575
|
+
stream: a,
|
|
1576
|
+
index: b
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
entries,
|
|
1581
|
+
trailer: dict
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
async function readSection(bytes, offset) {
|
|
1585
|
+
if (!Number.isInteger(offset) || offset < 0 || offset >= bytes.length) throw new Error("Xref offset out of range");
|
|
1586
|
+
return readToken(bytes, offset).token === "xref" ? readTable(bytes, offset) : readStream(bytes, offset);
|
|
1587
|
+
}
|
|
1588
|
+
/** Newest entry wins along the /Prev chain (and a hybrid file's /XRefStm). Undefined when startxref or a section cannot be read. */
|
|
1589
|
+
async function readXref(bytes) {
|
|
1590
|
+
try {
|
|
1591
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1592
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1593
|
+
let trailer;
|
|
1594
|
+
let next = findStartxref(bytes);
|
|
1595
|
+
while (next !== void 0 && !visited.has(next)) {
|
|
1596
|
+
visited.add(next);
|
|
1597
|
+
const section = await readSection(bytes, next);
|
|
1598
|
+
trailer ??= section.trailer;
|
|
1599
|
+
const hybrid = section.trailer.entries.get("XRefStm");
|
|
1600
|
+
if (typeof hybrid === "number" && !visited.has(hybrid)) {
|
|
1601
|
+
visited.add(hybrid);
|
|
1602
|
+
const extra = await readSection(bytes, hybrid);
|
|
1603
|
+
for (const [num, e] of extra.entries) if (!entries.has(num)) entries.set(num, e);
|
|
1604
|
+
}
|
|
1605
|
+
for (const [num, e] of section.entries) if (!entries.has(num)) entries.set(num, e);
|
|
1606
|
+
const prev = section.trailer.entries.get("Prev");
|
|
1607
|
+
next = typeof prev === "number" ? prev : void 0;
|
|
1608
|
+
}
|
|
1609
|
+
return trailer ? {
|
|
1610
|
+
entries,
|
|
1611
|
+
trailer
|
|
1612
|
+
} : void 0;
|
|
1613
|
+
} catch {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/pdf/objects.ts
|
|
1619
|
+
var Reader = class {
|
|
1620
|
+
bytes;
|
|
1621
|
+
xref;
|
|
1622
|
+
trailer;
|
|
1623
|
+
objectStreams = /* @__PURE__ */ new Map();
|
|
1624
|
+
constructor(bytes, xref) {
|
|
1625
|
+
this.bytes = bytes;
|
|
1626
|
+
this.xref = xref;
|
|
1627
|
+
this.trailer = xref.trailer;
|
|
1628
|
+
}
|
|
1629
|
+
async get(ref) {
|
|
1630
|
+
try {
|
|
1631
|
+
return await this.load(ref, true);
|
|
1632
|
+
} catch {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
async resolve(v) {
|
|
1637
|
+
return isRef(v) ? this.get(v) : v;
|
|
1638
|
+
}
|
|
1639
|
+
/** `followLength` is false while resolving a /Length, so a self-referencing length cannot recurse. */
|
|
1640
|
+
async load(ref, followLength) {
|
|
1641
|
+
const entry = this.xref.entries.get(ref.num);
|
|
1642
|
+
if (!entry || entry.type === "free") return void 0;
|
|
1643
|
+
if (entry.type === "offset") {
|
|
1644
|
+
if (entry.gen !== ref.gen) return void 0;
|
|
1645
|
+
const obj = parseIndirect(this.bytes, entry.offset);
|
|
1646
|
+
return obj.num === ref.num && obj.gen === ref.gen ? obj.value : void 0;
|
|
1647
|
+
}
|
|
1648
|
+
if (ref.gen !== 0) return void 0;
|
|
1649
|
+
const container = await this.objectStream(entry.stream, followLength);
|
|
1650
|
+
if (!container) return void 0;
|
|
1651
|
+
let pair = container.pairs[entry.index];
|
|
1652
|
+
if (pair?.num !== ref.num) pair = container.pairs.find((p) => p.num === ref.num);
|
|
1653
|
+
if (!pair) return void 0;
|
|
1654
|
+
return parseValue(container.decoded, container.first + pair.offset).value;
|
|
1655
|
+
}
|
|
1656
|
+
objectStream(num, followLength) {
|
|
1657
|
+
let cached = this.objectStreams.get(num);
|
|
1658
|
+
if (!cached) {
|
|
1659
|
+
cached = this.readObjectStream(num, followLength).catch(() => void 0);
|
|
1660
|
+
this.objectStreams.set(num, cached);
|
|
1661
|
+
}
|
|
1662
|
+
return cached;
|
|
1663
|
+
}
|
|
1664
|
+
async readObjectStream(num, followLength) {
|
|
1665
|
+
const entry = this.xref.entries.get(num);
|
|
1666
|
+
if (entry?.type !== "offset") return void 0;
|
|
1667
|
+
const obj = parseIndirect(this.bytes, entry.offset);
|
|
1668
|
+
const dict = obj.value;
|
|
1669
|
+
if (obj.num !== num || !isDict(dict) || obj.streamStart === void 0) return void 0;
|
|
1670
|
+
const n = dict.entries.get("N");
|
|
1671
|
+
const first = dict.entries.get("First");
|
|
1672
|
+
if (typeof n !== "number" || typeof first !== "number") return void 0;
|
|
1673
|
+
let length = dict.entries.get("Length");
|
|
1674
|
+
if (isRef(length)) length = followLength ? await this.loadLength(length) : void 0;
|
|
1675
|
+
const decoded = await decodeStream(this.bytes, dict, obj.streamStart, typeof length === "number" ? length : void 0);
|
|
1676
|
+
const pairs = [];
|
|
1677
|
+
let at = 0;
|
|
1678
|
+
for (let i = 0; i < n; i++) {
|
|
1679
|
+
const a = parseValue(decoded, at);
|
|
1680
|
+
const b = parseValue(decoded, a.end);
|
|
1681
|
+
if (typeof a.value !== "number" || typeof b.value !== "number") return void 0;
|
|
1682
|
+
pairs.push({
|
|
1683
|
+
num: a.value,
|
|
1684
|
+
offset: b.value
|
|
1685
|
+
});
|
|
1686
|
+
at = b.end;
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
decoded,
|
|
1690
|
+
first,
|
|
1691
|
+
pairs
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
async loadLength(ref) {
|
|
1695
|
+
try {
|
|
1696
|
+
return await this.load(ref, false);
|
|
1697
|
+
} catch {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
/** Undefined when the file is encrypted (`/Encrypt` in the trailer) or its xref cannot be read. */
|
|
1703
|
+
async function openPdfObjects(bytes) {
|
|
1704
|
+
try {
|
|
1705
|
+
const xref = await readXref(bytes);
|
|
1706
|
+
if (!xref || xref.trailer.entries.has("Encrypt")) return void 0;
|
|
1707
|
+
return new Reader(bytes, xref);
|
|
1708
|
+
} catch {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
//#endregion
|
|
1713
|
+
//#region src/model/annotations/embedded-fields.ts
|
|
1714
|
+
/**
|
|
1715
|
+
* `a` with `fields` merged in: `callout`, `fringe`, `head` and the font keys of `defaultStyle`
|
|
1716
|
+
* on a FreeText, `icon` on a note. Undefined when `a` is not embedded, the fields do not fit its
|
|
1717
|
+
* type, or the result fails the schema.
|
|
1718
|
+
*/
|
|
1719
|
+
function withEmbeddedFields(a, fields) {
|
|
1720
|
+
if (a.source !== "pdf") return void 0;
|
|
1721
|
+
const { callout, fringe, head, icon, font } = fields;
|
|
1722
|
+
let candidate;
|
|
1723
|
+
if (a.type === "text") {
|
|
1724
|
+
if (icon === void 0 || callout || fringe || head !== void 0 || font) return void 0;
|
|
1725
|
+
candidate = {
|
|
1726
|
+
...a,
|
|
1727
|
+
icon
|
|
1728
|
+
};
|
|
1729
|
+
} else if (a.type === "freetext") {
|
|
1730
|
+
if (icon !== void 0) return void 0;
|
|
1731
|
+
candidate = {
|
|
1732
|
+
...a,
|
|
1733
|
+
...callout ? { callout } : {},
|
|
1734
|
+
...fringe ? { fringe } : {},
|
|
1735
|
+
...head !== void 0 ? { head } : {},
|
|
1736
|
+
defaultStyle: {
|
|
1737
|
+
...a.defaultStyle,
|
|
1738
|
+
...font
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
} else return;
|
|
1742
|
+
const parsed = AnnotationSchema.safeParse(candidate);
|
|
1743
|
+
return parsed.success ? parsed.data : void 0;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Reads every target once, calling `getData` only if a target names an annotation in
|
|
1747
|
+
* `annotations` by an object reference, and returns the fields read, by id. A target the file
|
|
1748
|
+
* adds nothing to, or whose fields would fail the schema, is left out. Never throws: any
|
|
1749
|
+
* failure leaves the annotations as they are.
|
|
1750
|
+
*/
|
|
1751
|
+
async function readEmbeddedFields(annotations, targets, getData) {
|
|
1752
|
+
const read = /* @__PURE__ */ new Map();
|
|
1753
|
+
if (targets.length === 0) return read;
|
|
1754
|
+
try {
|
|
1755
|
+
const byId = new Map(annotations.map((a) => [a.id, a]));
|
|
1756
|
+
const work = targets.flatMap((target) => {
|
|
1757
|
+
const current = byId.get(target.id);
|
|
1758
|
+
const ref = refOfPdfjsId(target.id.split(":")[2] ?? "");
|
|
1759
|
+
return current && ref ? [{
|
|
1760
|
+
target,
|
|
1761
|
+
current,
|
|
1762
|
+
ref
|
|
1763
|
+
}] : [];
|
|
1764
|
+
});
|
|
1765
|
+
if (work.length === 0) return read;
|
|
1766
|
+
const objs = await openPdfObjects(await getData());
|
|
1767
|
+
if (!objs) return read;
|
|
1768
|
+
for (const { target, current, ref } of work) try {
|
|
1769
|
+
const fields = await readFields(objs, target, ref);
|
|
1770
|
+
if (fields && withEmbeddedFields(current, fields)) read.set(current.id, fields);
|
|
1771
|
+
} catch {}
|
|
1772
|
+
} catch {}
|
|
1773
|
+
return read;
|
|
1774
|
+
}
|
|
1775
|
+
async function readFields(objs, target, ref) {
|
|
1776
|
+
if (target.subtype === "Text") {
|
|
1777
|
+
const icon = await readTextIcon(objs, ref);
|
|
1778
|
+
return icon === void 0 ? void 0 : { icon };
|
|
1779
|
+
}
|
|
1780
|
+
const fields = await readFreeTextFields(objs, ref, target.rect, target.daFont);
|
|
1781
|
+
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
1782
|
+
}
|
|
1783
|
+
//#endregion
|
|
1166
1784
|
//#region src/model/env.ts
|
|
1167
1785
|
/** True only when the build or runtime says production; browsers without `process` count as dev. */
|
|
1168
1786
|
function isProduction() {
|
|
@@ -1215,6 +1833,19 @@ function createHistory(store, opts = {}) {
|
|
|
1215
1833
|
previous = current;
|
|
1216
1834
|
return;
|
|
1217
1835
|
}
|
|
1836
|
+
if (change.kind === "replace" && change.reason === "embedded") {
|
|
1837
|
+
const fill = (list) => list.map((a) => {
|
|
1838
|
+
const f = change.fields.get(a.id);
|
|
1839
|
+
return f && withEmbeddedFields(a, f) || a;
|
|
1840
|
+
});
|
|
1841
|
+
for (const entry of [...undoStack, ...redoStack]) {
|
|
1842
|
+
entry.before = fill(entry.before);
|
|
1843
|
+
entry.after = fill(entry.after);
|
|
1844
|
+
}
|
|
1845
|
+
if (batchStart) batchStart = fill(batchStart);
|
|
1846
|
+
previous = current;
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1218
1849
|
if (change.kind === "replace") {
|
|
1219
1850
|
undoStack.length = 0;
|
|
1220
1851
|
redoStack.length = 0;
|
|
@@ -1308,8 +1939,127 @@ function sameAnnotations(a, b) {
|
|
|
1308
1939
|
return a === b || a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1309
1940
|
}
|
|
1310
1941
|
//#endregion
|
|
1942
|
+
//#region src/model/annotations/gloss-mapping.ts
|
|
1943
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1944
|
+
/** `rest` with `own` merged into a copy of its `custom.inkbar`; `own` wins. */
|
|
1945
|
+
function withCustom(rest, own) {
|
|
1946
|
+
const custom = isObject(rest.custom) ? rest.custom : {};
|
|
1947
|
+
const inkbar = isObject(custom.inkbar) ? custom.inkbar : {};
|
|
1948
|
+
return {
|
|
1949
|
+
...rest,
|
|
1950
|
+
custom: {
|
|
1951
|
+
...custom,
|
|
1952
|
+
inkbar: {
|
|
1953
|
+
...inkbar,
|
|
1954
|
+
...own
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* An annotation as a Gloss file writes it. Gloss has no loupe or mask and no pen pressure or
|
|
1961
|
+
* free-text border colour, so these go under `custom.inkbar`, with loupes and masks written as
|
|
1962
|
+
* the circle or square they draw.
|
|
1963
|
+
*/
|
|
1964
|
+
function toFile(annotation) {
|
|
1965
|
+
switch (annotation.type) {
|
|
1966
|
+
case "ink": {
|
|
1967
|
+
const { pressure, ...rest } = annotation;
|
|
1968
|
+
return pressure ? withCustom(rest, { pressure }) : rest;
|
|
1969
|
+
}
|
|
1970
|
+
case "freetext": {
|
|
1971
|
+
const { borderColor, ...rest } = annotation;
|
|
1972
|
+
return borderColor ? withCustom(rest, { borderColor }) : rest;
|
|
1973
|
+
}
|
|
1974
|
+
case "loupe": {
|
|
1975
|
+
const { magnification, ...rest } = annotation;
|
|
1976
|
+
return withCustom({
|
|
1977
|
+
...rest,
|
|
1978
|
+
type: "circle"
|
|
1979
|
+
}, {
|
|
1980
|
+
type: "loupe",
|
|
1981
|
+
magnification
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
case "mask": {
|
|
1985
|
+
const { shape, dim, ...rest } = annotation;
|
|
1986
|
+
return withCustom({
|
|
1987
|
+
...rest,
|
|
1988
|
+
type: shape === "ellipse" ? "circle" : "square"
|
|
1989
|
+
}, {
|
|
1990
|
+
type: "mask",
|
|
1991
|
+
dim
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
default: return annotation;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Moves `keys` from `custom.inkbar` to the top level of `raw`, replacing any top-level value
|
|
1999
|
+
* (an absent key deletes it), then drops `custom.inkbar`, and `custom`, if that leaves them
|
|
2000
|
+
* empty. Everything else under `custom` stays as it was.
|
|
2001
|
+
*/
|
|
2002
|
+
function lift(raw, inkbar, keys, extra = {}) {
|
|
2003
|
+
const item = {
|
|
2004
|
+
...raw,
|
|
2005
|
+
...extra
|
|
2006
|
+
};
|
|
2007
|
+
const left = { ...inkbar };
|
|
2008
|
+
let consumed = false;
|
|
2009
|
+
for (const key of keys) {
|
|
2010
|
+
delete item[key];
|
|
2011
|
+
if (key in left) {
|
|
2012
|
+
item[key] = left[key];
|
|
2013
|
+
delete left[key];
|
|
2014
|
+
consumed = true;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (!consumed || !isObject(raw.custom)) return item;
|
|
2018
|
+
const { inkbar: _inkbar, ...others } = raw.custom;
|
|
2019
|
+
const custom = Object.keys(left).length > 0 ? {
|
|
2020
|
+
...others,
|
|
2021
|
+
inkbar: left
|
|
2022
|
+
} : others;
|
|
2023
|
+
if (Object.keys(custom).length > 0) item.custom = custom;
|
|
2024
|
+
else delete item.custom;
|
|
2025
|
+
return item;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* A raw file item as Inkbar parses it: Inkbar's own data moves out of `custom.inkbar`. An item
|
|
2029
|
+
* it doesn't recognise passes through unchanged.
|
|
2030
|
+
*/
|
|
2031
|
+
function fromFile(raw) {
|
|
2032
|
+
const inkbar = isObject(raw.custom) && isObject(raw.custom.inkbar) ? raw.custom.inkbar : {};
|
|
2033
|
+
const from = (keys, extra) => ({
|
|
2034
|
+
item: lift(raw, inkbar, keys, extra),
|
|
2035
|
+
lifted: keys
|
|
2036
|
+
});
|
|
2037
|
+
const unchanged = {
|
|
2038
|
+
item: raw,
|
|
2039
|
+
lifted: []
|
|
2040
|
+
};
|
|
2041
|
+
switch (raw.type) {
|
|
2042
|
+
case "ink": return from(["pressure"]);
|
|
2043
|
+
case "freetext": return from(["borderColor"]);
|
|
2044
|
+
case "circle":
|
|
2045
|
+
if (inkbar.type === "loupe") return from(["type", "magnification"]);
|
|
2046
|
+
if (inkbar.type === "mask") return from(["type", "dim"], { shape: "ellipse" });
|
|
2047
|
+
return unchanged;
|
|
2048
|
+
case "square":
|
|
2049
|
+
if (inkbar.type === "mask") return from(["type", "dim"], { shape: "rect" });
|
|
2050
|
+
return unchanged;
|
|
2051
|
+
default: return unchanged;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
//#endregion
|
|
1311
2055
|
//#region src/model/annotations/json.ts
|
|
1312
|
-
|
|
2056
|
+
/**
|
|
2057
|
+
* @internal The Gloss types this core parses, taken from its own schemas rather than Gloss's
|
|
2058
|
+
* list, so a type a later Gloss adds is still treated as foreign here. Loupes and masks are
|
|
2059
|
+
* Inkbar's own types: a file writes them as circles and squares, so a `loupe` or `mask` item is
|
|
2060
|
+
* foreign too.
|
|
2061
|
+
*/
|
|
2062
|
+
const KNOWN_TYPES = new Set(ExportedAnnotationSchema.options.flatMap((o) => [...o.shape.type._zod.values]).filter((type) => type !== "loupe" && type !== "mask"));
|
|
1313
2063
|
/**
|
|
1314
2064
|
* Session annotations plus any preserved foreign items. Embedded (`source: 'pdf'`) ones stay in
|
|
1315
2065
|
* the file. `source` and `locked` are viewer state and are left out.
|
|
@@ -1318,21 +2068,24 @@ const KNOWN_TYPES = new Set(ExportedAnnotationSchema.options.flatMap((o) => [...
|
|
|
1318
2068
|
* export would carry: editing it never touches the store, and values JSON cannot hold, such as
|
|
1319
2069
|
* functions or `undefined` inside `custom`, are left out exactly as `JSON.stringify` would.
|
|
1320
2070
|
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
2071
|
+
* Gloss has no loupe or mask, pen pressure or free-text border colour, so Inkbar writes these
|
|
2072
|
+
* under `custom.inkbar`, with loupes and masks as the circle or square they draw. Every other key
|
|
2073
|
+
* under `custom` goes out as it is.
|
|
2074
|
+
*
|
|
2075
|
+
* The result validates against the Gloss schema (`gloss-format/schema.json`). Foreign items
|
|
2076
|
+
* validate as unknown annotations while their type is one Gloss does not define.
|
|
1324
2077
|
*/
|
|
1325
2078
|
function exportAnnotations(store, preserved = [], documentKey) {
|
|
1326
|
-
const session = store.list.get().filter((a) => a.source === "session").map(({ source: _source, locked: _locked, ...rest }) => rest);
|
|
2079
|
+
const session = store.list.get().filter((a) => a.source === "session").map(({ source: _source, locked: _locked, ...rest }) => toFile(rest));
|
|
1327
2080
|
return {
|
|
1328
|
-
format:
|
|
1329
|
-
version: 1,
|
|
2081
|
+
format: GLOSS_FORMAT$1,
|
|
2082
|
+
version: GLOSS_VERSION$1,
|
|
1330
2083
|
...documentKey === void 0 ? {} : { documentKey },
|
|
1331
2084
|
annotations: JSON.parse(JSON.stringify([...session, ...preserved]))
|
|
1332
2085
|
};
|
|
1333
2086
|
}
|
|
1334
2087
|
/**
|
|
1335
|
-
* Validates the envelope (throws a TypeError when it is not
|
|
2088
|
+
* Validates the envelope (throws a TypeError when it is not a gloss v1 document),
|
|
1336
2089
|
* then each item on its own. Embedded annotations are never touched; imported items become
|
|
1337
2090
|
* session annotations.
|
|
1338
2091
|
*/
|
|
@@ -1353,7 +2106,7 @@ function importAnnotations(store, data, opts) {
|
|
|
1353
2106
|
function planImport(store, data, opts) {
|
|
1354
2107
|
const raw = typeof data === "string" ? JSON.parse(data) : data;
|
|
1355
2108
|
const envelope = AnnotationDocumentSchema.safeParse(raw);
|
|
1356
|
-
if (!envelope.success) throw new TypeError(`Expected
|
|
2109
|
+
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
2110
|
const embeddedIds = new Set(store.list.get().filter((a) => a.source === "pdf").map((a) => a.id));
|
|
1358
2111
|
const result = {
|
|
1359
2112
|
imported: 0,
|
|
@@ -1389,21 +2142,28 @@ function planImport(store, data, opts) {
|
|
|
1389
2142
|
accepted.add(id);
|
|
1390
2143
|
return true;
|
|
1391
2144
|
};
|
|
1392
|
-
envelope.data.annotations.forEach((
|
|
1393
|
-
const type =
|
|
2145
|
+
envelope.data.annotations.forEach((raw, index) => {
|
|
2146
|
+
const type = raw?.type;
|
|
1394
2147
|
if (typeof type === "string" && !KNOWN_TYPES.has(type)) {
|
|
1395
|
-
if (!isForeign(
|
|
2148
|
+
if (!isForeign(raw)) result.errors.push({
|
|
1396
2149
|
index,
|
|
1397
2150
|
issues: [`type: unknown type "${type}" needs a string id and a numeric page to be preserved`]
|
|
1398
2151
|
});
|
|
1399
|
-
else if (claim(
|
|
2152
|
+
else if (claim(raw.id, index)) result.unknown.push(raw);
|
|
1400
2153
|
return;
|
|
1401
2154
|
}
|
|
2155
|
+
const { item, lifted } = isRecord(raw) && typeof type === "string" ? fromFile(raw) : {
|
|
2156
|
+
item: raw,
|
|
2157
|
+
lifted: []
|
|
2158
|
+
};
|
|
1402
2159
|
const parsed = ExportedAnnotationSchema.safeParse(item);
|
|
1403
2160
|
if (!parsed.success) {
|
|
1404
2161
|
result.errors.push({
|
|
1405
2162
|
index,
|
|
1406
|
-
issues: parsed.error.issues.map((i) =>
|
|
2163
|
+
issues: parsed.error.issues.map((i) => {
|
|
2164
|
+
const path = i.path.join(".");
|
|
2165
|
+
return `${typeof i.path[0] === "string" && lifted.includes(i.path[0]) ? `custom.inkbar.${path}` : path}: ${i.message}`;
|
|
2166
|
+
})
|
|
1407
2167
|
});
|
|
1408
2168
|
return;
|
|
1409
2169
|
}
|
|
@@ -1435,8 +2195,11 @@ function planImport(store, data, opts) {
|
|
|
1435
2195
|
}
|
|
1436
2196
|
};
|
|
1437
2197
|
}
|
|
2198
|
+
function isRecord(item) {
|
|
2199
|
+
return typeof item === "object" && item !== null && !Array.isArray(item);
|
|
2200
|
+
}
|
|
1438
2201
|
function isForeign(item) {
|
|
1439
|
-
if (
|
|
2202
|
+
if (!isRecord(item)) return false;
|
|
1440
2203
|
const { type, id, page } = item;
|
|
1441
2204
|
return typeof type === "string" && typeof id === "string" && typeof page === "number";
|
|
1442
2205
|
}
|
|
@@ -1600,7 +2363,6 @@ function createAnnotationStore(opts) {
|
|
|
1600
2363
|
}
|
|
1601
2364
|
if (input.type === "redact") {
|
|
1602
2365
|
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
2366
|
}
|
|
1605
2367
|
return AnnotationSchema.parse(candidate);
|
|
1606
2368
|
}
|
|
@@ -1615,6 +2377,7 @@ function createAnnotationStore(opts) {
|
|
|
1615
2377
|
if (!entry) return;
|
|
1616
2378
|
pending.delete(key);
|
|
1617
2379
|
if (pending.size === 0) proposed = void 0;
|
|
2380
|
+
pruneSelection(basis());
|
|
1618
2381
|
if (isApplied(entry.change, list.get()) || isProduction()) return;
|
|
1619
2382
|
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
2383
|
}
|
|
@@ -1785,6 +2548,26 @@ function createAnnotationStore(opts) {
|
|
|
1785
2548
|
reason: "import"
|
|
1786
2549
|
});
|
|
1787
2550
|
},
|
|
2551
|
+
_patchEmbedded(fields) {
|
|
2552
|
+
if (destroyed) return;
|
|
2553
|
+
const applied = /* @__PURE__ */ new Map();
|
|
2554
|
+
const merge = (items) => items.map((a) => {
|
|
2555
|
+
const f = a.source === "pdf" ? fields.get(a.id) : void 0;
|
|
2556
|
+
const merged = f && withEmbeddedFields(a, f);
|
|
2557
|
+
if (!merged) return a;
|
|
2558
|
+
applied.set(a.id, f);
|
|
2559
|
+
return merged;
|
|
2560
|
+
});
|
|
2561
|
+
const next = merge(list.get());
|
|
2562
|
+
if (applied.size === 0) return;
|
|
2563
|
+
list.set(next);
|
|
2564
|
+
if (proposed) proposed = merge(proposed);
|
|
2565
|
+
emitter.emit("change", {
|
|
2566
|
+
kind: "replace",
|
|
2567
|
+
reason: "embedded",
|
|
2568
|
+
fields: applied
|
|
2569
|
+
});
|
|
2570
|
+
},
|
|
1788
2571
|
on: (event, fn) => emitter.on(event, fn),
|
|
1789
2572
|
destroy() {
|
|
1790
2573
|
destroyed = true;
|
|
@@ -2019,6 +2802,8 @@ async function openDocument(source, options) {
|
|
|
2019
2802
|
});
|
|
2020
2803
|
const embedded = [];
|
|
2021
2804
|
let preserved = [];
|
|
2805
|
+
/** Embedded annotations whose PDF.js data lacks fields the file holds; read after `ready`. */
|
|
2806
|
+
const targets = [];
|
|
2022
2807
|
const destroyedError = () => new InkError("DocumentDestroyed", "The document has been destroyed");
|
|
2023
2808
|
const ready = (async () => {
|
|
2024
2809
|
const pdfjs = await loadPdfjs(options.assets);
|
|
@@ -2059,6 +2844,7 @@ async function openDocument(source, options) {
|
|
|
2059
2844
|
});
|
|
2060
2845
|
embedded.push(...summarise(perPage.map((r) => r.counts)));
|
|
2061
2846
|
const fromFile = perPage.flatMap((r) => r.annotations);
|
|
2847
|
+
targets.push(...perPage.flatMap((r) => r.targets));
|
|
2062
2848
|
if (fromFile.length > 0) store._setEmbedded(fromFile);
|
|
2063
2849
|
})().then(() => {
|
|
2064
2850
|
status.set({ state: "ready" });
|
|
@@ -2075,6 +2861,13 @@ async function openDocument(source, options) {
|
|
|
2075
2861
|
const historyOption = options.history ?? true;
|
|
2076
2862
|
const historyLimit = historyOption === false ? 0 : historyOption === true ? void 0 : historyOption.limit;
|
|
2077
2863
|
const history = createHistory(store, historyLimit === void 0 ? {} : { limit: historyLimit });
|
|
2864
|
+
ready.then(() => {
|
|
2865
|
+
if (destroyed || !handles) return;
|
|
2866
|
+
const { proxy } = handles;
|
|
2867
|
+
readEmbeddedFields(store.list.get(), targets, () => proxy.getData()).then((patched) => {
|
|
2868
|
+
if (!destroyed && patched.size > 0) store._patchEmbedded(patched);
|
|
2869
|
+
}).catch(() => void 0);
|
|
2870
|
+
}, () => void 0);
|
|
2078
2871
|
return {
|
|
2079
2872
|
key: resolved.key,
|
|
2080
2873
|
status: readonly(status),
|
|
@@ -2835,23 +3628,6 @@ function createToolStyle(deps) {
|
|
|
2835
3628
|
};
|
|
2836
3629
|
}
|
|
2837
3630
|
//#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
3631
|
//#region src/model/annotations/threads.ts
|
|
2856
3632
|
function isThreadRoot(a) {
|
|
2857
3633
|
return a.type === "text" || isTextMarkup(a);
|
|
@@ -2968,24 +3744,41 @@ function rectOf(a, b) {
|
|
|
2968
3744
|
height: Math.abs(b.y - a.y)
|
|
2969
3745
|
};
|
|
2970
3746
|
}
|
|
2971
|
-
/**
|
|
3747
|
+
/**
|
|
3748
|
+
* Vertices round a circle, the first at the top centre, clockwise on the page, stretched so their
|
|
3749
|
+
* bounds fill `rect`.
|
|
3750
|
+
*/
|
|
2972
3751
|
function ring(rect, count, radius) {
|
|
2973
|
-
const
|
|
2974
|
-
const cy = rect.y + rect.height / 2;
|
|
2975
|
-
return Array.from({ length: count }, (_, i) => {
|
|
3752
|
+
const unit = Array.from({ length: count }, (_, i) => {
|
|
2976
3753
|
const angle = i * 2 * Math.PI / count;
|
|
2977
3754
|
const r = radius(i);
|
|
2978
3755
|
return {
|
|
2979
|
-
x:
|
|
2980
|
-
y:
|
|
3756
|
+
x: r * Math.sin(angle),
|
|
3757
|
+
y: r * Math.cos(angle)
|
|
2981
3758
|
};
|
|
2982
3759
|
});
|
|
3760
|
+
const xs = unit.map((p) => p.x);
|
|
3761
|
+
const ys = unit.map((p) => p.y);
|
|
3762
|
+
const fit = (v, min, max, start, size) => clean(max > min ? start + (v - min) / (max - min) * size : start + size / 2);
|
|
3763
|
+
const [minX, maxX, minY, maxY] = [
|
|
3764
|
+
Math.min(...xs),
|
|
3765
|
+
Math.max(...xs),
|
|
3766
|
+
Math.min(...ys),
|
|
3767
|
+
Math.max(...ys)
|
|
3768
|
+
];
|
|
3769
|
+
return unit.map((p) => ({
|
|
3770
|
+
x: fit(p.x, minX, maxX, rect.x, rect.width),
|
|
3771
|
+
y: fit(p.y, minY, maxY, rect.y, rect.height)
|
|
3772
|
+
}));
|
|
2983
3773
|
}
|
|
2984
|
-
/**
|
|
3774
|
+
/**
|
|
3775
|
+
* A star's `2 * points` vertices, outer and inner alternating, stretched to fill `rect`, the first
|
|
3776
|
+
* at the top centre.
|
|
3777
|
+
*/
|
|
2985
3778
|
function starVertices(rect, points, innerRatio = .5) {
|
|
2986
3779
|
return ring(rect, Math.max(2, Math.round(points)) * 2, (i) => i % 2 === 0 ? 1 : innerRatio);
|
|
2987
3780
|
}
|
|
2988
|
-
/** A regular polygon's vertices
|
|
3781
|
+
/** A regular polygon's vertices, stretched to fill `rect`, the first at the top centre. */
|
|
2989
3782
|
function polygonVertices(rect, sides) {
|
|
2990
3783
|
return ring(rect, Math.max(3, Math.round(sides)), () => 1);
|
|
2991
3784
|
}
|
|
@@ -3085,16 +3878,13 @@ function shapeInput(kind, page, start, end, opts) {
|
|
|
3085
3878
|
type: "line",
|
|
3086
3879
|
page,
|
|
3087
3880
|
start,
|
|
3088
|
-
end
|
|
3089
|
-
head: "None",
|
|
3090
|
-
tail: "None"
|
|
3881
|
+
end
|
|
3091
3882
|
};
|
|
3092
3883
|
case "arrow": return {
|
|
3093
3884
|
type: "line",
|
|
3094
3885
|
page,
|
|
3095
3886
|
start,
|
|
3096
3887
|
end,
|
|
3097
|
-
tail: "OpenArrow",
|
|
3098
3888
|
intent: "LineArrow"
|
|
3099
3889
|
};
|
|
3100
3890
|
case "rectangle": return {
|
|
@@ -3917,6 +4707,6 @@ function createSignatureLibrary(store) {
|
|
|
3917
4707
|
};
|
|
3918
4708
|
}
|
|
3919
4709
|
//#endregion
|
|
3920
|
-
export {
|
|
4710
|
+
export { importAnnotations as $, readonly 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, derived 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 _, isInkError as _n, IMAGE_DATA_URL as _t, createLocalSignatureStore as a, pointsFromFlat as an, pdfColorToHex as at, isAbandonedNote as b, LineSchema as bt, normaliseInk as c, rectContainsPoint as cn, AnnotationSchema as ct, recogniseStroke as d, rectsIntersect as dn, FreeTextSchema as dt, writable as en, createHistory as et, DEFAULT_CORNER_RADIUS as f, translateQuad as fn, FreeTextStyleSchema as ft, constrain as g, InkError as gn, GLOSS_VERSION$1 as gt, SHAPE_KINDS as h, Emitter as hn, GLOSS_SCHEMA_ID as ht, createInkTemplate as i, multiplyMatrix as in, importedId as it, styleGroupOfType as j, RedactSchema as jt, fromAnnotation as k, QuadSchema as kt, placeSignature as l, rectFromPoints as ln, CircleSchema as lt, DEFAULT_SHAPE_SIZE as m, unionRects as mn, GLOSS_MEDIA_TYPE as mt, SIGNATURE_INK_WIDTH as n, approxEqual as nn, SUPPORTED_SUBTYPES as nt, createMemorySignatureStore as o, quadFromRect as on, summarise as ot, DEFAULT_SHAPE_OPTIONS as p, translateRect as pn, GLOSS_FORMAT$1 as pt, translated as q, parsePdfDate as qt, createImageTemplate as r, invertMatrix as rn, importEmbeddedPage as rt, createSignatureLibrary as s, quadsFromFlat as sn, AnnotationDocumentSchema as st, DEFAULT_SIGNATURE_WIDTH as t, applyMatrix as tn, isProduction as tt, RECOGNISE_THRESHOLD as u, rectFromQuad as un, ExportedAnnotationSchema as ut, isPointerOnlyKind as v, InkSchema as vt, threadOf as w, NoteSchema as wt, isReply as x, LoupeSchema as xt, shapeInput as y, LineEndingSchema as yt, isBubble as z, isStandardCallout as zt };
|
|
3921
4711
|
|
|
3922
|
-
//# sourceMappingURL=signatures-
|
|
4712
|
+
//# sourceMappingURL=signatures-CMapTKrC.mjs.map
|