@inkbar/core 0.11.0 → 0.12.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.
@@ -130,6 +130,38 @@ function rectsIntersect(a, b) {
130
130
  function rectContainsPoint(r, p) {
131
131
  return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
132
132
  }
133
+ /**
134
+ * Whether `p` is inside `q` or on its edge. A quad's corners run top-left, top-right,
135
+ * bottom-left, bottom-right (PDF QuadPoints order), so its outline is p1, p2, p4, p3. Works for
136
+ * any convex quad, whichever way round it winds. A quad with no area (a point or a line, as a
137
+ * malformed file may give) contains nothing.
138
+ */
139
+ function quadContainsPoint(q, p) {
140
+ const ring = [
141
+ q.p1,
142
+ q.p2,
143
+ q.p4,
144
+ q.p3
145
+ ];
146
+ let area = 0;
147
+ for (let i = 0; i < 4; i++) {
148
+ const a = ring[i];
149
+ const b = ring[(i + 1) % 4];
150
+ area += a.x * b.y - b.x * a.y;
151
+ }
152
+ if (area === 0) return false;
153
+ let sign = 0;
154
+ for (let i = 0; i < 4; i++) {
155
+ const a = ring[i];
156
+ const b = ring[(i + 1) % 4];
157
+ const cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
158
+ if (cross === 0) continue;
159
+ const s = Math.sign(cross);
160
+ if (sign === 0) sign = s;
161
+ else if (s !== sign) return false;
162
+ }
163
+ return true;
164
+ }
133
165
  function translateRect(r, dx, dy) {
134
166
  return {
135
167
  x: r.x + dx,
@@ -1542,20 +1574,94 @@ async function openPdfObjects(bytes) {
1542
1574
  }
1543
1575
  //#endregion
1544
1576
  //#region src/pdfjs/destinations.ts
1577
+ /** A coordinate from a destination array: a finite number, or `undefined` for `null` or junk. */
1578
+ function coord(value) {
1579
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1580
+ }
1545
1581
  /**
1546
- * The page index a named destination points at, through PDF.js, which looks the name up in the
1547
- * catalog's `/Names /Dests` tree and its older `/Dests` dictionary. `undefined` when the name
1548
- * is unknown or its destination names no page of the document.
1582
+ * The destination `raw` names, through PDF.js, which looks a name up in the catalog's
1583
+ * `/Names /Dests` tree and its older `/Dests` dictionary. `undefined` when the name is unknown,
1584
+ * the array is malformed, or it names no page of the document.
1549
1585
  */
1550
- async function destinationPage(proxy, name) {
1586
+ async function resolveDestination(proxy, raw) {
1551
1587
  try {
1552
- const target = (await proxy.getDestination(name))?.[0];
1588
+ const array = typeof raw === "string" ? await proxy.getDestination(raw) : raw;
1589
+ if (!array) return void 0;
1590
+ const target = array[0];
1553
1591
  const page = typeof target === "number" ? target : typeof target === "object" && target !== null && "num" in target ? await proxy.getPageIndex(target) : void 0;
1554
- return page !== void 0 && Number.isInteger(page) && page >= 0 && page < proxy.numPages ? page : void 0;
1592
+ if (page === void 0 || !Number.isInteger(page) || page < 0 || page >= proxy.numPages) return;
1593
+ const name = array[1]?.name;
1594
+ switch (typeof name === "string" ? name.toLowerCase() : "") {
1595
+ case "xyz": return {
1596
+ page,
1597
+ fit: "xyz",
1598
+ left: coord(array[2]),
1599
+ top: coord(array[3]),
1600
+ zoom: coord(array[4])
1601
+ };
1602
+ case "fit": return {
1603
+ page,
1604
+ fit: "fit"
1605
+ };
1606
+ case "fitb": return {
1607
+ page,
1608
+ fit: "fitb"
1609
+ };
1610
+ case "fith": return {
1611
+ page,
1612
+ fit: "fith",
1613
+ top: coord(array[2])
1614
+ };
1615
+ case "fitbh": return {
1616
+ page,
1617
+ fit: "fitbh",
1618
+ top: coord(array[2])
1619
+ };
1620
+ case "fitv": return {
1621
+ page,
1622
+ fit: "fitv",
1623
+ left: coord(array[2])
1624
+ };
1625
+ case "fitbv": return {
1626
+ page,
1627
+ fit: "fitbv",
1628
+ left: coord(array[2])
1629
+ };
1630
+ case "fitr": {
1631
+ const [left, bottom, right, top] = [
1632
+ 2,
1633
+ 3,
1634
+ 4,
1635
+ 5
1636
+ ].map((i) => coord(array[i]));
1637
+ if (left === void 0 || bottom === void 0 || right === void 0 || top === void 0) return;
1638
+ return {
1639
+ page,
1640
+ fit: "fitr",
1641
+ rect: {
1642
+ x: Math.min(left, right),
1643
+ y: Math.min(bottom, top),
1644
+ width: Math.abs(right - left),
1645
+ height: Math.abs(top - bottom)
1646
+ }
1647
+ };
1648
+ }
1649
+ default: return {
1650
+ page,
1651
+ fit: "fit"
1652
+ };
1653
+ }
1555
1654
  } catch {
1556
1655
  return;
1557
1656
  }
1558
1657
  }
1658
+ /**
1659
+ * The page index a named destination points at. `undefined` when the name is unknown or its
1660
+ * destination names no page of the document.
1661
+ */
1662
+ async function destinationPage(proxy, name) {
1663
+ return (await resolveDestination(proxy, name))?.page;
1664
+ }
1559
1665
  //#endregion
1560
1666
  //#region src/pdfjs/glyphs.ts
1561
1667
  const IDENTITY = [
@@ -1817,1485 +1923,1630 @@ function mapLoadError(cause, context) {
1817
1923
  }, cause);
1818
1924
  }
1819
1925
  //#endregion
1820
- //#region src/model/form/values.ts
1821
- /**
1822
- * Why `value` cannot be the field's value, or `undefined` when it can. A locked or read-only
1823
- * field is `readOnly`; otherwise the value must suit the type: any text within `maxLength`, an
1824
- * option of a non-editable choice, an array of options for a multi-select list, an on state or
1825
- * `Off` for a checkbox or radio, and `{ signature }` for a signature field. A button has no value.
1826
- */
1827
- function checkValue(field, value) {
1828
- if (field.locked || field.flags.readOnly) return "readOnly";
1829
- return suits(field, value) ? void 0 : "invalid";
1830
- }
1831
- /** Whether `value` suits the field's type, whatever its flags. */
1832
- function suits(field, value) {
1833
- const isOption = (v) => field.options.some((o) => o.value === v);
1834
- switch (field.type) {
1835
- case "text": return typeof value === "string" && (field.maxLength === void 0 || value.length <= field.maxLength);
1836
- case "combo": return typeof value === "string" && (field.flags.editable || isOption(value));
1837
- case "list": return field.flags.multiSelect ? Array.isArray(value) && value.every(isOption) : typeof value === "string" && isOption(value);
1838
- case "checkbox":
1839
- case "radio": return value === "Off" || typeof value === "string" && (isOption(value) || field.widgets.some((w) => w.onState === value));
1840
- case "signature": return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.signature === "string" && value.signature !== "";
1841
- case "button": return false;
1926
+ //#region src/model/pdf-date.ts
1927
+ /** Parses a PDF date string (`D:YYYYMMDDHHmmSSOHH'mm'`) into ISO 8601. Returns `fallback` on anything unparseable. */
1928
+ function parsePdfDate(value, fallback) {
1929
+ if (!value) return fallback;
1930
+ const m = /^(?:D:)?(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}(?:'\d{2}')?)?/.exec(value.trim());
1931
+ if (!m) return fallback;
1932
+ const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
1933
+ let offsetMinutes = 0;
1934
+ if (zone && zone !== "Z") {
1935
+ const sign = zone.startsWith("-") ? -1 : 1;
1936
+ const zh = Number(zone.slice(1, 3));
1937
+ const zm = Number(zone.slice(4, 6) || 0);
1938
+ offsetMinutes = sign * (zh * 60 + zm);
1842
1939
  }
1940
+ const utc = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s)) - offsetMinutes * 6e4;
1941
+ if (Number.isNaN(utc)) return fallback;
1942
+ return new Date(utc).toISOString();
1843
1943
  }
1844
- /**
1845
- * A checked value as it is stored: frozen, a multi-select's options in option order without
1846
- * repeats, and a signature with only its id.
1847
- */
1848
- function normaliseValue(field, value) {
1849
- if (Array.isArray(value)) {
1850
- const chosen = new Set(value);
1851
- return Object.freeze(field.options.map((o) => o.value).filter((v) => chosen.has(v) && chosen.delete(v)));
1852
- }
1853
- if (typeof value === "object") return Object.freeze({ signature: value.signature });
1854
- return value;
1944
+ //#endregion
1945
+ //#region src/model/annotations/defaults.ts
1946
+ const COLORS = {
1947
+ highlight: "#F2C230",
1948
+ underline: "#2B5A9E",
1949
+ strikeout: "#C0392B",
1950
+ squiggly: "#1F7A4D",
1951
+ text: "#F2C230",
1952
+ freetext: "#151820",
1953
+ square: "#C0392B",
1954
+ circle: "#C0392B",
1955
+ line: "#151820",
1956
+ polyline: "#151820",
1957
+ polygon: "#151820",
1958
+ ink: "#151820",
1959
+ stamp: "#151820",
1960
+ loupe: "#151820",
1961
+ mask: "#000000",
1962
+ redact: "#C0392B"
1963
+ };
1964
+ const staticDefaults = {
1965
+ color: (type) => COLORS[type],
1966
+ fill: () => "#000000",
1967
+ opacity: (type) => type === "highlight" ? .4 : 1
1968
+ };
1969
+ const VARIABLES = {
1970
+ highlight: "--ink-highlight",
1971
+ underline: "--ink-underline",
1972
+ strikeout: "--ink-strikeout",
1973
+ squiggly: "--ink-squiggly",
1974
+ text: "--ink-note",
1975
+ freetext: "--ink-free-text",
1976
+ square: "--ink-shape",
1977
+ circle: "--ink-shape",
1978
+ line: "--ink-shape",
1979
+ polyline: "--ink-shape",
1980
+ polygon: "--ink-shape",
1981
+ ink: "--ink-ink",
1982
+ stamp: "--ink-ink",
1983
+ loupe: "--ink-shape",
1984
+ mask: "--ink-mask",
1985
+ redact: "--ink-redact"
1986
+ };
1987
+ /** Resolves default colours from CSS custom properties on `el`, falling back to `staticDefaults`. */
1988
+ function cssVariableDefaults(el) {
1989
+ const read = (name) => {
1990
+ const view = el.ownerDocument.defaultView;
1991
+ if (!view) return void 0;
1992
+ return normalizeColor(view.getComputedStyle(el).getPropertyValue(name));
1993
+ };
1994
+ return {
1995
+ color: (type) => read(VARIABLES[type]) ?? staticDefaults.color(type),
1996
+ fill: () => read("--ink-redact-fill") ?? staticDefaults.fill(),
1997
+ opacity: (type) => staticDefaults.opacity(type)
1998
+ };
1855
1999
  }
1856
- /** The value a reset gives: the PDF's default, else empty for the type, else none. */
1857
- function resetValue(field) {
1858
- if (field.defaultValue !== void 0) return field.defaultValue;
1859
- switch (field.type) {
1860
- case "text":
1861
- case "combo": return "";
1862
- case "checkbox":
1863
- case "radio": return "Off";
1864
- case "list": return field.flags.multiSelect ? Object.freeze([]) : void 0;
1865
- default: return;
2000
+ /** Accepts `#rgb`, `#rrggbb`, `rgb(r, g, b)` and `rgb(r g b)`; returns `#RRGGBB` or undefined. */
2001
+ function normalizeColor(value) {
2002
+ const v = value.trim();
2003
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(v);
2004
+ if (short) {
2005
+ const [, r = "", g = "", b = ""] = short;
2006
+ return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
1866
2007
  }
1867
- }
1868
- function sameValue$1(a, b) {
1869
- if (a === b) return true;
1870
- if (a === void 0 || b === void 0) return false;
1871
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => v === b[i]);
1872
- return typeof a === "object" && typeof b === "object" && a.signature === b.signature;
2008
+ if (/^#[0-9a-f]{6}$/i.test(v)) return v.toUpperCase();
2009
+ const rgb = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/i.exec(v);
2010
+ if (rgb) return `#${[
2011
+ rgb[1],
2012
+ rgb[2],
2013
+ rgb[3]
2014
+ ].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
1873
2015
  }
1874
2016
  //#endregion
1875
- //#region src/model/form/gloss.ts
1876
- /** A field value as Gloss writes it: text, the options of a multi-select, or a signature id. */
1877
- const GlossFieldValueSchema = z.union([
1878
- z.string(),
1879
- z.array(z.string()),
1880
- z.looseObject({ signature: z.string().min(1) })
1881
- ]);
1882
- z.record(z.string().min(1), GlossFieldValueSchema);
2017
+ //#region src/model/annotations/callout.ts
1883
2018
  /**
1884
- * The values to export: every field that has one and is not no-export, in field order, with a
1885
- * multi-select's options in option order.
2019
+ * The insets from a free text's `rect` to its text box, in points (PDF `/RD`, XFDF `fringe`,
2020
+ * both in this order).
1886
2021
  */
1887
- function fieldsToGloss(values, fields) {
1888
- const out = {};
1889
- for (const field of fields) {
1890
- const value = values[field.name];
1891
- if (value === void 0 || field.flags.noExport) continue;
1892
- out[field.name] = normaliseValue(field, value);
1893
- }
1894
- return out;
1895
- }
2022
+ const FringeSchema = z.object(Fringe.shape);
2023
+ /** An ending's length, and its full width, as a multiple of the stroke width. */
2024
+ const ENDING_SCALE = 3;
1896
2025
  /**
1897
- * The values of a Gloss `fields` map that suit the form, and the names that do not: unknown,
1898
- * read-only (or locked), or invalid for the field. Anything other than a map applies nothing.
2026
+ * A free text's text box: `rect` inset by `fringe`, or `rect` itself without one. Insets wider
2027
+ * or taller than `rect` leave a box of width or height 0 at the inset origin.
1899
2028
  */
1900
- function fieldsFromGloss(raw, fields) {
1901
- const values = {};
1902
- const rejected = [];
1903
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
1904
- values,
1905
- rejected
1906
- };
1907
- const byName = new Map(fields.map((f) => [f.name, f]));
1908
- for (const [name, value] of Object.entries(raw)) {
1909
- const field = byName.get(name);
1910
- if (!field) {
1911
- rejected.push({
1912
- name,
1913
- reason: "unknown"
1914
- });
1915
- continue;
1916
- }
1917
- const parsed = GlossFieldValueSchema.safeParse(value);
1918
- const reason = checkValue(field, value) ?? (parsed.success ? void 0 : "invalid");
1919
- if (reason || !parsed.success) {
1920
- rejected.push({
1921
- name,
1922
- reason: reason ?? "invalid"
1923
- });
1924
- continue;
1925
- }
1926
- values[name] = normaliseValue(field, parsed.data);
1927
- }
2029
+ function freeTextBox(a) {
2030
+ const { rect, fringe } = a;
2031
+ if (!fringe) return rect;
1928
2032
  return {
1929
- values,
1930
- rejected
2033
+ x: rect.x + fringe.left,
2034
+ y: rect.y + fringe.bottom,
2035
+ width: Math.max(0, rect.width - fringe.left - fringe.right),
2036
+ height: Math.max(0, rect.height - fringe.top - fringe.bottom)
1931
2037
  };
1932
2038
  }
1933
- //#endregion
1934
- //#region src/model/form/submit.ts
1935
2039
  /**
1936
- * The values a submit sends: every field with a value that is not no-export, limited to
1937
- * `names` (or everything but them, when `include` is false). A name covers the fields under it.
1938
- * An empty list means every field.
2040
+ * True for a free-text callout drawn the standard way: a box with a leader line through
2041
+ * `callout`. A callout with `cornerRadius` is Inkbar's speech bubble instead.
1939
2042
  */
1940
- function submitValues(values, fields, names, include) {
1941
- const all = fieldsToGloss(values, fields);
1942
- if (names.length === 0) return Object.freeze(all);
1943
- const listed = (name) => names.some((n) => name === n || name.startsWith(`${n}.`));
1944
- const out = {};
1945
- for (const [name, value] of Object.entries(all)) if (listed(name) === include) out[name] = value;
1946
- return Object.freeze(out);
2043
+ function isStandardCallout(a) {
2044
+ return a.type === "freetext" && a.intent === "FreeTextCallout" && a.callout !== void 0 && a.callout.length >= 2 && a.cornerRadius === void 0;
1947
2045
  }
1948
- //#endregion
1949
- //#region src/model/form/url.ts
1950
- /** The schemes a form may hand the host, as PDF.js allows for `/URI`. */
1951
- const SAFE = /* @__PURE__ */ new Set([
1952
- "http:",
1953
- "https:",
1954
- "mailto:",
1955
- "tel:"
1956
- ]);
1957
2046
  /**
1958
- * `url` when it is absolute and uses http, https, mailto or tel; otherwise `undefined`, so a
1959
- * `javascript:`, `data:`, `file:` or relative URL from a PDF never reaches the host.
2047
+ * The leader line's stroke: the border colour, else the text colour, and the border width, with
2048
+ * 1 pt when the box has no border.
1960
2049
  */
1961
- function safeUrl(url) {
1962
- try {
1963
- return SAFE.has(new URL(url).protocol) ? url : void 0;
1964
- } catch {
1965
- return;
1966
- }
2050
+ function calloutStroke(a) {
2051
+ return {
2052
+ color: a.borderColor ?? a.defaultStyle.color,
2053
+ width: a.width > 0 ? a.width : 1
2054
+ };
1967
2055
  }
1968
2056
  /**
1969
- * A mail address, kept unless it reads as a URL with another scheme (a script passing
1970
- * `javascript:…` as a recipient). A `mailto:` or `tel:` prefix is kept as it was given, not
1971
- * stripped, so the host may see `mailto:a@example.com` as a recipient.
2057
+ * The `rect` and `fringe` that hold the text box `box` and a leader line through `callout`
2058
+ * stroked `width` points wide: every point is padded by half the stroke, and by the ending's
2059
+ * size as well when `head` is one Inkbar draws.
1972
2060
  */
1973
- function safeAddress(address) {
1974
- try {
1975
- return SAFE.has(new URL(address).protocol) ? address : void 0;
1976
- } catch {
1977
- return address;
1978
- }
2061
+ function withCalloutBounds(box, callout, width, head) {
2062
+ const drawn = head !== void 0 && head !== "None" && isKnown(LINE_ENDINGS, head);
2063
+ const pad = width / 2 + (drawn ? ENDING_SCALE * Math.max(width, 1) : 0);
2064
+ const xs = [
2065
+ box.x,
2066
+ box.x + box.width,
2067
+ ...callout.map((p) => p.x)
2068
+ ];
2069
+ const ys = [
2070
+ box.y,
2071
+ box.y + box.height,
2072
+ ...callout.map((p) => p.y)
2073
+ ];
2074
+ const left = Math.min(...xs) - pad;
2075
+ const bottom = Math.min(...ys) - pad;
2076
+ const right = Math.max(...xs) + pad;
2077
+ const top = Math.max(...ys) + pad;
2078
+ return {
2079
+ rect: {
2080
+ x: left,
2081
+ y: bottom,
2082
+ width: right - left,
2083
+ height: top - bottom
2084
+ },
2085
+ fringe: {
2086
+ left: box.x - left,
2087
+ top: top - (box.y + box.height),
2088
+ right: right - (box.x + box.width),
2089
+ bottom: box.y - bottom
2090
+ }
2091
+ };
1979
2092
  }
1980
2093
  //#endregion
1981
- //#region src/pdfjs/sandbox-patch.ts
2094
+ //#region src/model/annotations/types.ts
2095
+ const PointSchema = z.object(Point.shape);
2096
+ const RectSchema = z.object(Rect.shape);
2097
+ const QuadSchema = z.object({
2098
+ p1: PointSchema,
2099
+ p2: PointSchema,
2100
+ p3: PointSchema,
2101
+ p4: PointSchema
2102
+ });
2103
+ /** What Inkbar adds to every Gloss type: the fields it always fills in, and its viewer state. */
2104
+ const Base = {
2105
+ /** Bounding box in PDF user space. */
2106
+ rect: RectSchema,
2107
+ /** Resolved #RRGGBB. Written into the PDF later, so it is always concrete. */
2108
+ color: Color,
2109
+ opacity: commonFields.opacity.removeDefault(),
2110
+ createdAt: commonFields.createdAt.unwrap(),
2111
+ modifiedAt: commonFields.modifiedAt.unwrap(),
2112
+ /** Created in this session, or imported from the file. Viewer state, never exported. */
2113
+ source: z.enum(["session", "pdf"]),
2114
+ /** Embedded annotations are locked until the engine can rewrite the file. Viewer state, never exported. */
2115
+ locked: z.boolean().optional(),
2116
+ /**
2117
+ * Embedded annotations only: whether the file carries its own appearance stream for it. PDF.js
2118
+ * paints the page with it; without one, it paints a stand-in for most types but nothing for a
2119
+ * note. Viewer state, never exported.
2120
+ */
2121
+ hasAppearance: z.boolean().optional()
2122
+ };
2123
+ const Quads = z.array(QuadSchema).min(1);
2124
+ const TextMarkupSchema = z.object({
2125
+ ...Highlight.shape,
2126
+ ...Base,
2127
+ type: z.enum([
2128
+ "highlight",
2129
+ "underline",
2130
+ "strikeout",
2131
+ "squiggly"
2132
+ ]),
2133
+ quads: Quads
2134
+ });
2135
+ /** A note's icon (XFDF `icon`): a PDF name, so a name Inkbar does not know is kept. */
2136
+ const NoteIconSchema = NoteIcon;
2137
+ const NoteSchema = z.object({
2138
+ ...Note.shape,
2139
+ ...Base
2140
+ });
2141
+ /** A line ending (XFDF `head`/`tail`): a PDF name, so a name Inkbar does not know is kept. */
2142
+ const LineEndingSchema = LineEnding;
1982
2143
  /**
1983
- * PDF.js's scripting API defines `submitForm`, `mailDoc`, `mailForm` (on Doc) and `launchURL`
1984
- * (on App) as empty methods, so a script calling them is silent. They cannot be replaced from a
1985
- * script run inside the sandbox: scripts run as global code, where `this` is the global object,
1986
- * and PDF.js copies each Doc method onto it as a bound, non-writable, non-configurable property
1987
- * before any script runs. So the bridge rewrites the four empty bodies in the scripting source
1988
- * before the sandbox evaluates it. Each replacement reports the call through the sandbox's own
1989
- * `send` channel, with a command name only Inkbar uses; the host maps the arguments.
2144
+ * A key a type refuses outright rather than dropping like other unknown keys: one an older
2145
+ * shape used (so its value is not silently lost), or one that means nothing on the type (so a
2146
+ * later version is free to give it a meaning).
1990
2147
  */
1991
- /** The command a rewritten method sends. */
1992
- const ACTION_COMMAND = "inkbar:action";
1993
- const METHODS = [
1994
- "submitForm",
1995
- "mailDoc",
1996
- "mailForm",
1997
- "launchURL"
1998
- ];
1999
- /** The empty method as it appears in PDF.js's scripting source, one class member per line. */
2000
- const stub = (method) => `\n ${method}() {}\n`;
2001
- const replacement = (method) => `\n ${method}(...args) { this._send({ command: "${ACTION_COMMAND}", method: "${method}", args }); }\n`;
2002
- /** How many times each empty method appears in `source`. */
2003
- function countStubs(source) {
2004
- const counts = {};
2005
- for (const m of METHODS) counts[m] = source.split(stub(m)).length - 1;
2006
- return counts;
2007
- }
2148
+ const Refused = z.never().optional();
2008
2149
  /**
2009
- * Rewrites the four empty methods. Throws when any of them does not appear exactly once, which
2010
- * means the PDF.js build differs from the one this was written against.
2150
+ * A free-text box's text style, keyed by the CSS property names XFDF writes in its
2151
+ * `defaultstyle` string. The older `align` is refused; other unknown keys are dropped.
2011
2152
  */
2012
- function patchScriptingSource(source) {
2013
- const counts = countStubs(source);
2014
- const off = METHODS.filter((m) => counts[m] !== 1);
2015
- if (off.length > 0) throw new Error(`The PDF.js scripting source does not match: ${off.join(", ")}`);
2016
- return METHODS.reduce((src, m) => src.replace(stub(m), () => replacement(m)), source);
2017
- }
2153
+ const FreeTextStyleSchema = z.object({
2154
+ ...FreeTextStyle.shape,
2155
+ /** The text colour. */
2156
+ color: Color,
2157
+ align: Refused
2158
+ });
2018
2159
  /**
2019
- * Wraps the engine's string copy so the first string it receives, the scripting source that
2020
- * `Sandbox.create` hands it, is patched. A mismatch throws inside `create`, which then fails to
2021
- * start the sandbox; `failure()` gives the reason.
2160
+ * Free text, as XFDF has it: `color` is the box's fill and the text style lives in
2161
+ * `defaultStyle`. A box in the older shape (top-level `fontFamily`, `fontSize`, `align`) is
2162
+ * refused rather than silently losing its style; other unknown keys are dropped.
2022
2163
  */
2023
- function patchSandboxSource(module) {
2024
- const original = module.stringToNewUTF8.bind(module);
2025
- let done = false;
2026
- let failure;
2027
- module.stringToNewUTF8 = (source) => {
2028
- if (done) return original(source);
2029
- done = true;
2030
- try {
2031
- return original(patchScriptingSource(source));
2032
- } catch (e) {
2033
- failure = e instanceof Error ? e : new Error(String(e));
2034
- throw failure;
2035
- }
2036
- };
2037
- return { failure: () => failure };
2164
+ const FreeTextSchema = z.object({
2165
+ ...FreeText.shape,
2166
+ ...Base,
2167
+ /** The box's fill (XFDF `color`). Absent means no fill. */
2168
+ color: FreeText.shape.color,
2169
+ /** The box's border colour. Absent means the text colour. */
2170
+ borderColor: Color.optional(),
2171
+ defaultStyle: FreeTextStyleSchema,
2172
+ fringe: FringeSchema.optional(),
2173
+ callout: z.array(PointSchema).min(2).max(3).optional(),
2174
+ fontFamily: Refused,
2175
+ fontSize: Refused,
2176
+ align: Refused
2177
+ });
2178
+ const SquareSchema = z.object({
2179
+ ...Square.shape,
2180
+ ...Base
2181
+ });
2182
+ const CircleSchema = z.object({
2183
+ ...Circle.shape,
2184
+ ...Base,
2185
+ cornerRadius: Refused
2186
+ });
2187
+ const ShapeSchema = z.discriminatedUnion("type", [SquareSchema, CircleSchema]);
2188
+ const LineSchema = z.object({
2189
+ ...Line.shape,
2190
+ ...Base,
2191
+ start: PointSchema,
2192
+ end: PointSchema
2193
+ });
2194
+ const Vertices = z.array(PointSchema).min(2);
2195
+ const PolylineSchema = z.object({
2196
+ ...Polyline.shape,
2197
+ ...Base,
2198
+ vertices: Vertices
2199
+ });
2200
+ const PolygonSchema = z.object({
2201
+ ...Polygon.shape,
2202
+ ...Base,
2203
+ vertices: Vertices,
2204
+ head: Refused,
2205
+ tail: Refused
2206
+ });
2207
+ const PolySchema = z.discriminatedUnion("type", [PolylineSchema, PolygonSchema]);
2208
+ const InkShape = z.object({
2209
+ ...Ink.shape,
2210
+ ...Base,
2211
+ inklist: z.array(z.array(PointSchema).min(1)).min(1),
2212
+ /** Pen pressure from 0 to 1, one value per point of each stroke. Absent means even pressure. */
2213
+ pressure: z.array(z.array(z.number().min(0).max(1))).optional()
2214
+ });
2215
+ /** Pressure must have one value per point of each stroke. */
2216
+ function checkPressure(a, ctx) {
2217
+ if (!a.pressure) return;
2218
+ if (!(a.pressure.length === a.inklist.length && a.pressure.every((p, i) => p.length === a.inklist[i]?.length))) ctx.addIssue({
2219
+ code: "custom",
2220
+ path: ["pressure"],
2221
+ message: "pressure must have the same shape as inklist"
2222
+ });
2038
2223
  }
2039
- //#endregion
2040
- //#region src/pdfjs/scripting.ts
2041
- /** Where the sandbox module comes from. An object, so tests can see whether it was loaded. */
2042
- const sandboxLoader = { load: () => import("#pdfjs-sandbox") };
2224
+ const InkSchema = InkShape.superRefine(checkPressure);
2043
2225
  /**
2044
- * Runs the document's JavaScript in PDF.js's QuickJS sandbox, wired to `form`. Resolves
2045
- * `undefined` without loading the sandbox when the document has no JavaScript. A sandbox that
2046
- * fails to load or start is reported as a `FormScriptError` on `form`, and resolves `undefined`,
2047
- * so the form stays fillable without scripts.
2048
- *
2049
- * The sandbox gets its own `window`: alerts go to `form`'s `alert` event, `app.response` returns
2050
- * `null` and asks the host with a `prompt` action, and timers are tracked and cleared on destroy.
2051
- * Nothing reaches the page's `window.alert`, `prompt` or `confirm`, and nothing is fetched.
2226
+ * An image on the page (XFDF `stamp`), drawn to fill `rect`. Image and typed signatures are
2227
+ * stamps with `subject: 'Signature'`.
2052
2228
  */
2053
- async function createFormScripting(opts) {
2054
- const { proxy, assets, form } = opts;
2055
- if (typeof window === "undefined") return void 0;
2056
- if (!await proxy.hasJSActions()) return void 0;
2057
- const [objects, calculationOrder, actions] = await Promise.all([
2058
- proxy.getFieldObjects(),
2059
- proxy.getCalculationOrderIds(),
2060
- proxy.getJSActions()
2061
- ]);
2062
- if (![...objects?.values() ?? []].some((list) => list.some((o) => hasEntries(o.actions))) && !hasEntries(actions)) return void 0;
2063
- const fields = form.fields.get();
2064
- const byName = new Map(fields.map((f) => [f.name, f]));
2065
- const byWidget = new Map(fields.flatMap((f) => f.widgets.map((w) => [w.id, f])));
2066
- const widgets = new Map(fields.flatMap((f) => f.widgets.map((w) => [w.id, w])));
2067
- /** Fields PDF.js reads as a date or time, from their standard format scripts. */
2068
- const dates = new Set([...objects?.entries() ?? []].filter(([, list]) => list.some((o) => o.hasDatetimeHTML === true)).map(([name]) => name));
2069
- /** What the sandbox holds, by field name, in the form's terms. */
2070
- const mirror = /* @__PURE__ */ new Map();
2071
- for (const f of fields) if (f.value !== void 0) mirror.set(f.name, f.value);
2072
- let dead = false;
2073
- /** Updates sent during a synchronous dispatch, applied together once it returns. */
2074
- let buffer;
2075
- const report = (cause) => {
2076
- if (dead) return;
2077
- const message = cause instanceof Error ? cause.message : String(cause);
2078
- form._error(new InkError("FormScriptError", message, {}, cause));
2079
- };
2080
- const onSend = (detail) => {
2081
- if (dead || typeof detail !== "object" || detail === null) return;
2082
- if (buffer) buffer.push(detail);
2083
- else apply([detail], { values: true });
2084
- };
2085
- const win = createWindow(form, onSend, report, () => dead);
2086
- let sandbox;
2087
- try {
2088
- sandbox = await withTimeout(sandboxLoader.load().then(({ QuickJSSandbox }) => QuickJSSandbox(assets.wasm)), SANDBOX_TIMEOUT_MS);
2089
- const support = sandbox.support;
2090
- const engine = sandbox._module;
2091
- if (!support || !engine) throw new Error("The PDF.js sandbox has no engine");
2092
- support.win = win.shim;
2093
- const patch = patchSandboxSource(engine);
2094
- try {
2095
- sandbox.create({
2096
- objects,
2097
- calculationOrder,
2098
- appInfo: {
2099
- platform: navigator.platform,
2100
- language: navigator.language
2101
- },
2102
- docInfo: {
2103
- Title: opts.info.title ?? "",
2104
- Author: opts.info.author ?? "",
2105
- numPages: opts.info.pageCount,
2106
- URL: "",
2107
- baseURL: "",
2108
- actions
2109
- }
2110
- });
2111
- } catch (cause) {
2112
- win.clear();
2113
- throw patch.failure() ?? cause;
2114
- }
2115
- } catch (cause) {
2116
- dead = true;
2117
- try {
2118
- sandbox?.nukeSandbox();
2119
- } catch {}
2120
- form._error(new InkError("FormScriptError", "The form's scripts could not start; it can still be filled in", {}, cause));
2121
- return;
2122
- }
2123
- /** Page events run one after another. */
2124
- let pages = Promise.resolve();
2125
- /** Pages whose actions the sandbox already has. */
2126
- const visited = /* @__PURE__ */ new Set();
2127
- /** Dispatches `event` and returns what the scripts sent while it ran. */
2128
- const run = (event) => {
2129
- const outer = buffer;
2130
- const got = [];
2131
- buffer = got;
2132
- try {
2133
- sandbox.dispatchEvent(event);
2134
- } catch (cause) {
2135
- report(cause);
2136
- } finally {
2137
- buffer = outer;
2138
- }
2139
- return got;
2140
- };
2141
- const fieldOf = (id) => typeof id === "string" ? byWidget.get(id) : void 0;
2142
- const ownOf = (u, field) => fieldOf(u.id) === field;
2143
- /**
2144
- * Applies updates. `values: false` keeps the sandbox's values out of the form (an undo
2145
- * replaying a state calculate already produced). The `source` field's own updates are
2146
- * skipped when its commit was rejected, and its value always, since the form writes it.
2147
- */
2148
- function apply(updates, o) {
2149
- const next = {};
2150
- for (const u of updates) {
2151
- if (dead) return;
2152
- if (u.id === void 0 || u.id === null) {
2153
- command(u);
2154
- continue;
2155
- }
2156
- const field = fieldOf(u.id);
2157
- if (!field) continue;
2158
- const own = o.source?.field === field;
2159
- if (own && !o.source?.accepted) continue;
2160
- if ("value" in u) {
2161
- const raw = dates.has(field.name) && typeof u.value === "number" ? u.formattedValue : u.value;
2162
- const value = raw === u.value || typeof raw === "string" ? fromSandbox(field, u.id, raw) : void 0;
2163
- if (value !== void 0) {
2164
- mirror.set(field.name, value);
2165
- if (o.values && !own) next[field.name] = value;
2166
- }
2167
- }
2168
- if ("formattedValue" in u) form._setFormatted(field.name, typeof u.formattedValue === "string" ? u.formattedValue : void 0);
2169
- const override = overrideOf(u);
2170
- if (override) {
2171
- const siblings = Array.isArray(u.siblings) ? u.siblings : [];
2172
- for (const id of [u.id, ...siblings]) if (typeof id === "string" && widgets.has(id)) form._setWidgetState(id, override);
2173
- }
2174
- if (u.focus === true) form._focus(u.id);
2175
- }
2176
- if (Object.keys(next).length > 0) form._scriptValues(next);
2177
- }
2178
- function command(u) {
2179
- switch (u.command) {
2180
- case "print":
2181
- form._emitAction({ kind: "print" });
2182
- break;
2183
- case ACTION_COMMAND: {
2184
- const action = scriptAction$1(u.method, u.args, form);
2185
- if (action) form._emitAction(action);
2186
- break;
2187
- }
2188
- case "error": report(new Error(typeof u.value === "string" ? u.value : "A form script failed"));
2189
- }
2190
- }
2191
- /**
2192
- * Commits `value` to `field` in the sandbox: validate, calculate and format run. Returns the
2193
- * value the sandbox kept, which a script may have changed, or `undefined` when rejected.
2194
- */
2195
- const commitTo = (field, value, values) => {
2196
- const event = commitEvent(field, value);
2197
- if (!event) {
2198
- mirror.set(field.name, value);
2199
- return value;
2200
- }
2201
- const updates = run(event);
2202
- const accepted = !updates.some((u) => ownOf(u, field) && "selRange" in u);
2203
- apply(updates, {
2204
- values,
2205
- source: {
2206
- field,
2207
- accepted
2208
- }
2209
- });
2210
- if (!accepted) return void 0;
2211
- const echo = updates.filter((u) => ownOf(u, field) && "value" in u).at(-1);
2212
- const kept = echo ? scriptChange(field, echo) : void 0;
2213
- const stored = kept !== void 0 && !sameValue$1(kept, value) ? kept : value;
2214
- mirror.set(field.name, stored);
2215
- return stored;
2216
- };
2217
- /**
2218
- * The value a script changed a commit to, from the sandbox's echo of it; `undefined` when
2219
- * the echo is no change. The sandbox reports a text value as the string it was given (it
2220
- * keeps the original alongside any number it parses), so a different string is a script's
2221
- * change. A number is the sandbox's own parse: a date or time field reports the time it
2222
- * read. A checkbox or radio echo names the first widget with the export value, which is
2223
- * not always the one chosen, so the chosen state stands; a rejection shows as a selection.
2224
- */
2225
- const scriptChange = (field, echo) => {
2226
- switch (field.type) {
2227
- case "text":
2228
- case "combo": return typeof echo.value === "string" ? fromSandbox(field, echo.id, echo.value) : void 0;
2229
- case "list": return fromSandbox(field, echo.id, echo.value);
2230
- default: return;
2231
- }
2232
- };
2233
- const scripting = {
2234
- commit(name, value) {
2235
- const field = byName.get(name);
2236
- if (dead || !field) return value;
2237
- return commitTo(field, value, true);
2238
- },
2239
- sync(values, calculate) {
2240
- if (dead) return;
2241
- for (const field of fields) {
2242
- const value = values[field.name];
2243
- if (value === void 0 || sameValue$1(value, mirror.get(field.name))) continue;
2244
- commitTo(field, value, calculate);
2245
- if (dead) return;
2246
- }
2247
- },
2248
- dispatch(event) {
2249
- const field = byWidget.get(event.widget);
2250
- const widget = widgets.get(event.widget);
2251
- if (dead || !field || !widget) return;
2252
- const value = event.value ?? currentValue(form, field, widget);
2253
- apply(run(toSandboxEvent(event, value)), { values: true });
2254
- },
2255
- keystroke(widget, text, change, selStart, selEnd) {
2256
- const field = byWidget.get(widget);
2257
- if (dead || !field) return void 0;
2258
- const updates = run({
2259
- id: widget,
2260
- name: "Keystroke",
2261
- value: text,
2262
- change,
2263
- selStart,
2264
- selEnd,
2265
- willCommit: false
2266
- });
2267
- mirror.delete(field.name);
2268
- apply(updates, {
2269
- values: true,
2270
- source: {
2271
- field,
2272
- accepted: false
2273
- }
2274
- });
2275
- return keystrokeResult(updates, field, text, change, selStart, selEnd);
2276
- },
2277
- pageEvent(page, name) {
2278
- if (dead) return Promise.resolve();
2279
- pages = pages.then(async () => {
2280
- let actions = null;
2281
- if (name === "PageOpen" && !visited.has(page)) {
2282
- visited.add(page);
2283
- actions = await (await proxy.getPage(page + 1)).getJSActions();
2284
- }
2285
- if (dead) return;
2286
- apply(run({
2287
- id: "page",
2288
- name,
2289
- pageNumber: page + 1,
2290
- actions
2291
- }), { values: true });
2292
- }).catch(report);
2293
- return pages;
2294
- },
2295
- destroy() {
2296
- if (dead) return;
2297
- dead = true;
2298
- win.clear();
2299
- try {
2300
- sandbox.nukeSandbox();
2301
- } catch {}
2302
- }
2303
- };
2304
- if (opts.testing) scripting._sandboxForTesting = sandbox;
2305
- apply(run({
2306
- id: "doc",
2307
- name: "Open"
2308
- }), { values: true });
2309
- scripting.sync(form.values.get(), opts.calculateAtStart === true);
2310
- return scripting;
2311
- }
2312
- /** A scalar from the sandbox as text; anything else is empty. */
2313
- function textOf(v) {
2314
- if (typeof v === "string") return v;
2315
- return typeof v === "number" || typeof v === "boolean" ? String(v) : "";
2316
- }
2317
- /** How long the sandbox may take to load before the form goes on without scripts. */
2318
- const SANDBOX_TIMEOUT_MS = 1e4;
2319
- /**
2320
- * Rejects when `promise` takes longer than `ms`, so a stalled wasm load never holds up the
2321
- * document. A sandbox that arrives late is torn down.
2322
- */
2323
- function withTimeout(promise, ms) {
2324
- return new Promise((resolve, reject) => {
2325
- let late = false;
2326
- const timer = setTimeout(() => {
2327
- late = true;
2328
- reject(/* @__PURE__ */ new Error(`The PDF.js sandbox did not load within ${ms} ms`));
2329
- }, ms);
2330
- promise.then((sandbox) => {
2331
- clearTimeout(timer);
2332
- if (late) sandbox.nukeSandbox();
2333
- else resolve(sandbox);
2334
- }, (cause) => {
2335
- clearTimeout(timer);
2336
- reject(cause instanceof Error ? cause : new Error(String(cause)));
2337
- });
2338
- });
2339
- }
2340
- function hasEntries(value) {
2341
- if (value instanceof Map) return value.size > 0;
2342
- return typeof value === "object" && value !== null && Object.keys(value).length > 0;
2343
- }
2344
- /** The `window` the sandbox sees. */
2345
- function createWindow(form, onSend, report, isDead) {
2346
- const timers = /* @__PURE__ */ new Map();
2347
- const clear = (id) => {
2348
- const kind = timers.get(id);
2349
- if (!kind) return;
2350
- timers.delete(id);
2351
- if (kind === "interval") clearInterval(id);
2352
- else clearTimeout(id);
2353
- };
2354
- const noop = () => void 0;
2355
- class ShimEvent {
2356
- type;
2357
- detail;
2358
- constructor(type, init) {
2359
- this.type = type;
2360
- this.detail = init?.detail;
2361
- }
2362
- }
2363
- return {
2364
- shim: {
2365
- alert: (message) => {
2366
- if (!isDead()) form._alert(message);
2367
- },
2368
- confirm: () => false,
2369
- prompt: (message) => {
2370
- if (!isDead()) form._emitAction({
2371
- kind: "prompt",
2372
- message
2373
- });
2374
- return null;
2375
- },
2376
- setTimeout: (fn, ms) => {
2377
- if (isDead()) return 0;
2378
- const id = setTimeout(() => {
2379
- timers.delete(id);
2380
- if (!isDead()) fn();
2381
- }, ms);
2382
- timers.set(id, "timeout");
2383
- return id;
2384
- },
2385
- setInterval: (fn, ms) => {
2386
- if (isDead()) return 0;
2387
- const id = setInterval(() => {
2388
- if (!isDead()) fn();
2389
- }, ms);
2390
- timers.set(id, "interval");
2391
- return id;
2392
- },
2393
- clearTimeout: clear,
2394
- clearInterval: clear,
2395
- console: {
2396
- log: noop,
2397
- info: noop,
2398
- warn: noop,
2399
- debug: noop,
2400
- error: report
2401
- },
2402
- URL,
2403
- CustomEvent: ShimEvent,
2404
- dispatchEvent: (event) => {
2405
- if (event.type === "updatefromsandbox") onSend(event.detail);
2406
- return true;
2407
- }
2408
- },
2409
- clear() {
2410
- for (const id of [...timers.keys()]) clear(id);
2411
- }
2412
- };
2413
- }
2414
- /** The event that commits `value` the way a person would, or none for a field without one. */
2415
- function commitEvent(field, value) {
2416
- const first = field.widgets[0];
2417
- if (!first) return void 0;
2418
- switch (field.type) {
2419
- case "checkbox":
2420
- case "radio": {
2421
- const on = field.widgets.find((w) => w.onState !== void 0 && w.onState === value);
2422
- if (!on && (value !== "Off" || field.type === "radio")) return void 0;
2423
- return {
2424
- id: (on ?? first).id,
2425
- name: "Validate",
2426
- value: on !== void 0
2427
- };
2428
- }
2429
- case "text":
2430
- case "combo":
2431
- case "list": {
2432
- if (typeof value !== "string" && !Array.isArray(value)) return void 0;
2433
- const end = typeof value === "string" ? value.length : 0;
2434
- return {
2435
- id: first.id,
2436
- name: "Keystroke",
2437
- value,
2438
- change: "",
2439
- willCommit: true,
2440
- commitKey: 1,
2441
- selStart: end,
2442
- selEnd: end
2443
- };
2444
- }
2445
- default: return;
2446
- }
2447
- }
2448
- /** The value an event carries when the form layer gives none: what the widget shows. */
2449
- function currentValue(form, field, widget) {
2450
- const value = form.get(field.name);
2451
- switch (field.type) {
2452
- case "checkbox":
2453
- case "radio": return widget.onState !== void 0 && value === widget.onState;
2454
- case "text":
2455
- case "combo":
2456
- case "list": return typeof value === "string" || Array.isArray(value) ? value : "";
2457
- default: return;
2458
- }
2229
+ const StampSchema = z.object({
2230
+ ...Stamp.shape,
2231
+ ...Base
2232
+ });
2233
+ /**
2234
+ * A loupe (Inkbar's own type, a circle with `custom.inkbar.type: 'loupe'` in a file): a circle
2235
+ * inscribed in `rect` that shows the page content under its own centre, magnified. `color` and
2236
+ * `width` draw its rim.
2237
+ */
2238
+ const LoupeSchema = z.object({
2239
+ type: z.literal("loupe"),
2240
+ ...commonFields,
2241
+ /** How many times larger the content under the centre is drawn. Above 1. */
2242
+ magnification: z.number().gt(1).default(2),
2243
+ /** The rim's width in points. */
2244
+ width: z.number().nonnegative().default(2),
2245
+ ...Base
2246
+ });
2247
+ /**
2248
+ * A mask (Inkbar's own type, a circle or square with `custom.inkbar.type: 'mask'` in a file):
2249
+ * dims everything on the page outside `shape` drawn in `rect`, an ellipse inscribed in it or the
2250
+ * rectangle itself. `color` is the dimming colour and `dim` how strongly it covers, from 0 to 1.
2251
+ * One per page: adding a second replaces the first.
2252
+ */
2253
+ const MaskSchema = z.object({
2254
+ type: z.literal("mask"),
2255
+ ...commonFields,
2256
+ shape: z.enum(["rect", "ellipse"]),
2257
+ dim: z.number().min(0).max(1).default(.5),
2258
+ ...Base
2259
+ });
2260
+ const RedactSchema = z.object({
2261
+ ...Redact.shape,
2262
+ ...Base,
2263
+ /** Present when the mark came from text; absent for an area mark. */
2264
+ quads: Quads.optional(),
2265
+ /** Colour the redaction will be filled with when applied. */
2266
+ interiorColor: Color
2267
+ });
2268
+ const AnnotationSchema = z.discriminatedUnion("type", [
2269
+ TextMarkupSchema,
2270
+ NoteSchema,
2271
+ FreeTextSchema,
2272
+ SquareSchema,
2273
+ CircleSchema,
2274
+ LineSchema,
2275
+ PolylineSchema,
2276
+ PolygonSchema,
2277
+ InkSchema,
2278
+ StampSchema,
2279
+ LoupeSchema,
2280
+ MaskSchema,
2281
+ RedactSchema
2282
+ ]);
2283
+ /** The viewer's own state on an annotation, left out of exports. */
2284
+ const VIEWER_STATE = {
2285
+ source: true,
2286
+ locked: true,
2287
+ hasAppearance: true
2288
+ };
2289
+ /** One exported annotation: the stored annotation without its viewer state. */
2290
+ const ExportedAnnotationSchema = z.discriminatedUnion("type", [
2291
+ TextMarkupSchema.omit(VIEWER_STATE),
2292
+ NoteSchema.omit(VIEWER_STATE),
2293
+ FreeTextSchema.omit(VIEWER_STATE),
2294
+ SquareSchema.omit(VIEWER_STATE),
2295
+ CircleSchema.omit(VIEWER_STATE),
2296
+ LineSchema.omit(VIEWER_STATE),
2297
+ PolylineSchema.omit(VIEWER_STATE),
2298
+ PolygonSchema.omit(VIEWER_STATE),
2299
+ InkShape.omit(VIEWER_STATE).superRefine(checkPressure),
2300
+ StampSchema.omit(VIEWER_STATE),
2301
+ LoupeSchema.omit(VIEWER_STATE),
2302
+ MaskSchema.omit(VIEWER_STATE),
2303
+ RedactSchema.omit(VIEWER_STATE)
2304
+ ]);
2305
+ const AnnotationDocumentSchema = z.object({
2306
+ format: z.literal(GLOSS_FORMAT),
2307
+ version: z.literal(GLOSS_VERSION),
2308
+ documentKey: z.string().optional(),
2309
+ /** Validated item by item on import so unknown types can be preserved. */
2310
+ annotations: z.array(z.unknown()),
2311
+ /** Form field values by full name, validated field by field against the form on import. */
2312
+ fields: z.record(z.string(), z.unknown()).optional()
2313
+ });
2314
+ //#endregion
2315
+ //#region src/model/annotations/embedded.ts
2316
+ const SUPPORTED_SUBTYPES = /* @__PURE__ */ new Set([
2317
+ "Highlight",
2318
+ "Underline",
2319
+ "StrikeOut",
2320
+ "Squiggly",
2321
+ "Text",
2322
+ "FreeText",
2323
+ "Square",
2324
+ "Circle",
2325
+ "Line",
2326
+ "Polygon",
2327
+ "PolyLine",
2328
+ "Ink",
2329
+ "Redact"
2330
+ ]);
2331
+ /** Subtypes the document reads as links rather than annotations. */
2332
+ const LINK_SUBTYPES = /* @__PURE__ */ new Set(["Link"]);
2333
+ /** An RGB colour in 0–255 as upper-case `#RRGGBB`; `fallback` when it has fewer than three parts. */
2334
+ function pdfColorToHex(c, fallback) {
2335
+ if (!c || c.length < 3) return fallback;
2336
+ return `#${[
2337
+ c[0],
2338
+ c[1],
2339
+ c[2]
2340
+ ].map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
2459
2341
  }
2460
- function toSandboxEvent(event, value) {
2461
- const out = {
2462
- id: event.widget,
2463
- name: event.name
2464
- };
2465
- if (value !== void 0) out.value = value;
2466
- for (const key of [
2467
- "change",
2468
- "selStart",
2469
- "selEnd",
2470
- "willCommit",
2471
- "commitKey"
2472
- ]) if (event[key] !== void 0) out[key] = event[key];
2473
- return out;
2342
+ /** PDF.js hands back `rect` already in user space, but not always ordered; normalise with min/max. */
2343
+ function rectFromPdf(r) {
2344
+ return rectFromPoints({
2345
+ x: r[0],
2346
+ y: r[1]
2347
+ }, {
2348
+ x: r[2],
2349
+ y: r[3]
2350
+ });
2474
2351
  }
2475
- /** A value from the sandbox in the form's terms; `undefined` for a field without one. */
2476
- function fromSandbox(field, id, value) {
2477
- switch (field.type) {
2478
- case "checkbox":
2479
- case "radio":
2480
- if (value === "Off" || value === "" || value === null || value === void 0) return "Off";
2481
- return (field.widgets.find((w) => w.id === id && w.onState !== void 0) ?? field.widgets.find((w) => w.onState === value))?.onState ?? textOf(value);
2482
- case "list":
2483
- if (field.flags.multiSelect) return Array.isArray(value) ? value.map(textOf) : value === "" ? [] : [textOf(value)];
2484
- return Array.isArray(value) ? textOf(value[0]) : textOf(value);
2485
- case "text":
2486
- case "combo": return Array.isArray(value) ? textOf(value[0]) : textOf(value);
2487
- default: return;
2488
- }
2352
+ /** A PDF name exactly as PDF.js gives it (names are case-sensitive), or undefined if invalid. */
2353
+ function pdfName(raw) {
2354
+ return raw !== void 0 && PDF_NAME$1.test(raw) ? raw : void 0;
2489
2355
  }
2490
- /** `["RGB", r, g, b]`, `["G", g]`, `["CMYK", c, m, y, k]` or `["T"]` as `#rrggbb`. */
2491
- function colorOf(value) {
2492
- if (!Array.isArray(value) || typeof value[0] !== "string") return void 0;
2493
- const [space, ...raw] = value;
2494
- const n = raw.map((v) => typeof v === "number" ? Math.min(1, Math.max(0, v)) : 0);
2495
- let rgb;
2496
- if (space === "T") return "transparent";
2497
- if (space === "G") rgb = [
2498
- n[0] ?? 0,
2499
- n[0] ?? 0,
2500
- n[0] ?? 0
2501
- ];
2502
- else if (space === "RGB") rgb = [
2503
- n[0] ?? 0,
2504
- n[1] ?? 0,
2505
- n[2] ?? 0
2506
- ];
2507
- else if (space === "CMYK") {
2508
- const k = n[3] ?? 0;
2509
- rgb = [
2510
- n[0] ?? 0,
2511
- n[1] ?? 0,
2512
- n[2] ?? 0
2513
- ].map((c) => (1 - c) * (1 - k));
2514
- } else return void 0;
2515
- return `#${rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join("")}`;
2356
+ /** `head` and `tail` from a Line's or PolyLine's `/LE`, left out when both are `None`. */
2357
+ function lineEndingsOf(raw) {
2358
+ const endings = raw?.map((e) => pdfName(e) ?? "None");
2359
+ return endings && endings.length === 2 && endings.some((e) => e !== "None") ? {
2360
+ head: endings[0],
2361
+ tail: endings[1]
2362
+ } : {};
2516
2363
  }
2517
- function overrideOf(u) {
2518
- const o = {};
2519
- if (typeof u.hidden === "boolean") o.hidden = u.hidden;
2520
- if (typeof u.display === "number") o.hidden = u.display % 2 === 1;
2521
- if (typeof u.readonly === "boolean") o.readOnly = u.readonly;
2522
- if (typeof u.required === "boolean") o.required = u.required;
2523
- const text = colorOf(u.textColor ?? u.fgColor);
2524
- const fill = colorOf(u.fillColor ?? u.bgColor);
2525
- const stroke = colorOf(u.strokeColor ?? u.borderColor);
2526
- if (text) o.textColor = text;
2527
- if (fill) o.fillColor = fill;
2528
- if (stroke) o.strokeColor = stroke;
2529
- if (typeof u.textSize === "number" && Number.isFinite(u.textSize) && u.textSize >= 0) o.textSize = u.textSize;
2530
- return Object.keys(o).length > 0 ? o : void 0;
2364
+ /**
2365
+ * Imported ids are `pdf:<page>:<pdfjsId>` (page is 0-based, pdfjsId is the annotation's object
2366
+ * reference such as `12R`). They are stable across opens of the same file, and `inReplyTo`
2367
+ * uses the same scheme so threads resolve.
2368
+ */
2369
+ function importedId(page, pdfjsId) {
2370
+ return `pdf:${page}:${pdfjsId}`;
2531
2371
  }
2532
- function keystrokeResult(updates, field, text, change, selStart, selEnd) {
2533
- const merged = text.slice(0, selStart) + change + text.slice(selEnd);
2534
- const caret = selStart + change.length;
2535
- const last = updates.filter((u) => {
2536
- const id = typeof u.id === "string" ? u.id : void 0;
2537
- return id !== void 0 && field.widgets.some((w) => w.id === id) && "value" in u;
2538
- }).at(-1);
2539
- if (!last) return {
2540
- accepted: true,
2541
- value: merged,
2542
- selStart: caret,
2543
- selEnd: caret
2544
- };
2545
- const value = textOf(last.value);
2546
- const accepted = !(value === text && merged !== text);
2547
- const range = Array.isArray(last.selRange) ? last.selRange : [];
2548
- const start = typeof range[0] === "number" ? range[0] : caret;
2549
- const end = typeof range[1] === "number" ? range[1] : start;
2550
- return accepted ? {
2551
- accepted,
2552
- value,
2553
- selStart: start,
2554
- selEnd: end
2555
- } : {
2556
- accepted,
2557
- value: text,
2558
- selStart,
2559
- selEnd
2372
+ /**
2373
+ * Imports one page's PDF.js annotation data. `targets` is @internal: the annotations whose
2374
+ * missing fields the document reads from the file after `ready`.
2375
+ */
2376
+ function importEmbeddedPage(page, raw, importedAt) {
2377
+ const counts = {};
2378
+ const annotations = [];
2379
+ const targets = [];
2380
+ for (const data of raw) {
2381
+ counts[data.subtype] = (counts[data.subtype] ?? 0) + 1;
2382
+ if (!SUPPORTED_SUBTYPES.has(data.subtype) || data.hidden || data.noView) continue;
2383
+ const converted = convert(page, data, importedAt);
2384
+ if (!converted) continue;
2385
+ annotations.push(converted);
2386
+ const target = targetOf$1(converted.id, data);
2387
+ if (target) targets.push(target);
2388
+ }
2389
+ return {
2390
+ annotations,
2391
+ counts,
2392
+ targets
2560
2393
  };
2561
2394
  }
2562
- /** Maps a rewritten method's call (positional or Acrobat's single-object form) to an action. */
2563
- function scriptAction$1(method, args, form) {
2564
- const list = Array.isArray(args) ? args : [];
2565
- const first = list[0];
2566
- const named = typeof first === "object" && first !== null && !Array.isArray(first) ? first : void 0;
2567
- const arg = (key, index) => named ? named[key] : list[index];
2568
- const str = (key, index) => {
2569
- const v = arg(key, index);
2570
- return typeof v === "string" ? v : void 0;
2395
+ function targetOf$1(id, d) {
2396
+ const rect = Array.from(d.rect);
2397
+ if (d.subtype === "Text") return d.name === "NoIcon" ? {
2398
+ id,
2399
+ subtype: "Text",
2400
+ rect
2401
+ } : void 0;
2402
+ if (d.subtype !== "FreeText") return void 0;
2403
+ const daFont = d.defaultAppearanceData?.fontName;
2404
+ return {
2405
+ id,
2406
+ subtype: "FreeText",
2407
+ rect,
2408
+ ...daFont ? { daFont } : {}
2571
2409
  };
2572
- switch (method) {
2573
- case "submitForm": {
2574
- const url = safeUrl(str("cURL", 0) ?? "");
2575
- if (!url) return void 0;
2576
- const format = submitFormat(str("cSubmitAs", 15), arg("bPDF", 8), arg("bXML", 6), arg("bFDF", 1));
2577
- const raw = arg("aFields", 3);
2578
- const names = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).filter((n) => typeof n === "string");
2579
- return {
2580
- kind: "submit",
2581
- url,
2582
- fields: submitValues(form.values.get(), form.fields.get(), names, true),
2583
- format,
2584
- ...format === "html" ? { method: arg("bGet", 4) === true ? "GET" : "POST" } : {}
2410
+ }
2411
+ function summarise(perPage) {
2412
+ const totals = /* @__PURE__ */ new Map();
2413
+ for (const counts of perPage) for (const [k, v] of Object.entries(counts)) totals.set(k, (totals.get(k) ?? 0) + v);
2414
+ return [...totals.entries()].map(([subtype, count]) => ({
2415
+ subtype,
2416
+ count,
2417
+ supported: SUPPORTED_SUBTYPES.has(subtype) || LINK_SUBTYPES.has(subtype)
2418
+ })).sort((a, b) => a.subtype < b.subtype ? -1 : a.subtype > b.subtype ? 1 : 0);
2419
+ }
2420
+ function convert(page, d, importedAt) {
2421
+ const created = parsePdfDate(d.creationDate, parsePdfDate(d.modificationDate, importedAt));
2422
+ const author = d.titleObj?.str;
2423
+ const contents = d.contentsObj?.str;
2424
+ const stroke = d.borderStyle?.width ?? 1;
2425
+ const quads = d.quadPoints ? quadsFromFlat(d.quadPoints) : [];
2426
+ const optional = (v) => v !== void 0 && v !== "";
2427
+ const withColor = (type, source = d.color) => pdfColorToHex(source, staticDefaults.color(type));
2428
+ const intent = pdfName(d.it);
2429
+ const base = (type, color = withColor(type)) => ({
2430
+ id: importedId(page, d.id),
2431
+ page,
2432
+ rect: rectFromPdf(d.rect),
2433
+ color,
2434
+ opacity: d.opacity ?? 1,
2435
+ ...optional(author) ? { author } : {},
2436
+ ...optional(contents) ? { contents } : {},
2437
+ ...d.inReplyTo ? { inReplyTo: importedId(page, d.inReplyTo) } : {},
2438
+ ...intent !== void 0 ? { intent } : {},
2439
+ createdAt: created,
2440
+ modifiedAt: parsePdfDate(d.modificationDate, created),
2441
+ source: "pdf",
2442
+ locked: true,
2443
+ hasAppearance: d.hasAppearance === true
2444
+ });
2445
+ const fillOf = (c) => c && c.length >= 3 ? { interiorColor: pdfColorToHex(c, "#FFFFFF") } : {};
2446
+ let candidate;
2447
+ switch (d.subtype) {
2448
+ case "Highlight":
2449
+ case "Underline":
2450
+ case "StrikeOut":
2451
+ case "Squiggly": {
2452
+ if (quads.length === 0) return void 0;
2453
+ const type = d.subtype === "StrikeOut" ? "strikeout" : d.subtype.toLowerCase();
2454
+ candidate = {
2455
+ ...base(type),
2456
+ type,
2457
+ quads
2585
2458
  };
2459
+ break;
2586
2460
  }
2587
- case "mailDoc":
2588
- case "mailForm": {
2589
- const out = {
2590
- kind: "mail",
2591
- withData: method === "mailForm"
2461
+ case "Text": {
2462
+ const icon = d.name === "NoIcon" ? void 0 : pdfName(d.name);
2463
+ candidate = {
2464
+ ...base("text"),
2465
+ type: "text",
2466
+ ...icon !== void 0 ? { icon } : {}
2592
2467
  };
2593
- const to = str("cTo", 1);
2594
- const cc = str("cCc", 2);
2595
- const bcc = str("cBcc", 3);
2596
- const subject = str("cSubject", 4);
2597
- const body = str("cMsg", 5);
2598
- for (const address of [
2599
- to,
2600
- cc,
2601
- bcc
2602
- ]) if (address !== void 0 && safeAddress(address) === void 0) return void 0;
2603
- if (to !== void 0) out.to = to;
2604
- if (cc !== void 0) out.cc = cc;
2605
- if (bcc !== void 0) out.bcc = bcc;
2606
- if (subject !== void 0) out.subject = subject;
2607
- if (body !== void 0) out.body = body;
2608
- return out;
2468
+ break;
2609
2469
  }
2610
- case "launchURL": {
2611
- const url = safeUrl(str("cURL", 0) ?? "");
2612
- return url ? {
2613
- kind: "open-url",
2614
- url,
2615
- newWindow: arg("bNewFrame", 1) === true
2616
- } : void 0;
2470
+ case "FreeText": {
2471
+ const da = d.defaultAppearanceData;
2472
+ const { color: _stroke, ...rest } = base("freetext");
2473
+ candidate = {
2474
+ ...rest,
2475
+ ...d.color && d.color.length >= 3 ? { color: pdfColorToHex(d.color, "#FFFFFF") } : {},
2476
+ type: "freetext",
2477
+ contents: contents ?? "",
2478
+ width: d.borderStyle?.width ?? 0,
2479
+ defaultStyle: {
2480
+ color: pdfColorToHex(da?.fontColor, staticDefaults.color("freetext")),
2481
+ ...da && da.fontSize > 0 ? { fontSize: da.fontSize } : {}
2482
+ }
2483
+ };
2484
+ break;
2485
+ }
2486
+ case "Square":
2487
+ case "Circle": {
2488
+ const type = d.subtype.toLowerCase();
2489
+ candidate = {
2490
+ ...base(type),
2491
+ type,
2492
+ width: stroke,
2493
+ ...fillOf(d.interiorColor)
2494
+ };
2495
+ break;
2496
+ }
2497
+ case "Line": {
2498
+ const l = d.lineCoordinates ?? [
2499
+ d.rect[0],
2500
+ d.rect[1],
2501
+ d.rect[2],
2502
+ d.rect[3]
2503
+ ];
2504
+ candidate = {
2505
+ ...base("line"),
2506
+ type: "line",
2507
+ start: {
2508
+ x: l[0],
2509
+ y: l[1]
2510
+ },
2511
+ end: {
2512
+ x: l[2],
2513
+ y: l[3]
2514
+ },
2515
+ width: stroke,
2516
+ ...lineEndingsOf(d.lineEndings)
2517
+ };
2518
+ break;
2519
+ }
2520
+ case "Polygon":
2521
+ case "PolyLine": {
2522
+ const type = d.subtype === "Polygon" ? "polygon" : "polyline";
2523
+ const vertices = d.vertices ? pointsFromFlat(d.vertices) : [];
2524
+ if (vertices.length < 2) return void 0;
2525
+ candidate = {
2526
+ ...base(type),
2527
+ type,
2528
+ vertices,
2529
+ width: stroke,
2530
+ ...fillOf(d.interiorColor),
2531
+ ...type === "polyline" ? lineEndingsOf(d.lineEndings) : {}
2532
+ };
2533
+ break;
2534
+ }
2535
+ case "Ink": {
2536
+ const inklist = (d.inkLists ?? []).map((p) => pointsFromFlat(p)).filter((p) => p.length > 0);
2537
+ if (inklist.length === 0) return void 0;
2538
+ candidate = {
2539
+ ...base("ink"),
2540
+ type: "ink",
2541
+ inklist,
2542
+ width: stroke
2543
+ };
2544
+ break;
2617
2545
  }
2546
+ case "Redact":
2547
+ candidate = {
2548
+ ...base("redact"),
2549
+ type: "redact",
2550
+ interiorColor: pdfColorToHex(d.interiorColor, staticDefaults.fill()),
2551
+ ...quads.length > 0 ? { quads } : {}
2552
+ };
2553
+ break;
2618
2554
  default: return;
2619
2555
  }
2556
+ const parsed = AnnotationSchema.safeParse(candidate);
2557
+ return parsed.success ? parsed.data : void 0;
2620
2558
  }
2621
- /** `cSubmitAs` wins, then `bPDF`, then `bXML`; otherwise FDF, or HTML when `bFDF` is false. */
2622
- function submitFormat(submitAs, pdf, xml, fdf) {
2623
- switch (submitAs?.toUpperCase()) {
2624
- case "FDF": return "fdf";
2625
- case "XFDF":
2626
- case "XFD":
2627
- case "XML": return "xfdf";
2628
- case "HTML": return "html";
2629
- case "PDF": return "pdf";
2630
- }
2631
- if (pdf === true) return "pdf";
2632
- if (xml === true) return "xfdf";
2633
- return fdf === false ? "html" : "fdf";
2559
+ //#endregion
2560
+ //#region src/model/destination.ts
2561
+ const NAMED_ACTIONS = [
2562
+ "NextPage",
2563
+ "PrevPage",
2564
+ "FirstPage",
2565
+ "LastPage"
2566
+ ];
2567
+ /** Whether `s` is one of the four page-navigation actions Inkbar follows. */
2568
+ function isNamedAction(s) {
2569
+ return typeof s === "string" && NAMED_ACTIONS.includes(s);
2570
+ }
2571
+ //#endregion
2572
+ //#region src/model/form/url.ts
2573
+ /** The schemes a form may hand the host, as PDF.js allows for `/URI`. */
2574
+ const SAFE = /* @__PURE__ */ new Set([
2575
+ "http:",
2576
+ "https:",
2577
+ "mailto:",
2578
+ "tel:"
2579
+ ]);
2580
+ /**
2581
+ * `url` when it is absolute and uses http, https, mailto or tel; otherwise `undefined`, so a
2582
+ * `javascript:`, `data:`, `file:` or relative URL from a PDF never reaches the host.
2583
+ */
2584
+ function safeUrl(url) {
2585
+ try {
2586
+ return SAFE.has(new URL(url).protocol) ? url : void 0;
2587
+ } catch {
2588
+ return;
2589
+ }
2634
2590
  }
2635
- //#endregion
2636
- //#region src/pdfjs/structure.ts
2637
- /** The prefix PDF.js gives an annotation's id in its structure tree. */
2638
- const ANNOTATION_PREFIX = "pdfjs_internal_id_";
2639
2591
  /**
2640
- * The page's annotations in structure order, by PDF.js annotation id (`12R`): the order of the
2641
- * OBJR references in the page's part of the structure tree, depth first. Empty when the file has
2642
- * no structure tree or PDF.js cannot read it.
2592
+ * A mail address, kept unless it reads as a URL with another scheme (a script passing
2593
+ * `javascript:…` as a recipient). A `mailto:` or `tel:` prefix is kept as it was given, not
2594
+ * stripped, so the host may see `mailto:a@example.com` as a recipient.
2643
2595
  */
2644
- async function structureAnnotationOrder(page) {
2645
- let root;
2596
+ function safeAddress(address) {
2646
2597
  try {
2647
- root = await page.getStructTree();
2598
+ return SAFE.has(new URL(address).protocol) ? address : void 0;
2648
2599
  } catch {
2649
- return [];
2600
+ return address;
2650
2601
  }
2651
- const out = [];
2652
- const seen = /* @__PURE__ */ new Set();
2653
- const walk = (node, depth) => {
2654
- if (depth > 64 || typeof node !== "object" || node === null) return;
2655
- const n = node;
2656
- if (n.type === "annotation" && typeof n.id === "string") {
2657
- const id = n.id.startsWith(ANNOTATION_PREFIX) ? n.id.slice(18) : n.id;
2658
- if (!seen.has(id)) {
2659
- seen.add(id);
2660
- out.push(id);
2661
- }
2662
- }
2663
- if (Array.isArray(n.children)) for (const kid of n.children) walk(kid, depth + 1);
2664
- };
2665
- walk(root, 0);
2666
- return out;
2667
2602
  }
2668
2603
  //#endregion
2669
- //#region src/model/annotations/defaults.ts
2670
- const COLORS = {
2671
- highlight: "#F2C230",
2672
- underline: "#2B5A9E",
2673
- strikeout: "#C0392B",
2674
- squiggly: "#1F7A4D",
2675
- text: "#F2C230",
2676
- freetext: "#151820",
2677
- square: "#C0392B",
2678
- circle: "#C0392B",
2679
- line: "#151820",
2680
- polyline: "#151820",
2681
- polygon: "#151820",
2682
- ink: "#151820",
2683
- stamp: "#151820",
2684
- loupe: "#151820",
2685
- mask: "#000000",
2686
- redact: "#C0392B"
2687
- };
2688
- const staticDefaults = {
2689
- color: (type) => COLORS[type],
2690
- fill: () => "#000000",
2691
- opacity: (type) => type === "highlight" ? .4 : 1
2692
- };
2693
- const VARIABLES = {
2694
- highlight: "--ink-highlight",
2695
- underline: "--ink-underline",
2696
- strikeout: "--ink-strikeout",
2697
- squiggly: "--ink-squiggly",
2698
- text: "--ink-note",
2699
- freetext: "--ink-free-text",
2700
- square: "--ink-shape",
2701
- circle: "--ink-shape",
2702
- line: "--ink-shape",
2703
- polyline: "--ink-shape",
2704
- polygon: "--ink-shape",
2705
- ink: "--ink-ink",
2706
- stamp: "--ink-ink",
2707
- loupe: "--ink-shape",
2708
- mask: "--ink-mask",
2709
- redact: "--ink-redact"
2710
- };
2711
- /** Resolves default colours from CSS custom properties on `el`, falling back to `staticDefaults`. */
2712
- function cssVariableDefaults(el) {
2713
- const read = (name) => {
2714
- const view = el.ownerDocument.defaultView;
2715
- if (!view) return void 0;
2716
- return normalizeColor(view.getComputedStyle(el).getPropertyValue(name));
2604
+ //#region src/model/links.ts
2605
+ /** A URL with a scheme of its own, as opposed to a file name or relative path. */
2606
+ const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
2607
+ /**
2608
+ * Why PDF.js's `unsafeUrl` was not passed on. PDF.js reads a GoToR's file and destination into
2609
+ * `file#dest`, and a Launch's file as a bare name, so those two are told apart by the `#`; a
2610
+ * scheme `safeUrl` refuses (`javascript:`, `file:`, `data:`) is unsafe whatever follows it.
2611
+ */
2612
+ function unsafeReason(unsafeUrl) {
2613
+ if (HAS_SCHEME.test(unsafeUrl)) return "unsafe-url";
2614
+ return unsafeUrl.includes("#") ? "remote" : "launch";
2615
+ }
2616
+ /**
2617
+ * Where a link or outline item goes, from what PDF.js read of its `/Dest` or `/A`. A
2618
+ * destination stays raw (`dest`); a URL is passed only through `safeUrl`.
2619
+ */
2620
+ function targetOf(data) {
2621
+ if (data.dest !== void 0 && data.dest !== null) return {
2622
+ kind: "dest",
2623
+ raw: data.dest
2624
+ };
2625
+ if (isNamedAction(data.action)) return {
2626
+ kind: "named",
2627
+ action: data.action
2628
+ };
2629
+ if (data.url) {
2630
+ const url = safeUrl(data.url);
2631
+ if (url) return {
2632
+ kind: "url",
2633
+ url,
2634
+ newWindow: !!data.newWindow
2635
+ };
2636
+ }
2637
+ if (data.unsafeUrl) return safeUrl(data.unsafeUrl) ? {
2638
+ kind: "unsupported",
2639
+ reason: "unknown"
2640
+ } : {
2641
+ kind: "unsupported",
2642
+ reason: unsafeReason(data.unsafeUrl)
2717
2643
  };
2718
2644
  return {
2719
- color: (type) => read(VARIABLES[type]) ?? staticDefaults.color(type),
2720
- fill: () => read("--ink-redact-fill") ?? staticDefaults.fill(),
2721
- opacity: (type) => staticDefaults.opacity(type)
2645
+ kind: "unsupported",
2646
+ reason: "unknown"
2722
2647
  };
2723
2648
  }
2724
- /** Accepts `#rgb`, `#rrggbb`, `rgb(r, g, b)` and `rgb(r g b)`; returns `#RRGGBB` or undefined. */
2725
- function normalizeColor(value) {
2726
- const v = value.trim();
2727
- const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(v);
2728
- if (short) {
2729
- const [, r = "", g = "", b = ""] = short;
2730
- return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
2649
+ /**
2650
+ * A link annotation read from PDF.js, or `undefined` for anything that is not a visible link.
2651
+ * Links are never Gloss annotations: they are read-only structure of the file. `order` is its
2652
+ * position in the page's `/Annots`.
2653
+ */
2654
+ function linkFromPdfjs(page, data, order) {
2655
+ if (data.subtype !== "Link" || data.hidden || data.noView) return void 0;
2656
+ const quads = data.quadPoints ? quadsFromFlat(data.quadPoints) : [];
2657
+ return {
2658
+ id: importedId(page, data.id),
2659
+ page,
2660
+ rect: rectFromPoints({
2661
+ x: data.rect[0],
2662
+ y: data.rect[1]
2663
+ }, {
2664
+ x: data.rect[2],
2665
+ y: data.rect[3]
2666
+ }),
2667
+ ...quads.length > 0 ? { quads } : {},
2668
+ ...order === void 0 ? {} : { order },
2669
+ target: targetOf(data)
2670
+ };
2671
+ }
2672
+ //#endregion
2673
+ //#region src/model/map-in-order.ts
2674
+ /** Runs `task(0..count-1)` with at most `limit` in flight; results come back in index order. */
2675
+ async function mapInOrder(count, limit, task) {
2676
+ const results = new Array(count);
2677
+ let next = 0;
2678
+ const worker = async () => {
2679
+ while (next < count) {
2680
+ const i = next++;
2681
+ results[i] = await task(i);
2682
+ }
2683
+ };
2684
+ await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
2685
+ return results;
2686
+ }
2687
+ //#endregion
2688
+ //#region src/pdfjs/outline.ts
2689
+ /** Destinations looked up at once. A long outline names many, each a request to the worker. */
2690
+ const CONCURRENCY = 8;
2691
+ /**
2692
+ * The document's outline, every destination resolved in full: the current item needs each
2693
+ * one's position. `[]` when the file has none or PDF.js cannot read it.
2694
+ */
2695
+ async function readOutline(proxy) {
2696
+ const roots = await proxy.getOutline().catch(() => null);
2697
+ if (!roots || roots.length === 0) return [];
2698
+ const raws = [];
2699
+ const collect = (nodes) => {
2700
+ for (const node of nodes) {
2701
+ const target = targetOf(node);
2702
+ if (target.kind === "dest") raws.push(target.raw);
2703
+ collect(node.items ?? []);
2704
+ }
2705
+ };
2706
+ collect(roots);
2707
+ const dests = await mapInOrder(raws.length, CONCURRENCY, (i) => resolveDestination(proxy, raws[i]));
2708
+ let next = 0;
2709
+ const build = (nodes, path) => nodes.map((node, i) => {
2710
+ const id = path === "" ? String(i) : `${path}.${i}`;
2711
+ let target = targetOf(node);
2712
+ if (target.kind === "dest") {
2713
+ const dest = dests[next++];
2714
+ target = dest ? {
2715
+ kind: "resolved",
2716
+ dest
2717
+ } : {
2718
+ kind: "unsupported",
2719
+ reason: "unknown"
2720
+ };
2721
+ }
2722
+ return {
2723
+ id,
2724
+ title: String(node.title ?? "").replace(/\s+/g, " ").trim(),
2725
+ target,
2726
+ bold: !!node.bold,
2727
+ italic: !!node.italic,
2728
+ open: (node.count ?? 0) > 0,
2729
+ children: build(node.items ?? [], id)
2730
+ };
2731
+ });
2732
+ return build(roots, "");
2733
+ }
2734
+ //#endregion
2735
+ //#region src/model/form/values.ts
2736
+ /**
2737
+ * Why `value` cannot be the field's value, or `undefined` when it can. A locked or read-only
2738
+ * field is `readOnly`; otherwise the value must suit the type: any text within `maxLength`, an
2739
+ * option of a non-editable choice, an array of options for a multi-select list, an on state or
2740
+ * `Off` for a checkbox or radio, and `{ signature }` for a signature field. A button has no value.
2741
+ */
2742
+ function checkValue(field, value) {
2743
+ if (field.locked || field.flags.readOnly) return "readOnly";
2744
+ return suits(field, value) ? void 0 : "invalid";
2745
+ }
2746
+ /** Whether `value` suits the field's type, whatever its flags. */
2747
+ function suits(field, value) {
2748
+ const isOption = (v) => field.options.some((o) => o.value === v);
2749
+ switch (field.type) {
2750
+ case "text": return typeof value === "string" && (field.maxLength === void 0 || value.length <= field.maxLength);
2751
+ case "combo": return typeof value === "string" && (field.flags.editable || isOption(value));
2752
+ case "list": return field.flags.multiSelect ? Array.isArray(value) && value.every(isOption) : typeof value === "string" && isOption(value);
2753
+ case "checkbox":
2754
+ case "radio": return value === "Off" || typeof value === "string" && (isOption(value) || field.widgets.some((w) => w.onState === value));
2755
+ case "signature": return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.signature === "string" && value.signature !== "";
2756
+ case "button": return false;
2731
2757
  }
2732
- if (/^#[0-9a-f]{6}$/i.test(v)) return v.toUpperCase();
2733
- const rgb = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/i.exec(v);
2734
- if (rgb) return `#${[
2735
- rgb[1],
2736
- rgb[2],
2737
- rgb[3]
2738
- ].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
2758
+ }
2759
+ /**
2760
+ * A checked value as it is stored: frozen, a multi-select's options in option order without
2761
+ * repeats, and a signature with only its id.
2762
+ */
2763
+ function normaliseValue(field, value) {
2764
+ if (Array.isArray(value)) {
2765
+ const chosen = new Set(value);
2766
+ return Object.freeze(field.options.map((o) => o.value).filter((v) => chosen.has(v) && chosen.delete(v)));
2767
+ }
2768
+ if (typeof value === "object") return Object.freeze({ signature: value.signature });
2769
+ return value;
2770
+ }
2771
+ /** The value a reset gives: the PDF's default, else empty for the type, else none. */
2772
+ function resetValue(field) {
2773
+ if (field.defaultValue !== void 0) return field.defaultValue;
2774
+ switch (field.type) {
2775
+ case "text":
2776
+ case "combo": return "";
2777
+ case "checkbox":
2778
+ case "radio": return "Off";
2779
+ case "list": return field.flags.multiSelect ? Object.freeze([]) : void 0;
2780
+ default: return;
2781
+ }
2782
+ }
2783
+ function sameValue$1(a, b) {
2784
+ if (a === b) return true;
2785
+ if (a === void 0 || b === void 0) return false;
2786
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => v === b[i]);
2787
+ return typeof a === "object" && typeof b === "object" && a.signature === b.signature;
2739
2788
  }
2740
2789
  //#endregion
2741
- //#region src/model/pdf-date.ts
2742
- /** Parses a PDF date string (`D:YYYYMMDDHHmmSSOHH'mm'`) into ISO 8601. Returns `fallback` on anything unparseable. */
2743
- function parsePdfDate(value, fallback) {
2744
- if (!value) return fallback;
2745
- const m = /^(?:D:)?(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(Z|[+-]\d{2}(?:'\d{2}')?)?/.exec(value.trim());
2746
- if (!m) return fallback;
2747
- const [, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", zone] = m;
2748
- let offsetMinutes = 0;
2749
- if (zone && zone !== "Z") {
2750
- const sign = zone.startsWith("-") ? -1 : 1;
2751
- const zh = Number(zone.slice(1, 3));
2752
- const zm = Number(zone.slice(4, 6) || 0);
2753
- offsetMinutes = sign * (zh * 60 + zm);
2790
+ //#region src/model/form/gloss.ts
2791
+ /** A field value as Gloss writes it: text, the options of a multi-select, or a signature id. */
2792
+ const GlossFieldValueSchema = z.union([
2793
+ z.string(),
2794
+ z.array(z.string()),
2795
+ z.looseObject({ signature: z.string().min(1) })
2796
+ ]);
2797
+ z.record(z.string().min(1), GlossFieldValueSchema);
2798
+ /**
2799
+ * The values to export: every field that has one and is not no-export, in field order, with a
2800
+ * multi-select's options in option order.
2801
+ */
2802
+ function fieldsToGloss(values, fields) {
2803
+ const out = {};
2804
+ for (const field of fields) {
2805
+ const value = values[field.name];
2806
+ if (value === void 0 || field.flags.noExport) continue;
2807
+ out[field.name] = normaliseValue(field, value);
2808
+ }
2809
+ return out;
2810
+ }
2811
+ /**
2812
+ * The values of a Gloss `fields` map that suit the form, and the names that do not: unknown,
2813
+ * read-only (or locked), or invalid for the field. Anything other than a map applies nothing.
2814
+ */
2815
+ function fieldsFromGloss(raw, fields) {
2816
+ const values = {};
2817
+ const rejected = [];
2818
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
2819
+ values,
2820
+ rejected
2821
+ };
2822
+ const byName = new Map(fields.map((f) => [f.name, f]));
2823
+ for (const [name, value] of Object.entries(raw)) {
2824
+ const field = byName.get(name);
2825
+ if (!field) {
2826
+ rejected.push({
2827
+ name,
2828
+ reason: "unknown"
2829
+ });
2830
+ continue;
2831
+ }
2832
+ const parsed = GlossFieldValueSchema.safeParse(value);
2833
+ const reason = checkValue(field, value) ?? (parsed.success ? void 0 : "invalid");
2834
+ if (reason || !parsed.success) {
2835
+ rejected.push({
2836
+ name,
2837
+ reason: reason ?? "invalid"
2838
+ });
2839
+ continue;
2840
+ }
2841
+ values[name] = normaliseValue(field, parsed.data);
2754
2842
  }
2755
- const utc = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s)) - offsetMinutes * 6e4;
2756
- if (Number.isNaN(utc)) return fallback;
2757
- return new Date(utc).toISOString();
2843
+ return {
2844
+ values,
2845
+ rejected
2846
+ };
2758
2847
  }
2759
2848
  //#endregion
2760
- //#region src/model/annotations/callout.ts
2849
+ //#region src/model/form/submit.ts
2761
2850
  /**
2762
- * The insets from a free text's `rect` to its text box, in points (PDF `/RD`, XFDF `fringe`,
2763
- * both in this order).
2851
+ * The values a submit sends: every field with a value that is not no-export, limited to
2852
+ * `names` (or everything but them, when `include` is false). A name covers the fields under it.
2853
+ * An empty list means every field.
2764
2854
  */
2765
- const FringeSchema = z.object(Fringe.shape);
2766
- /** An ending's length, and its full width, as a multiple of the stroke width. */
2767
- const ENDING_SCALE = 3;
2855
+ function submitValues(values, fields, names, include) {
2856
+ const all = fieldsToGloss(values, fields);
2857
+ if (names.length === 0) return Object.freeze(all);
2858
+ const listed = (name) => names.some((n) => name === n || name.startsWith(`${n}.`));
2859
+ const out = {};
2860
+ for (const [name, value] of Object.entries(all)) if (listed(name) === include) out[name] = value;
2861
+ return Object.freeze(out);
2862
+ }
2863
+ //#endregion
2864
+ //#region src/pdfjs/sandbox-patch.ts
2768
2865
  /**
2769
- * A free text's text box: `rect` inset by `fringe`, or `rect` itself without one. Insets wider
2770
- * or taller than `rect` leave a box of width or height 0 at the inset origin.
2866
+ * PDF.js's scripting API defines `submitForm`, `mailDoc`, `mailForm` (on Doc) and `launchURL`
2867
+ * (on App) as empty methods, so a script calling them is silent. They cannot be replaced from a
2868
+ * script run inside the sandbox: scripts run as global code, where `this` is the global object,
2869
+ * and PDF.js copies each Doc method onto it as a bound, non-writable, non-configurable property
2870
+ * before any script runs. So the bridge rewrites the four empty bodies in the scripting source
2871
+ * before the sandbox evaluates it. Each replacement reports the call through the sandbox's own
2872
+ * `send` channel, with a command name only Inkbar uses; the host maps the arguments.
2771
2873
  */
2772
- function freeTextBox(a) {
2773
- const { rect, fringe } = a;
2774
- if (!fringe) return rect;
2775
- return {
2776
- x: rect.x + fringe.left,
2777
- y: rect.y + fringe.bottom,
2778
- width: Math.max(0, rect.width - fringe.left - fringe.right),
2779
- height: Math.max(0, rect.height - fringe.top - fringe.bottom)
2780
- };
2874
+ /** The command a rewritten method sends. */
2875
+ const ACTION_COMMAND = "inkbar:action";
2876
+ const METHODS = [
2877
+ "submitForm",
2878
+ "mailDoc",
2879
+ "mailForm",
2880
+ "launchURL"
2881
+ ];
2882
+ /** The empty method as it appears in PDF.js's scripting source, one class member per line. */
2883
+ const stub = (method) => `\n ${method}() {}\n`;
2884
+ const replacement = (method) => `\n ${method}(...args) { this._send({ command: "${ACTION_COMMAND}", method: "${method}", args }); }\n`;
2885
+ /** How many times each empty method appears in `source`. */
2886
+ function countStubs(source) {
2887
+ const counts = {};
2888
+ for (const m of METHODS) counts[m] = source.split(stub(m)).length - 1;
2889
+ return counts;
2781
2890
  }
2782
2891
  /**
2783
- * True for a free-text callout drawn the standard way: a box with a leader line through
2784
- * `callout`. A callout with `cornerRadius` is Inkbar's speech bubble instead.
2892
+ * Rewrites the four empty methods. Throws when any of them does not appear exactly once, which
2893
+ * means the PDF.js build differs from the one this was written against.
2785
2894
  */
2786
- function isStandardCallout(a) {
2787
- return a.type === "freetext" && a.intent === "FreeTextCallout" && a.callout !== void 0 && a.callout.length >= 2 && a.cornerRadius === void 0;
2895
+ function patchScriptingSource(source) {
2896
+ const counts = countStubs(source);
2897
+ const off = METHODS.filter((m) => counts[m] !== 1);
2898
+ if (off.length > 0) throw new Error(`The PDF.js scripting source does not match: ${off.join(", ")}`);
2899
+ return METHODS.reduce((src, m) => src.replace(stub(m), () => replacement(m)), source);
2788
2900
  }
2789
2901
  /**
2790
- * The leader line's stroke: the border colour, else the text colour, and the border width, with
2791
- * 1 pt when the box has no border.
2902
+ * Wraps the engine's string copy so the first string it receives, the scripting source that
2903
+ * `Sandbox.create` hands it, is patched. A mismatch throws inside `create`, which then fails to
2904
+ * start the sandbox; `failure()` gives the reason.
2792
2905
  */
2793
- function calloutStroke(a) {
2794
- return {
2795
- color: a.borderColor ?? a.defaultStyle.color,
2796
- width: a.width > 0 ? a.width : 1
2906
+ function patchSandboxSource(module) {
2907
+ const original = module.stringToNewUTF8.bind(module);
2908
+ let done = false;
2909
+ let failure;
2910
+ module.stringToNewUTF8 = (source) => {
2911
+ if (done) return original(source);
2912
+ done = true;
2913
+ try {
2914
+ return original(patchScriptingSource(source));
2915
+ } catch (e) {
2916
+ failure = e instanceof Error ? e : new Error(String(e));
2917
+ throw failure;
2918
+ }
2797
2919
  };
2920
+ return { failure: () => failure };
2798
2921
  }
2922
+ //#endregion
2923
+ //#region src/pdfjs/scripting.ts
2924
+ /** Where the sandbox module comes from. An object, so tests can see whether it was loaded. */
2925
+ const sandboxLoader = { load: () => import("#pdfjs-sandbox") };
2799
2926
  /**
2800
- * The `rect` and `fringe` that hold the text box `box` and a leader line through `callout`
2801
- * stroked `width` points wide: every point is padded by half the stroke, and by the ending's
2802
- * size as well when `head` is one Inkbar draws.
2927
+ * Runs the document's JavaScript in PDF.js's QuickJS sandbox, wired to `form`. Resolves
2928
+ * `undefined` without loading the sandbox when the document has no JavaScript. A sandbox that
2929
+ * fails to load or start is reported as a `FormScriptError` on `form`, and resolves `undefined`,
2930
+ * so the form stays fillable without scripts.
2931
+ *
2932
+ * The sandbox gets its own `window`: alerts go to `form`'s `alert` event, `app.response` returns
2933
+ * `null` and asks the host with a `prompt` action, and timers are tracked and cleared on destroy.
2934
+ * Nothing reaches the page's `window.alert`, `prompt` or `confirm`, and nothing is fetched.
2803
2935
  */
2804
- function withCalloutBounds(box, callout, width, head) {
2805
- const drawn = head !== void 0 && head !== "None" && isKnown(LINE_ENDINGS, head);
2806
- const pad = width / 2 + (drawn ? ENDING_SCALE * Math.max(width, 1) : 0);
2807
- const xs = [
2808
- box.x,
2809
- box.x + box.width,
2810
- ...callout.map((p) => p.x)
2811
- ];
2812
- const ys = [
2813
- box.y,
2814
- box.y + box.height,
2815
- ...callout.map((p) => p.y)
2816
- ];
2817
- const left = Math.min(...xs) - pad;
2818
- const bottom = Math.min(...ys) - pad;
2819
- const right = Math.max(...xs) + pad;
2820
- const top = Math.max(...ys) + pad;
2821
- return {
2822
- rect: {
2823
- x: left,
2824
- y: bottom,
2825
- width: right - left,
2826
- height: top - bottom
2827
- },
2828
- fringe: {
2829
- left: box.x - left,
2830
- top: top - (box.y + box.height),
2831
- right: right - (box.x + box.width),
2832
- bottom: box.y - bottom
2936
+ async function createFormScripting(opts) {
2937
+ const { proxy, assets, form } = opts;
2938
+ if (typeof window === "undefined") return void 0;
2939
+ if (!await proxy.hasJSActions()) return void 0;
2940
+ const [objects, calculationOrder, actions] = await Promise.all([
2941
+ proxy.getFieldObjects(),
2942
+ proxy.getCalculationOrderIds(),
2943
+ proxy.getJSActions()
2944
+ ]);
2945
+ if (![...objects?.values() ?? []].some((list) => list.some((o) => hasEntries(o.actions))) && !hasEntries(actions)) return void 0;
2946
+ const fields = form.fields.get();
2947
+ const byName = new Map(fields.map((f) => [f.name, f]));
2948
+ const byWidget = new Map(fields.flatMap((f) => f.widgets.map((w) => [w.id, f])));
2949
+ const widgets = new Map(fields.flatMap((f) => f.widgets.map((w) => [w.id, w])));
2950
+ /** Fields PDF.js reads as a date or time, from their standard format scripts. */
2951
+ const dates = new Set([...objects?.entries() ?? []].filter(([, list]) => list.some((o) => o.hasDatetimeHTML === true)).map(([name]) => name));
2952
+ /** What the sandbox holds, by field name, in the form's terms. */
2953
+ const mirror = /* @__PURE__ */ new Map();
2954
+ for (const f of fields) if (f.value !== void 0) mirror.set(f.name, f.value);
2955
+ let dead = false;
2956
+ /** Updates sent during a synchronous dispatch, applied together once it returns. */
2957
+ let buffer;
2958
+ const report = (cause) => {
2959
+ if (dead) return;
2960
+ const message = cause instanceof Error ? cause.message : String(cause);
2961
+ form._error(new InkError("FormScriptError", message, {}, cause));
2962
+ };
2963
+ const onSend = (detail) => {
2964
+ if (dead || typeof detail !== "object" || detail === null) return;
2965
+ if (buffer) buffer.push(detail);
2966
+ else apply([detail], { values: true });
2967
+ };
2968
+ const win = createWindow(form, onSend, report, () => dead);
2969
+ let sandbox;
2970
+ try {
2971
+ sandbox = await withTimeout(sandboxLoader.load().then(({ QuickJSSandbox }) => QuickJSSandbox(assets.wasm)), SANDBOX_TIMEOUT_MS);
2972
+ const support = sandbox.support;
2973
+ const engine = sandbox._module;
2974
+ if (!support || !engine) throw new Error("The PDF.js sandbox has no engine");
2975
+ support.win = win.shim;
2976
+ const patch = patchSandboxSource(engine);
2977
+ try {
2978
+ sandbox.create({
2979
+ objects,
2980
+ calculationOrder,
2981
+ appInfo: {
2982
+ platform: navigator.platform,
2983
+ language: navigator.language
2984
+ },
2985
+ docInfo: {
2986
+ Title: opts.info.title ?? "",
2987
+ Author: opts.info.author ?? "",
2988
+ numPages: opts.info.pageCount,
2989
+ URL: "",
2990
+ baseURL: "",
2991
+ actions
2992
+ }
2993
+ });
2994
+ } catch (cause) {
2995
+ win.clear();
2996
+ throw patch.failure() ?? cause;
2997
+ }
2998
+ } catch (cause) {
2999
+ dead = true;
3000
+ try {
3001
+ sandbox?.nukeSandbox();
3002
+ } catch {}
3003
+ form._error(new InkError("FormScriptError", "The form's scripts could not start; it can still be filled in", {}, cause));
3004
+ return;
3005
+ }
3006
+ /** Page events run one after another. */
3007
+ let pages = Promise.resolve();
3008
+ /** Pages whose actions the sandbox already has. */
3009
+ const visited = /* @__PURE__ */ new Set();
3010
+ /** Dispatches `event` and returns what the scripts sent while it ran. */
3011
+ const run = (event) => {
3012
+ const outer = buffer;
3013
+ const got = [];
3014
+ buffer = got;
3015
+ try {
3016
+ sandbox.dispatchEvent(event);
3017
+ } catch (cause) {
3018
+ report(cause);
3019
+ } finally {
3020
+ buffer = outer;
3021
+ }
3022
+ return got;
3023
+ };
3024
+ const fieldOf = (id) => typeof id === "string" ? byWidget.get(id) : void 0;
3025
+ const ownOf = (u, field) => fieldOf(u.id) === field;
3026
+ /**
3027
+ * Applies updates. `values: false` keeps the sandbox's values out of the form (an undo
3028
+ * replaying a state calculate already produced). The `source` field's own updates are
3029
+ * skipped when its commit was rejected, and its value always, since the form writes it.
3030
+ */
3031
+ function apply(updates, o) {
3032
+ const next = {};
3033
+ for (const u of updates) {
3034
+ if (dead) return;
3035
+ if (u.id === void 0 || u.id === null) {
3036
+ command(u);
3037
+ continue;
3038
+ }
3039
+ const field = fieldOf(u.id);
3040
+ if (!field) continue;
3041
+ const own = o.source?.field === field;
3042
+ if (own && !o.source?.accepted) continue;
3043
+ if ("value" in u) {
3044
+ const raw = dates.has(field.name) && typeof u.value === "number" ? u.formattedValue : u.value;
3045
+ const value = raw === u.value || typeof raw === "string" ? fromSandbox(field, u.id, raw) : void 0;
3046
+ if (value !== void 0) {
3047
+ mirror.set(field.name, value);
3048
+ if (o.values && !own) next[field.name] = value;
3049
+ }
3050
+ }
3051
+ if ("formattedValue" in u) form._setFormatted(field.name, typeof u.formattedValue === "string" ? u.formattedValue : void 0);
3052
+ const override = overrideOf(u);
3053
+ if (override) {
3054
+ const siblings = Array.isArray(u.siblings) ? u.siblings : [];
3055
+ for (const id of [u.id, ...siblings]) if (typeof id === "string" && widgets.has(id)) form._setWidgetState(id, override);
3056
+ }
3057
+ if (u.focus === true) form._focus(u.id);
3058
+ }
3059
+ if (Object.keys(next).length > 0) form._scriptValues(next);
3060
+ }
3061
+ function command(u) {
3062
+ switch (u.command) {
3063
+ case "print":
3064
+ form._emitAction({ kind: "print" });
3065
+ break;
3066
+ case ACTION_COMMAND: {
3067
+ const action = scriptAction$1(u.method, u.args, form);
3068
+ if (action) form._emitAction(action);
3069
+ break;
3070
+ }
3071
+ case "error": report(new Error(typeof u.value === "string" ? u.value : "A form script failed"));
3072
+ }
3073
+ }
3074
+ /**
3075
+ * Commits `value` to `field` in the sandbox: validate, calculate and format run. Returns the
3076
+ * value the sandbox kept, which a script may have changed, or `undefined` when rejected.
3077
+ */
3078
+ const commitTo = (field, value, values) => {
3079
+ const event = commitEvent(field, value);
3080
+ if (!event) {
3081
+ mirror.set(field.name, value);
3082
+ return value;
2833
3083
  }
3084
+ const updates = run(event);
3085
+ const accepted = !updates.some((u) => ownOf(u, field) && "selRange" in u);
3086
+ apply(updates, {
3087
+ values,
3088
+ source: {
3089
+ field,
3090
+ accepted
3091
+ }
3092
+ });
3093
+ if (!accepted) return void 0;
3094
+ const echo = updates.filter((u) => ownOf(u, field) && "value" in u).at(-1);
3095
+ const kept = echo ? scriptChange(field, echo) : void 0;
3096
+ const stored = kept !== void 0 && !sameValue$1(kept, value) ? kept : value;
3097
+ mirror.set(field.name, stored);
3098
+ return stored;
2834
3099
  };
2835
- }
2836
- //#endregion
2837
- //#region src/model/annotations/types.ts
2838
- const PointSchema = z.object(Point.shape);
2839
- const RectSchema = z.object(Rect.shape);
2840
- const QuadSchema = z.object({
2841
- p1: PointSchema,
2842
- p2: PointSchema,
2843
- p3: PointSchema,
2844
- p4: PointSchema
2845
- });
2846
- /** What Inkbar adds to every Gloss type: the fields it always fills in, and its viewer state. */
2847
- const Base = {
2848
- /** Bounding box in PDF user space. */
2849
- rect: RectSchema,
2850
- /** Resolved #RRGGBB. Written into the PDF later, so it is always concrete. */
2851
- color: Color,
2852
- opacity: commonFields.opacity.removeDefault(),
2853
- createdAt: commonFields.createdAt.unwrap(),
2854
- modifiedAt: commonFields.modifiedAt.unwrap(),
2855
- /** Created in this session, or imported from the file. Viewer state, never exported. */
2856
- source: z.enum(["session", "pdf"]),
2857
- /** Embedded annotations are locked until the engine can rewrite the file. Viewer state, never exported. */
2858
- locked: z.boolean().optional(),
2859
3100
  /**
2860
- * Embedded annotations only: whether the file carries its own appearance stream for it. PDF.js
2861
- * paints the page with it; without one, it paints a stand-in for most types but nothing for a
2862
- * note. Viewer state, never exported.
3101
+ * The value a script changed a commit to, from the sandbox's echo of it; `undefined` when
3102
+ * the echo is no change. The sandbox reports a text value as the string it was given (it
3103
+ * keeps the original alongside any number it parses), so a different string is a script's
3104
+ * change. A number is the sandbox's own parse: a date or time field reports the time it
3105
+ * read. A checkbox or radio echo names the first widget with the export value, which is
3106
+ * not always the one chosen, so the chosen state stands; a rejection shows as a selection.
2863
3107
  */
2864
- hasAppearance: z.boolean().optional()
2865
- };
2866
- const Quads = z.array(QuadSchema).min(1);
2867
- const TextMarkupSchema = z.object({
2868
- ...Highlight.shape,
2869
- ...Base,
2870
- type: z.enum([
2871
- "highlight",
2872
- "underline",
2873
- "strikeout",
2874
- "squiggly"
2875
- ]),
2876
- quads: Quads
2877
- });
2878
- /** A note's icon (XFDF `icon`): a PDF name, so a name Inkbar does not know is kept. */
2879
- const NoteIconSchema = NoteIcon;
2880
- const NoteSchema = z.object({
2881
- ...Note.shape,
2882
- ...Base
2883
- });
2884
- /** A line ending (XFDF `head`/`tail`): a PDF name, so a name Inkbar does not know is kept. */
2885
- const LineEndingSchema = LineEnding;
2886
- /**
2887
- * A key a type refuses outright rather than dropping like other unknown keys: one an older
2888
- * shape used (so its value is not silently lost), or one that means nothing on the type (so a
2889
- * later version is free to give it a meaning).
2890
- */
2891
- const Refused = z.never().optional();
2892
- /**
2893
- * A free-text box's text style, keyed by the CSS property names XFDF writes in its
2894
- * `defaultstyle` string. The older `align` is refused; other unknown keys are dropped.
2895
- */
2896
- const FreeTextStyleSchema = z.object({
2897
- ...FreeTextStyle.shape,
2898
- /** The text colour. */
2899
- color: Color,
2900
- align: Refused
2901
- });
2902
- /**
2903
- * Free text, as XFDF has it: `color` is the box's fill and the text style lives in
2904
- * `defaultStyle`. A box in the older shape (top-level `fontFamily`, `fontSize`, `align`) is
2905
- * refused rather than silently losing its style; other unknown keys are dropped.
2906
- */
2907
- const FreeTextSchema = z.object({
2908
- ...FreeText.shape,
2909
- ...Base,
2910
- /** The box's fill (XFDF `color`). Absent means no fill. */
2911
- color: FreeText.shape.color,
2912
- /** The box's border colour. Absent means the text colour. */
2913
- borderColor: Color.optional(),
2914
- defaultStyle: FreeTextStyleSchema,
2915
- fringe: FringeSchema.optional(),
2916
- callout: z.array(PointSchema).min(2).max(3).optional(),
2917
- fontFamily: Refused,
2918
- fontSize: Refused,
2919
- align: Refused
2920
- });
2921
- const SquareSchema = z.object({
2922
- ...Square.shape,
2923
- ...Base
2924
- });
2925
- const CircleSchema = z.object({
2926
- ...Circle.shape,
2927
- ...Base,
2928
- cornerRadius: Refused
2929
- });
2930
- const ShapeSchema = z.discriminatedUnion("type", [SquareSchema, CircleSchema]);
2931
- const LineSchema = z.object({
2932
- ...Line.shape,
2933
- ...Base,
2934
- start: PointSchema,
2935
- end: PointSchema
2936
- });
2937
- const Vertices = z.array(PointSchema).min(2);
2938
- const PolylineSchema = z.object({
2939
- ...Polyline.shape,
2940
- ...Base,
2941
- vertices: Vertices
2942
- });
2943
- const PolygonSchema = z.object({
2944
- ...Polygon.shape,
2945
- ...Base,
2946
- vertices: Vertices,
2947
- head: Refused,
2948
- tail: Refused
2949
- });
2950
- const PolySchema = z.discriminatedUnion("type", [PolylineSchema, PolygonSchema]);
2951
- const InkShape = z.object({
2952
- ...Ink.shape,
2953
- ...Base,
2954
- inklist: z.array(z.array(PointSchema).min(1)).min(1),
2955
- /** Pen pressure from 0 to 1, one value per point of each stroke. Absent means even pressure. */
2956
- pressure: z.array(z.array(z.number().min(0).max(1))).optional()
2957
- });
2958
- /** Pressure must have one value per point of each stroke. */
2959
- function checkPressure(a, ctx) {
2960
- if (!a.pressure) return;
2961
- if (!(a.pressure.length === a.inklist.length && a.pressure.every((p, i) => p.length === a.inklist[i]?.length))) ctx.addIssue({
2962
- code: "custom",
2963
- path: ["pressure"],
2964
- message: "pressure must have the same shape as inklist"
2965
- });
3108
+ const scriptChange = (field, echo) => {
3109
+ switch (field.type) {
3110
+ case "text":
3111
+ case "combo": return typeof echo.value === "string" ? fromSandbox(field, echo.id, echo.value) : void 0;
3112
+ case "list": return fromSandbox(field, echo.id, echo.value);
3113
+ default: return;
3114
+ }
3115
+ };
3116
+ const scripting = {
3117
+ commit(name, value) {
3118
+ const field = byName.get(name);
3119
+ if (dead || !field) return value;
3120
+ return commitTo(field, value, true);
3121
+ },
3122
+ sync(values, calculate) {
3123
+ if (dead) return;
3124
+ for (const field of fields) {
3125
+ const value = values[field.name];
3126
+ if (value === void 0 || sameValue$1(value, mirror.get(field.name))) continue;
3127
+ commitTo(field, value, calculate);
3128
+ if (dead) return;
3129
+ }
3130
+ },
3131
+ dispatch(event) {
3132
+ const field = byWidget.get(event.widget);
3133
+ const widget = widgets.get(event.widget);
3134
+ if (dead || !field || !widget) return;
3135
+ const value = event.value ?? currentValue(form, field, widget);
3136
+ apply(run(toSandboxEvent(event, value)), { values: true });
3137
+ },
3138
+ keystroke(widget, text, change, selStart, selEnd) {
3139
+ const field = byWidget.get(widget);
3140
+ if (dead || !field) return void 0;
3141
+ const updates = run({
3142
+ id: widget,
3143
+ name: "Keystroke",
3144
+ value: text,
3145
+ change,
3146
+ selStart,
3147
+ selEnd,
3148
+ willCommit: false
3149
+ });
3150
+ mirror.delete(field.name);
3151
+ apply(updates, {
3152
+ values: true,
3153
+ source: {
3154
+ field,
3155
+ accepted: false
3156
+ }
3157
+ });
3158
+ return keystrokeResult(updates, field, text, change, selStart, selEnd);
3159
+ },
3160
+ pageEvent(page, name) {
3161
+ if (dead) return Promise.resolve();
3162
+ pages = pages.then(async () => {
3163
+ let actions = null;
3164
+ if (name === "PageOpen" && !visited.has(page)) {
3165
+ visited.add(page);
3166
+ actions = await (await proxy.getPage(page + 1)).getJSActions();
3167
+ }
3168
+ if (dead) return;
3169
+ apply(run({
3170
+ id: "page",
3171
+ name,
3172
+ pageNumber: page + 1,
3173
+ actions
3174
+ }), { values: true });
3175
+ }).catch(report);
3176
+ return pages;
3177
+ },
3178
+ destroy() {
3179
+ if (dead) return;
3180
+ dead = true;
3181
+ win.clear();
3182
+ try {
3183
+ sandbox.nukeSandbox();
3184
+ } catch {}
3185
+ }
3186
+ };
3187
+ if (opts.testing) scripting._sandboxForTesting = sandbox;
3188
+ apply(run({
3189
+ id: "doc",
3190
+ name: "Open"
3191
+ }), { values: true });
3192
+ scripting.sync(form.values.get(), opts.calculateAtStart === true);
3193
+ return scripting;
2966
3194
  }
2967
- const InkSchema = InkShape.superRefine(checkPressure);
2968
- /**
2969
- * An image on the page (XFDF `stamp`), drawn to fill `rect`. Image and typed signatures are
2970
- * stamps with `subject: 'Signature'`.
2971
- */
2972
- const StampSchema = z.object({
2973
- ...Stamp.shape,
2974
- ...Base
2975
- });
2976
- /**
2977
- * A loupe (Inkbar's own type, a circle with `custom.inkbar.type: 'loupe'` in a file): a circle
2978
- * inscribed in `rect` that shows the page content under its own centre, magnified. `color` and
2979
- * `width` draw its rim.
2980
- */
2981
- const LoupeSchema = z.object({
2982
- type: z.literal("loupe"),
2983
- ...commonFields,
2984
- /** How many times larger the content under the centre is drawn. Above 1. */
2985
- magnification: z.number().gt(1).default(2),
2986
- /** The rim's width in points. */
2987
- width: z.number().nonnegative().default(2),
2988
- ...Base
2989
- });
3195
+ /** A scalar from the sandbox as text; anything else is empty. */
3196
+ function textOf(v) {
3197
+ if (typeof v === "string") return v;
3198
+ return typeof v === "number" || typeof v === "boolean" ? String(v) : "";
3199
+ }
3200
+ /** How long the sandbox may take to load before the form goes on without scripts. */
3201
+ const SANDBOX_TIMEOUT_MS = 1e4;
2990
3202
  /**
2991
- * A mask (Inkbar's own type, a circle or square with `custom.inkbar.type: 'mask'` in a file):
2992
- * dims everything on the page outside `shape` drawn in `rect`, an ellipse inscribed in it or the
2993
- * rectangle itself. `color` is the dimming colour and `dim` how strongly it covers, from 0 to 1.
2994
- * One per page: adding a second replaces the first.
3203
+ * Rejects when `promise` takes longer than `ms`, so a stalled wasm load never holds up the
3204
+ * document. A sandbox that arrives late is torn down.
2995
3205
  */
2996
- const MaskSchema = z.object({
2997
- type: z.literal("mask"),
2998
- ...commonFields,
2999
- shape: z.enum(["rect", "ellipse"]),
3000
- dim: z.number().min(0).max(1).default(.5),
3001
- ...Base
3002
- });
3003
- const RedactSchema = z.object({
3004
- ...Redact.shape,
3005
- ...Base,
3006
- /** Present when the mark came from text; absent for an area mark. */
3007
- quads: Quads.optional(),
3008
- /** Colour the redaction will be filled with when applied. */
3009
- interiorColor: Color
3010
- });
3011
- const AnnotationSchema = z.discriminatedUnion("type", [
3012
- TextMarkupSchema,
3013
- NoteSchema,
3014
- FreeTextSchema,
3015
- SquareSchema,
3016
- CircleSchema,
3017
- LineSchema,
3018
- PolylineSchema,
3019
- PolygonSchema,
3020
- InkSchema,
3021
- StampSchema,
3022
- LoupeSchema,
3023
- MaskSchema,
3024
- RedactSchema
3025
- ]);
3026
- /** The viewer's own state on an annotation, left out of exports. */
3027
- const VIEWER_STATE = {
3028
- source: true,
3029
- locked: true,
3030
- hasAppearance: true
3031
- };
3032
- /** One exported annotation: the stored annotation without its viewer state. */
3033
- const ExportedAnnotationSchema = z.discriminatedUnion("type", [
3034
- TextMarkupSchema.omit(VIEWER_STATE),
3035
- NoteSchema.omit(VIEWER_STATE),
3036
- FreeTextSchema.omit(VIEWER_STATE),
3037
- SquareSchema.omit(VIEWER_STATE),
3038
- CircleSchema.omit(VIEWER_STATE),
3039
- LineSchema.omit(VIEWER_STATE),
3040
- PolylineSchema.omit(VIEWER_STATE),
3041
- PolygonSchema.omit(VIEWER_STATE),
3042
- InkShape.omit(VIEWER_STATE).superRefine(checkPressure),
3043
- StampSchema.omit(VIEWER_STATE),
3044
- LoupeSchema.omit(VIEWER_STATE),
3045
- MaskSchema.omit(VIEWER_STATE),
3046
- RedactSchema.omit(VIEWER_STATE)
3047
- ]);
3048
- const AnnotationDocumentSchema = z.object({
3049
- format: z.literal(GLOSS_FORMAT),
3050
- version: z.literal(GLOSS_VERSION),
3051
- documentKey: z.string().optional(),
3052
- /** Validated item by item on import so unknown types can be preserved. */
3053
- annotations: z.array(z.unknown()),
3054
- /** Form field values by full name, validated field by field against the form on import. */
3055
- fields: z.record(z.string(), z.unknown()).optional()
3056
- });
3057
- //#endregion
3058
- //#region src/model/annotations/embedded.ts
3059
- const SUPPORTED_SUBTYPES = /* @__PURE__ */ new Set([
3060
- "Highlight",
3061
- "Underline",
3062
- "StrikeOut",
3063
- "Squiggly",
3064
- "Text",
3065
- "FreeText",
3066
- "Square",
3067
- "Circle",
3068
- "Line",
3069
- "Polygon",
3070
- "PolyLine",
3071
- "Ink",
3072
- "Redact"
3073
- ]);
3074
- /** An RGB colour in 0–255 as upper-case `#RRGGBB`; `fallback` when it has fewer than three parts. */
3075
- function pdfColorToHex(c, fallback) {
3076
- if (!c || c.length < 3) return fallback;
3077
- return `#${[
3078
- c[0],
3079
- c[1],
3080
- c[2]
3081
- ].map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
3082
- }
3083
- /** PDF.js hands back `rect` already in user space, but not always ordered; normalise with min/max. */
3084
- function rectFromPdf(r) {
3085
- return rectFromPoints({
3086
- x: r[0],
3087
- y: r[1]
3088
- }, {
3089
- x: r[2],
3090
- y: r[3]
3206
+ function withTimeout(promise, ms) {
3207
+ return new Promise((resolve, reject) => {
3208
+ let late = false;
3209
+ const timer = setTimeout(() => {
3210
+ late = true;
3211
+ reject(/* @__PURE__ */ new Error(`The PDF.js sandbox did not load within ${ms} ms`));
3212
+ }, ms);
3213
+ promise.then((sandbox) => {
3214
+ clearTimeout(timer);
3215
+ if (late) sandbox.nukeSandbox();
3216
+ else resolve(sandbox);
3217
+ }, (cause) => {
3218
+ clearTimeout(timer);
3219
+ reject(cause instanceof Error ? cause : new Error(String(cause)));
3220
+ });
3091
3221
  });
3092
3222
  }
3093
- /** A PDF name exactly as PDF.js gives it (names are case-sensitive), or undefined if invalid. */
3094
- function pdfName(raw) {
3095
- return raw !== void 0 && PDF_NAME$1.test(raw) ? raw : void 0;
3223
+ function hasEntries(value) {
3224
+ if (value instanceof Map) return value.size > 0;
3225
+ return typeof value === "object" && value !== null && Object.keys(value).length > 0;
3226
+ }
3227
+ /** The `window` the sandbox sees. */
3228
+ function createWindow(form, onSend, report, isDead) {
3229
+ const timers = /* @__PURE__ */ new Map();
3230
+ const clear = (id) => {
3231
+ const kind = timers.get(id);
3232
+ if (!kind) return;
3233
+ timers.delete(id);
3234
+ if (kind === "interval") clearInterval(id);
3235
+ else clearTimeout(id);
3236
+ };
3237
+ const noop = () => void 0;
3238
+ class ShimEvent {
3239
+ type;
3240
+ detail;
3241
+ constructor(type, init) {
3242
+ this.type = type;
3243
+ this.detail = init?.detail;
3244
+ }
3245
+ }
3246
+ return {
3247
+ shim: {
3248
+ alert: (message) => {
3249
+ if (!isDead()) form._alert(message);
3250
+ },
3251
+ confirm: () => false,
3252
+ prompt: (message) => {
3253
+ if (!isDead()) form._emitAction({
3254
+ kind: "prompt",
3255
+ message
3256
+ });
3257
+ return null;
3258
+ },
3259
+ setTimeout: (fn, ms) => {
3260
+ if (isDead()) return 0;
3261
+ const id = setTimeout(() => {
3262
+ timers.delete(id);
3263
+ if (!isDead()) fn();
3264
+ }, ms);
3265
+ timers.set(id, "timeout");
3266
+ return id;
3267
+ },
3268
+ setInterval: (fn, ms) => {
3269
+ if (isDead()) return 0;
3270
+ const id = setInterval(() => {
3271
+ if (!isDead()) fn();
3272
+ }, ms);
3273
+ timers.set(id, "interval");
3274
+ return id;
3275
+ },
3276
+ clearTimeout: clear,
3277
+ clearInterval: clear,
3278
+ console: {
3279
+ log: noop,
3280
+ info: noop,
3281
+ warn: noop,
3282
+ debug: noop,
3283
+ error: report
3284
+ },
3285
+ URL,
3286
+ CustomEvent: ShimEvent,
3287
+ dispatchEvent: (event) => {
3288
+ if (event.type === "updatefromsandbox") onSend(event.detail);
3289
+ return true;
3290
+ }
3291
+ },
3292
+ clear() {
3293
+ for (const id of [...timers.keys()]) clear(id);
3294
+ }
3295
+ };
3296
+ }
3297
+ /** The event that commits `value` the way a person would, or none for a field without one. */
3298
+ function commitEvent(field, value) {
3299
+ const first = field.widgets[0];
3300
+ if (!first) return void 0;
3301
+ switch (field.type) {
3302
+ case "checkbox":
3303
+ case "radio": {
3304
+ const on = field.widgets.find((w) => w.onState !== void 0 && w.onState === value);
3305
+ if (!on && (value !== "Off" || field.type === "radio")) return void 0;
3306
+ return {
3307
+ id: (on ?? first).id,
3308
+ name: "Validate",
3309
+ value: on !== void 0
3310
+ };
3311
+ }
3312
+ case "text":
3313
+ case "combo":
3314
+ case "list": {
3315
+ if (typeof value !== "string" && !Array.isArray(value)) return void 0;
3316
+ const end = typeof value === "string" ? value.length : 0;
3317
+ return {
3318
+ id: first.id,
3319
+ name: "Keystroke",
3320
+ value,
3321
+ change: "",
3322
+ willCommit: true,
3323
+ commitKey: 1,
3324
+ selStart: end,
3325
+ selEnd: end
3326
+ };
3327
+ }
3328
+ default: return;
3329
+ }
3096
3330
  }
3097
- /** `head` and `tail` from a Line's or PolyLine's `/LE`, left out when both are `None`. */
3098
- function lineEndingsOf(raw) {
3099
- const endings = raw?.map((e) => pdfName(e) ?? "None");
3100
- return endings && endings.length === 2 && endings.some((e) => e !== "None") ? {
3101
- head: endings[0],
3102
- tail: endings[1]
3103
- } : {};
3331
+ /** The value an event carries when the form layer gives none: what the widget shows. */
3332
+ function currentValue(form, field, widget) {
3333
+ const value = form.get(field.name);
3334
+ switch (field.type) {
3335
+ case "checkbox":
3336
+ case "radio": return widget.onState !== void 0 && value === widget.onState;
3337
+ case "text":
3338
+ case "combo":
3339
+ case "list": return typeof value === "string" || Array.isArray(value) ? value : "";
3340
+ default: return;
3341
+ }
3104
3342
  }
3105
- /**
3106
- * Imported ids are `pdf:<page>:<pdfjsId>` (page is 0-based, pdfjsId is the annotation's object
3107
- * reference such as `12R`). They are stable across opens of the same file, and `inReplyTo`
3108
- * uses the same scheme so threads resolve.
3109
- */
3110
- function importedId(page, pdfjsId) {
3111
- return `pdf:${page}:${pdfjsId}`;
3343
+ function toSandboxEvent(event, value) {
3344
+ const out = {
3345
+ id: event.widget,
3346
+ name: event.name
3347
+ };
3348
+ if (value !== void 0) out.value = value;
3349
+ for (const key of [
3350
+ "change",
3351
+ "selStart",
3352
+ "selEnd",
3353
+ "willCommit",
3354
+ "commitKey"
3355
+ ]) if (event[key] !== void 0) out[key] = event[key];
3356
+ return out;
3112
3357
  }
3113
- /**
3114
- * Imports one page's PDF.js annotation data. `targets` is @internal: the annotations whose
3115
- * missing fields the document reads from the file after `ready`.
3116
- */
3117
- function importEmbeddedPage(page, raw, importedAt) {
3118
- const counts = {};
3119
- const annotations = [];
3120
- const targets = [];
3121
- for (const data of raw) {
3122
- counts[data.subtype] = (counts[data.subtype] ?? 0) + 1;
3123
- if (!SUPPORTED_SUBTYPES.has(data.subtype) || data.hidden || data.noView) continue;
3124
- const converted = convert(page, data, importedAt);
3125
- if (!converted) continue;
3126
- annotations.push(converted);
3127
- const target = targetOf(converted.id, data);
3128
- if (target) targets.push(target);
3358
+ /** A value from the sandbox in the form's terms; `undefined` for a field without one. */
3359
+ function fromSandbox(field, id, value) {
3360
+ switch (field.type) {
3361
+ case "checkbox":
3362
+ case "radio":
3363
+ if (value === "Off" || value === "" || value === null || value === void 0) return "Off";
3364
+ return (field.widgets.find((w) => w.id === id && w.onState !== void 0) ?? field.widgets.find((w) => w.onState === value))?.onState ?? textOf(value);
3365
+ case "list":
3366
+ if (field.flags.multiSelect) return Array.isArray(value) ? value.map(textOf) : value === "" ? [] : [textOf(value)];
3367
+ return Array.isArray(value) ? textOf(value[0]) : textOf(value);
3368
+ case "text":
3369
+ case "combo": return Array.isArray(value) ? textOf(value[0]) : textOf(value);
3370
+ default: return;
3129
3371
  }
3130
- return {
3131
- annotations,
3132
- counts,
3133
- targets
3134
- };
3135
3372
  }
3136
- function targetOf(id, d) {
3137
- const rect = Array.from(d.rect);
3138
- if (d.subtype === "Text") return d.name === "NoIcon" ? {
3139
- id,
3140
- subtype: "Text",
3141
- rect
3142
- } : void 0;
3143
- if (d.subtype !== "FreeText") return void 0;
3144
- const daFont = d.defaultAppearanceData?.fontName;
3145
- return {
3146
- id,
3147
- subtype: "FreeText",
3148
- rect,
3149
- ...daFont ? { daFont } : {}
3150
- };
3373
+ /** `["RGB", r, g, b]`, `["G", g]`, `["CMYK", c, m, y, k]` or `["T"]` as `#rrggbb`. */
3374
+ function colorOf(value) {
3375
+ if (!Array.isArray(value) || typeof value[0] !== "string") return void 0;
3376
+ const [space, ...raw] = value;
3377
+ const n = raw.map((v) => typeof v === "number" ? Math.min(1, Math.max(0, v)) : 0);
3378
+ let rgb;
3379
+ if (space === "T") return "transparent";
3380
+ if (space === "G") rgb = [
3381
+ n[0] ?? 0,
3382
+ n[0] ?? 0,
3383
+ n[0] ?? 0
3384
+ ];
3385
+ else if (space === "RGB") rgb = [
3386
+ n[0] ?? 0,
3387
+ n[1] ?? 0,
3388
+ n[2] ?? 0
3389
+ ];
3390
+ else if (space === "CMYK") {
3391
+ const k = n[3] ?? 0;
3392
+ rgb = [
3393
+ n[0] ?? 0,
3394
+ n[1] ?? 0,
3395
+ n[2] ?? 0
3396
+ ].map((c) => (1 - c) * (1 - k));
3397
+ } else return void 0;
3398
+ return `#${rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join("")}`;
3151
3399
  }
3152
- function summarise(perPage) {
3153
- const totals = /* @__PURE__ */ new Map();
3154
- for (const counts of perPage) for (const [k, v] of Object.entries(counts)) totals.set(k, (totals.get(k) ?? 0) + v);
3155
- return [...totals.entries()].map(([subtype, count]) => ({
3156
- subtype,
3157
- count,
3158
- supported: SUPPORTED_SUBTYPES.has(subtype)
3159
- })).sort((a, b) => a.subtype < b.subtype ? -1 : a.subtype > b.subtype ? 1 : 0);
3400
+ function overrideOf(u) {
3401
+ const o = {};
3402
+ if (typeof u.hidden === "boolean") o.hidden = u.hidden;
3403
+ if (typeof u.display === "number") o.hidden = u.display % 2 === 1;
3404
+ if (typeof u.readonly === "boolean") o.readOnly = u.readonly;
3405
+ if (typeof u.required === "boolean") o.required = u.required;
3406
+ const text = colorOf(u.textColor ?? u.fgColor);
3407
+ const fill = colorOf(u.fillColor ?? u.bgColor);
3408
+ const stroke = colorOf(u.strokeColor ?? u.borderColor);
3409
+ if (text) o.textColor = text;
3410
+ if (fill) o.fillColor = fill;
3411
+ if (stroke) o.strokeColor = stroke;
3412
+ if (typeof u.textSize === "number" && Number.isFinite(u.textSize) && u.textSize >= 0) o.textSize = u.textSize;
3413
+ return Object.keys(o).length > 0 ? o : void 0;
3160
3414
  }
3161
- function convert(page, d, importedAt) {
3162
- const created = parsePdfDate(d.creationDate, parsePdfDate(d.modificationDate, importedAt));
3163
- const author = d.titleObj?.str;
3164
- const contents = d.contentsObj?.str;
3165
- const stroke = d.borderStyle?.width ?? 1;
3166
- const quads = d.quadPoints ? quadsFromFlat(d.quadPoints) : [];
3167
- const optional = (v) => v !== void 0 && v !== "";
3168
- const withColor = (type, source = d.color) => pdfColorToHex(source, staticDefaults.color(type));
3169
- const intent = pdfName(d.it);
3170
- const base = (type, color = withColor(type)) => ({
3171
- id: importedId(page, d.id),
3172
- page,
3173
- rect: rectFromPdf(d.rect),
3174
- color,
3175
- opacity: d.opacity ?? 1,
3176
- ...optional(author) ? { author } : {},
3177
- ...optional(contents) ? { contents } : {},
3178
- ...d.inReplyTo ? { inReplyTo: importedId(page, d.inReplyTo) } : {},
3179
- ...intent !== void 0 ? { intent } : {},
3180
- createdAt: created,
3181
- modifiedAt: parsePdfDate(d.modificationDate, created),
3182
- source: "pdf",
3183
- locked: true,
3184
- hasAppearance: d.hasAppearance === true
3185
- });
3186
- const fillOf = (c) => c && c.length >= 3 ? { interiorColor: pdfColorToHex(c, "#FFFFFF") } : {};
3187
- let candidate;
3188
- switch (d.subtype) {
3189
- case "Highlight":
3190
- case "Underline":
3191
- case "StrikeOut":
3192
- case "Squiggly": {
3193
- if (quads.length === 0) return void 0;
3194
- const type = d.subtype === "StrikeOut" ? "strikeout" : d.subtype.toLowerCase();
3195
- candidate = {
3196
- ...base(type),
3197
- type,
3198
- quads
3199
- };
3200
- break;
3201
- }
3202
- case "Text": {
3203
- const icon = d.name === "NoIcon" ? void 0 : pdfName(d.name);
3204
- candidate = {
3205
- ...base("text"),
3206
- type: "text",
3207
- ...icon !== void 0 ? { icon } : {}
3208
- };
3209
- break;
3210
- }
3211
- case "FreeText": {
3212
- const da = d.defaultAppearanceData;
3213
- const { color: _stroke, ...rest } = base("freetext");
3214
- candidate = {
3215
- ...rest,
3216
- ...d.color && d.color.length >= 3 ? { color: pdfColorToHex(d.color, "#FFFFFF") } : {},
3217
- type: "freetext",
3218
- contents: contents ?? "",
3219
- width: d.borderStyle?.width ?? 0,
3220
- defaultStyle: {
3221
- color: pdfColorToHex(da?.fontColor, staticDefaults.color("freetext")),
3222
- ...da && da.fontSize > 0 ? { fontSize: da.fontSize } : {}
3223
- }
3224
- };
3225
- break;
3226
- }
3227
- case "Square":
3228
- case "Circle": {
3229
- const type = d.subtype.toLowerCase();
3230
- candidate = {
3231
- ...base(type),
3232
- type,
3233
- width: stroke,
3234
- ...fillOf(d.interiorColor)
3235
- };
3236
- break;
3237
- }
3238
- case "Line": {
3239
- const l = d.lineCoordinates ?? [
3240
- d.rect[0],
3241
- d.rect[1],
3242
- d.rect[2],
3243
- d.rect[3]
3244
- ];
3245
- candidate = {
3246
- ...base("line"),
3247
- type: "line",
3248
- start: {
3249
- x: l[0],
3250
- y: l[1]
3251
- },
3252
- end: {
3253
- x: l[2],
3254
- y: l[3]
3255
- },
3256
- width: stroke,
3257
- ...lineEndingsOf(d.lineEndings)
3415
+ function keystrokeResult(updates, field, text, change, selStart, selEnd) {
3416
+ const merged = text.slice(0, selStart) + change + text.slice(selEnd);
3417
+ const caret = selStart + change.length;
3418
+ const last = updates.filter((u) => {
3419
+ const id = typeof u.id === "string" ? u.id : void 0;
3420
+ return id !== void 0 && field.widgets.some((w) => w.id === id) && "value" in u;
3421
+ }).at(-1);
3422
+ if (!last) return {
3423
+ accepted: true,
3424
+ value: merged,
3425
+ selStart: caret,
3426
+ selEnd: caret
3427
+ };
3428
+ const value = textOf(last.value);
3429
+ const accepted = !(value === text && merged !== text);
3430
+ const range = Array.isArray(last.selRange) ? last.selRange : [];
3431
+ const start = typeof range[0] === "number" ? range[0] : caret;
3432
+ const end = typeof range[1] === "number" ? range[1] : start;
3433
+ return accepted ? {
3434
+ accepted,
3435
+ value,
3436
+ selStart: start,
3437
+ selEnd: end
3438
+ } : {
3439
+ accepted,
3440
+ value: text,
3441
+ selStart,
3442
+ selEnd
3443
+ };
3444
+ }
3445
+ /** Maps a rewritten method's call (positional or Acrobat's single-object form) to an action. */
3446
+ function scriptAction$1(method, args, form) {
3447
+ const list = Array.isArray(args) ? args : [];
3448
+ const first = list[0];
3449
+ const named = typeof first === "object" && first !== null && !Array.isArray(first) ? first : void 0;
3450
+ const arg = (key, index) => named ? named[key] : list[index];
3451
+ const str = (key, index) => {
3452
+ const v = arg(key, index);
3453
+ return typeof v === "string" ? v : void 0;
3454
+ };
3455
+ switch (method) {
3456
+ case "submitForm": {
3457
+ const url = safeUrl(str("cURL", 0) ?? "");
3458
+ if (!url) return void 0;
3459
+ const format = submitFormat(str("cSubmitAs", 15), arg("bPDF", 8), arg("bXML", 6), arg("bFDF", 1));
3460
+ const raw = arg("aFields", 3);
3461
+ const names = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).filter((n) => typeof n === "string");
3462
+ return {
3463
+ kind: "submit",
3464
+ url,
3465
+ fields: submitValues(form.values.get(), form.fields.get(), names, true),
3466
+ format,
3467
+ ...format === "html" ? { method: arg("bGet", 4) === true ? "GET" : "POST" } : {}
3258
3468
  };
3259
- break;
3260
3469
  }
3261
- case "Polygon":
3262
- case "PolyLine": {
3263
- const type = d.subtype === "Polygon" ? "polygon" : "polyline";
3264
- const vertices = d.vertices ? pointsFromFlat(d.vertices) : [];
3265
- if (vertices.length < 2) return void 0;
3266
- candidate = {
3267
- ...base(type),
3268
- type,
3269
- vertices,
3270
- width: stroke,
3271
- ...fillOf(d.interiorColor),
3272
- ...type === "polyline" ? lineEndingsOf(d.lineEndings) : {}
3470
+ case "mailDoc":
3471
+ case "mailForm": {
3472
+ const out = {
3473
+ kind: "mail",
3474
+ withData: method === "mailForm"
3273
3475
  };
3274
- break;
3476
+ const to = str("cTo", 1);
3477
+ const cc = str("cCc", 2);
3478
+ const bcc = str("cBcc", 3);
3479
+ const subject = str("cSubject", 4);
3480
+ const body = str("cMsg", 5);
3481
+ for (const address of [
3482
+ to,
3483
+ cc,
3484
+ bcc
3485
+ ]) if (address !== void 0 && safeAddress(address) === void 0) return void 0;
3486
+ if (to !== void 0) out.to = to;
3487
+ if (cc !== void 0) out.cc = cc;
3488
+ if (bcc !== void 0) out.bcc = bcc;
3489
+ if (subject !== void 0) out.subject = subject;
3490
+ if (body !== void 0) out.body = body;
3491
+ return out;
3275
3492
  }
3276
- case "Ink": {
3277
- const inklist = (d.inkLists ?? []).map((p) => pointsFromFlat(p)).filter((p) => p.length > 0);
3278
- if (inklist.length === 0) return void 0;
3279
- candidate = {
3280
- ...base("ink"),
3281
- type: "ink",
3282
- inklist,
3283
- width: stroke
3284
- };
3285
- break;
3493
+ case "launchURL": {
3494
+ const url = safeUrl(str("cURL", 0) ?? "");
3495
+ return url ? {
3496
+ kind: "open-url",
3497
+ url,
3498
+ newWindow: arg("bNewFrame", 1) === true
3499
+ } : void 0;
3286
3500
  }
3287
- case "Redact":
3288
- candidate = {
3289
- ...base("redact"),
3290
- type: "redact",
3291
- interiorColor: pdfColorToHex(d.interiorColor, staticDefaults.fill()),
3292
- ...quads.length > 0 ? { quads } : {}
3293
- };
3294
- break;
3295
3501
  default: return;
3296
3502
  }
3297
- const parsed = AnnotationSchema.safeParse(candidate);
3298
- return parsed.success ? parsed.data : void 0;
3503
+ }
3504
+ /** `cSubmitAs` wins, then `bPDF`, then `bXML`; otherwise FDF, or HTML when `bFDF` is false. */
3505
+ function submitFormat(submitAs, pdf, xml, fdf) {
3506
+ switch (submitAs?.toUpperCase()) {
3507
+ case "FDF": return "fdf";
3508
+ case "XFDF":
3509
+ case "XFD":
3510
+ case "XML": return "xfdf";
3511
+ case "HTML": return "html";
3512
+ case "PDF": return "pdf";
3513
+ }
3514
+ if (pdf === true) return "pdf";
3515
+ if (xml === true) return "xfdf";
3516
+ return fdf === false ? "html" : "fdf";
3517
+ }
3518
+ //#endregion
3519
+ //#region src/pdfjs/structure.ts
3520
+ /** The prefix PDF.js gives an annotation's id in its structure tree. */
3521
+ const ANNOTATION_PREFIX = "pdfjs_internal_id_";
3522
+ /**
3523
+ * The page's annotations in structure order, by PDF.js annotation id (`12R`): the order of the
3524
+ * OBJR references in the page's part of the structure tree, depth first. Empty when the file has
3525
+ * no structure tree or PDF.js cannot read it.
3526
+ */
3527
+ async function structureAnnotationOrder(page) {
3528
+ let root;
3529
+ try {
3530
+ root = await page.getStructTree();
3531
+ } catch {
3532
+ return [];
3533
+ }
3534
+ const out = [];
3535
+ const seen = /* @__PURE__ */ new Set();
3536
+ const walk = (node, depth) => {
3537
+ if (depth > 64 || typeof node !== "object" || node === null) return;
3538
+ const n = node;
3539
+ if (n.type === "annotation" && typeof n.id === "string") {
3540
+ const id = n.id.startsWith(ANNOTATION_PREFIX) ? n.id.slice(18) : n.id;
3541
+ if (!seen.has(id)) {
3542
+ seen.add(id);
3543
+ out.push(id);
3544
+ }
3545
+ }
3546
+ if (Array.isArray(n.children)) for (const kid of n.children) walk(kid, depth + 1);
3547
+ };
3548
+ walk(root, 0);
3549
+ return out;
3299
3550
  }
3300
3551
  //#endregion
3301
3552
  //#region src/model/annotations/embedded-fields.ts
@@ -5233,7 +5484,7 @@ function buildFormFields(raw, extras) {
5233
5484
  const name = w.data.fieldName;
5234
5485
  if (w.data.subtype !== "Widget" || !name) continue;
5235
5486
  const at = perPage.get(w.page) ?? 0;
5236
- order.set(w, at);
5487
+ order.set(w, w.at ?? at);
5237
5488
  perPage.set(w.page, at + 1);
5238
5489
  namesById.set(w.data.id, name);
5239
5490
  let group = groups.get(name);
@@ -5875,6 +6126,10 @@ async function openDocument(source, options) {
5875
6126
  defaults: options.defaults ?? staticDefaults
5876
6127
  });
5877
6128
  const embedded = [];
6129
+ const linksByPage = /* @__PURE__ */ new Map();
6130
+ const destinationCache = /* @__PURE__ */ new Map();
6131
+ /** The outline, read on the first `outline()` call: most viewers never show it. */
6132
+ let outlineRead;
5878
6133
  let preserved = [];
5879
6134
  /** Embedded annotations whose PDF.js data lacks fields the file holds; read after `ready`. */
5880
6135
  const targets = [];
@@ -5933,15 +6188,21 @@ async function openDocument(source, options) {
5933
6188
  const raw = await (await getPage(i)).getAnnotations().catch(() => {
5934
6189
  return [];
5935
6190
  });
5936
- const widgets = raw.flatMap((data) => data.subtype === "Widget" ? [{
6191
+ const widgets = raw.flatMap((data, at) => data.subtype === "Widget" ? [{
5937
6192
  page: i,
5938
- data
6193
+ data,
6194
+ at
5939
6195
  }] : []);
6196
+ const links = raw.flatMap((data, at) => linkFromPdfjs(i, data, at) ?? []);
5940
6197
  return {
5941
6198
  ...importEmbeddedPage(i, raw, importedAt),
5942
- widgets
6199
+ widgets,
6200
+ links
5943
6201
  };
5944
6202
  });
6203
+ perPage.forEach((r, i) => {
6204
+ if (r.links.length > 0) linksByPage.set(i, r.links);
6205
+ });
5945
6206
  embedded.push(...summarise(perPage.map((r) => r.counts)));
5946
6207
  const fromFile = perPage.flatMap((r) => r.annotations);
5947
6208
  targets.push(...perPage.flatMap((r) => r.targets));
@@ -6096,6 +6357,21 @@ async function openDocument(source, options) {
6096
6357
  };
6097
6358
  },
6098
6359
  annotations: store,
6360
+ links: (page) => linksByPage.get(page) ?? [],
6361
+ resolveDestination(raw) {
6362
+ if (destroyed || !handles) return Promise.resolve(void 0);
6363
+ const key = JSON.stringify(raw);
6364
+ let found = destinationCache.get(key);
6365
+ if (!found) {
6366
+ found = resolveDestination(handles.proxy, raw).catch(() => void 0);
6367
+ destinationCache.set(key, found);
6368
+ }
6369
+ return found;
6370
+ },
6371
+ outline() {
6372
+ outlineRead ??= ready.then(() => destroyed || !handles ? [] : readOutline(handles.proxy));
6373
+ return outlineRead;
6374
+ },
6099
6375
  embedded,
6100
6376
  history,
6101
6377
  form,
@@ -6121,6 +6397,7 @@ async function openDocument(source, options) {
6121
6397
  store.destroy();
6122
6398
  pageCache?.clear();
6123
6399
  searchable.clear();
6400
+ destinationCache.clear();
6124
6401
  if (destroyTask) teardown(destroyTask);
6125
6402
  },
6126
6403
  get _pdfjs() {
@@ -6131,19 +6408,6 @@ async function openDocument(source, options) {
6131
6408
  return doc;
6132
6409
  }
6133
6410
  const EMBEDDED_CONCURRENCY = 8;
6134
- /** Runs `task(0..count-1)` with at most `limit` in flight; results come back in index order. */
6135
- async function mapInOrder(count, limit, task) {
6136
- const results = new Array(count);
6137
- let next = 0;
6138
- const worker = async () => {
6139
- while (next < count) {
6140
- const i = next++;
6141
- results[i] = await task(i);
6142
- }
6143
- };
6144
- await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
6145
- return results;
6146
- }
6147
6411
  function teardown(destroyTask) {
6148
6412
  destroyTask().catch(() => {});
6149
6413
  }
@@ -7648,6 +7912,97 @@ function recogniseStroke(points) {
7648
7912
  };
7649
7913
  }
7650
7914
  //#endregion
7915
+ //#region src/model/storage.ts
7916
+ const copy$1 = (value) => value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
7917
+ /** Values in memory, gone on reload. The default when a host passes no store. */
7918
+ function createMemoryStorage(initial = {}) {
7919
+ const map = new Map(Object.entries(initial).map(([k, v]) => [k, copy$1(v)]));
7920
+ return {
7921
+ get: (key) => copy$1(map.get(key)),
7922
+ set(key, value) {
7923
+ map.set(key, copy$1(value));
7924
+ },
7925
+ remove(key) {
7926
+ map.delete(key);
7927
+ }
7928
+ };
7929
+ }
7930
+ /**
7931
+ * A `localStorage` store whose keys go to storage as `keyOf` maps them. Storage is probed once.
7932
+ * Where it is blocked (it refuses even a tiny test write, as in some private windows) values
7933
+ * stay in memory for the page's life. A full store that already `holdsOurs` is kept, so its
7934
+ * values show and writes reject.
7935
+ */
7936
+ function createLocalStore(keyOf, probeKey, holdsOurs) {
7937
+ const storage = (() => {
7938
+ try {
7939
+ const s = globalThis.localStorage ?? void 0;
7940
+ if (!s) return void 0;
7941
+ try {
7942
+ s.setItem(probeKey, "1");
7943
+ s.removeItem(probeKey);
7944
+ } catch {
7945
+ return holdsOurs(s) ? s : void 0;
7946
+ }
7947
+ return s;
7948
+ } catch {
7949
+ return;
7950
+ }
7951
+ })();
7952
+ if (!storage) return createMemoryStorage();
7953
+ return {
7954
+ get(key) {
7955
+ try {
7956
+ const raw = storage.getItem(keyOf(key));
7957
+ return raw === null ? void 0 : JSON.parse(raw);
7958
+ } catch {
7959
+ return;
7960
+ }
7961
+ },
7962
+ set(key, value) {
7963
+ try {
7964
+ storage.setItem(keyOf(key), JSON.stringify(value));
7965
+ } catch (cause) {
7966
+ return Promise.reject(new Error(`Browser storage could not save "${key}"`, { cause }));
7967
+ }
7968
+ },
7969
+ remove(key) {
7970
+ try {
7971
+ storage.removeItem(keyOf(key));
7972
+ } catch {}
7973
+ }
7974
+ };
7975
+ }
7976
+ /**
7977
+ * Values in `localStorage` as JSON under `<prefix>:<key>`, read on every call so another tab's
7978
+ * write shows. Where storage is blocked the values stay in memory. A `set` that storage
7979
+ * refuses, as when it is full, keeps the old value and returns a rejected promise.
7980
+ */
7981
+ function createLocalStorage(prefix = "inkbar") {
7982
+ const start = `${prefix}:`;
7983
+ return createLocalStore((key) => start + key, `${start}probe`, (s) => {
7984
+ for (let i = 0; i < s.length; i++) if (s.key(i)?.startsWith(start)) return true;
7985
+ return false;
7986
+ });
7987
+ }
7988
+ /**
7989
+ * Like `createLocalStorage`, but keys reach storage exactly as given, with no prefix. Internal:
7990
+ * the signature store keeps its caller's key. `own` is the key whose presence marks a full
7991
+ * store as ours.
7992
+ */
7993
+ function createRawLocalStorage(own) {
7994
+ return createLocalStore((key) => key, `${own}:probe`, (s) => s.getItem(own) !== null);
7995
+ }
7996
+ /** Keys under `<prefix>.`: `get('x')` reads `<prefix>.x`. */
7997
+ function scopeStorage(storage, prefix) {
7998
+ const scoped = (key) => `${prefix}.${key}`;
7999
+ return {
8000
+ get: (key) => storage.get(scoped(key)),
8001
+ set: (key, value) => storage.set(scoped(key), value),
8002
+ remove: (key) => storage.remove(scoped(key))
8003
+ };
8004
+ }
8005
+ //#endregion
7651
8006
  //#region src/model/signatures.ts
7652
8007
  /** How wide a placed signature is, in points. */
7653
8008
  const DEFAULT_SIGNATURE_WIDTH = 150;
@@ -7696,99 +8051,64 @@ function createMemorySignatureStore(initial = []) {
7696
8051
  }
7697
8052
  };
7698
8053
  }
8054
+ /** The id of an entry that may not be a template, if it has one. */
8055
+ const idOf = (x) => typeof x === "object" && x !== null ? x.id : void 0;
7699
8056
  /**
7700
- * Templates in `localStorage` under `key`, as a JSON array. Where storage is blocked (it refuses
7701
- * even a tiny test write, as in some private windows) the templates stay in memory for the
7702
- * page's life, so signing still works. Where storage works, or is full but holds a saved list,
7703
- * and has no room for a change, the change is undone and `save` or `remove` rejects, so the host can say so. Stored entries it
7704
- * cannot read, such as a later version's templates, are left out of `list` and kept on write.
8057
+ * Templates under `key` (default `'signatures'`) in `storage`, as a JSON array, read on every
8058
+ * call so another tab's write shows. Stored entries it cannot read, such as a later version's
8059
+ * templates, are left out of `list`, kept on write, and dropped only when a template takes
8060
+ * their id. When `storage.set` rejects, as when it is full, the list stays as it was and `save`
8061
+ * or `remove` rejects, so the host can say so.
7705
8062
  */
7706
- function createLocalSignatureStore(key) {
7707
- /** Usable storage, or undefined when it is missing or blocked. */
7708
- const storage = (() => {
7709
- try {
7710
- const s = globalThis.localStorage ?? void 0;
7711
- if (!s) return void 0;
7712
- const probe = `${key}:probe`;
7713
- try {
7714
- s.setItem(probe, "1");
7715
- s.removeItem(probe);
7716
- } catch {
7717
- return s.getItem(key) === null ? void 0 : s;
7718
- }
7719
- return s;
7720
- } catch {
7721
- return;
7722
- }
7723
- })();
7724
- /** The stored list; undefined when storage cannot be read. */
7725
- const read = () => {
7726
- if (!storage) return void 0;
7727
- try {
7728
- const raw = storage.getItem(key);
7729
- if (raw === null) return {
7730
- items: [],
7731
- unread: []
7732
- };
7733
- const parsed = JSON.parse(raw);
7734
- if (!Array.isArray(parsed)) return {
7735
- items: [],
7736
- unread: []
7737
- };
7738
- return {
7739
- items: validTemplates(parsed),
7740
- unread: parsed.filter((x) => validTemplates([x]).length === 0)
7741
- };
7742
- } catch (e) {
7743
- return e instanceof SyntaxError ? {
7744
- items: [],
7745
- unread: []
7746
- } : void 0;
7747
- }
7748
- };
7749
- let { items, unread } = read() ?? {
7750
- items: [],
7751
- unread: []
7752
- };
7753
- /** Picks up what another tab wrote. */
7754
- const sync = () => {
7755
- const stored = read();
7756
- if (stored) ({items, unread} = stored);
8063
+ function createStorageSignatureStore(storage, key = "signatures") {
8064
+ /** The stored list: the templates, and the entries that are not (kept, never shown). */
8065
+ const read = async () => {
8066
+ const stored = await storage.get(key);
8067
+ if (!Array.isArray(stored)) return {
8068
+ items: [],
8069
+ unread: []
8070
+ };
8071
+ const parsed = stored;
8072
+ return {
8073
+ items: validTemplates(parsed),
8074
+ unread: parsed.filter((x) => validTemplates([x]).length === 0)
8075
+ };
7757
8076
  };
7758
- /** The id of an entry that may not be a template, if it has one. */
7759
- const idOf = (x) => typeof x === "object" && x !== null ? x.id : void 0;
7760
- /**
7761
- * Writes `next`, then adopts it; leaves everything as it was when storage refuses. Entries
7762
- * it cannot read go back after the templates, less any whose id a template now has.
7763
- */
7764
- const commit = (next) => {
8077
+ /** Writes `next` with the unread entries after it, less any whose id a template now has. */
8078
+ const commit = async (next, unread) => {
7765
8079
  const ids = new Set(next.map((t) => t.id));
7766
8080
  const kept = unread.filter((x) => idOf(x) === void 0 || !ids.has(idOf(x)));
7767
- if (storage) try {
7768
- storage.setItem(key, JSON.stringify([...next, ...kept]));
8081
+ try {
8082
+ await storage.set(key, [...next, ...kept]);
7769
8083
  } catch (cause) {
7770
- return Promise.reject(new Error("The signature could not be saved: browser storage is full", { cause }));
8084
+ throw new Error("The signature could not be saved: browser storage is full", { cause });
7771
8085
  }
7772
- items = next;
7773
- unread = kept;
7774
- return Promise.resolve();
7775
8086
  };
7776
8087
  return {
7777
- list() {
7778
- sync();
7779
- return Promise.resolve(items.map(copy));
8088
+ async list() {
8089
+ return (await read()).items.map(copy);
7780
8090
  },
7781
- save(t) {
7782
- sync();
7783
- return commit(upsert(items, t));
8091
+ async save(t) {
8092
+ const { items, unread } = await read();
8093
+ await commit(upsert(items, t), unread);
7784
8094
  },
7785
- remove(id) {
7786
- sync();
7787
- return commit(items.filter((x) => x.id !== id));
8095
+ async remove(id) {
8096
+ const { items, unread } = await read();
8097
+ await commit(items.filter((x) => x.id !== id), unread);
7788
8098
  }
7789
8099
  };
7790
8100
  }
7791
8101
  /**
8102
+ * Templates in `localStorage` under `key`, as a JSON array. Where storage is blocked the
8103
+ * templates stay in memory for the page's life, so signing still works. Where storage is full
8104
+ * and has no room for a change, `save` or `remove` rejects.
8105
+ *
8106
+ * @deprecated Pass `storage` (for example `createLocalStorage()`) to the viewer instead.
8107
+ */
8108
+ function createLocalSignatureStore(key) {
8109
+ return createStorageSignatureStore(createRawLocalStorage(key), key);
8110
+ }
8111
+ /**
7792
8112
  * Fits strokes into a unit box, for an ink template. Points come in any y-up space; they go
7793
8113
  * out between 0 and 1 on both axes, and `aspect` is the box's width over its height. A flat
7794
8114
  * side is given 1% of the long side (a dot, a square), so the aspect stays finite, and the
@@ -7984,6 +8304,6 @@ function signField(doc, field, widget, t) {
7984
8304
  return id;
7985
8305
  }
7986
8306
  //#endregion
7987
- export { deriveRect as $, pdfNameSet as $t, fromAnnotation as A, PolylineSchema as At, isBubble as B, freeTextBox as Bt, isThreadRoot as C, isInkError as Cn, LoupeSchema as Ct, withoutReplies as D, PointSchema as Dt, threadRoot as E, NoteSchema as Et, toFields as F, SquareSchema as Ft, resizedBox as G, normalizeColor as Gt, keepsAspect as H, withCalloutBounds as Ht, typesInGroup as I, StampSchema as It, translated as J, INTENTS as Jt, resizedRect as K, staticDefaults as Kt, hasCallout as L, TextMarkupSchema as Lt, styleGroupOfType as M, RectSchema as Mt, styleGroupsOf as N, RedactSchema as Nt, STYLE_GROUPS as O, PolySchema as Ot, supportedProps as P, ShapeSchema as Pt, createAnnotationStore as Q, isKnown as Qt, hasCornerHandles as R, FringeSchema as Rt, isReply as S, InkError as Sn, LineSchema as St, threadOf as T, NoteIconSchema as Tt, movedEndpoint as U, parsePdfDate as Ut, isTextMarkup as V, isStandardCallout as Vt, movedTip as W, cssVariableDefaults as Wt, openDocument as X, NOTE_ICONS as Xt, withBox as Y, LINE_ENDINGS as Yt, isBlankQuery as Z, PDF_NAME$1 as Zt, constrain as _, rectsIntersect as _n, GLOSS_SCHEMA_ID as _t, createInkTemplate as a, applyMatrix as an, importEmbeddedPage as at, shapeInput as b, unionRects as bn, InkSchema as bt, createSignatureLibrary as c, multiplyMatrix as cn, summarise as ct, RECOGNISE_THRESHOLD as d, quadsFromFlat as dn, CircleSchema as dt, mergeLineRects as en, exportAnnotations as et, recogniseStroke as f, quadsFromRect as fn, ExportedAnnotationSchema as ft, SHAPE_KINDS as g, rectFromQuads as gn, GLOSS_MEDIA_TYPE as gt, DEFAULT_SHAPE_SIZE as h, rectFromQuad as hn, GLOSS_FORMAT$1 as ht, createImageTemplate as i, writable as in, SUPPORTED_SUBTYPES as it, styleGroupOf as j, QuadSchema as jt, createToolStyle as k, PolygonSchema as kt, normaliseInk as l, pointsFromFlat as ln, AnnotationDocumentSchema as lt, DEFAULT_SHAPE_OPTIONS as m, rectFromPoints as mn, FreeTextStyleSchema as mt, DEFAULT_SIGNATURE_WIDTH as n, derived as nn, createHistory as nt, createLocalSignatureStore as o, approxEqual as on, importedId as ot, DEFAULT_CORNER_RADIUS as p, rectContainsPoint as pn, FreeTextSchema as pt, scaled as q, sameValue$1 as qt, SIGNATURE_INK_WIDTH as r, readonly as rn, isProduction as rt, createMemorySignatureStore as s, invertMatrix as sn, pdfColorToHex as st, signField as t, createRecordStore as tn, importAnnotations as tt, placeSignature as u, quadFromRect as un, AnnotationSchema as ut, defaultEnds as v, translateQuad as vn, GLOSS_VERSION$1 as vt, repliesTo as w, MaskSchema as wt, isAbandonedNote as x, Emitter as xn, LineEndingSchema as xt, isPointerOnlyKind as y, translateRect as yn, IMAGE_DATA_URL as yt, hasRectGeometry as z, calloutStroke as zt };
8307
+ export { withBox as $, INTENTS as $t, threadRoot as A, MaskSchema as At, typesInGroup as B, ShapeSchema as Bt, isPointerOnlyKind as C, rectsIntersect as Cn, GLOSS_SCHEMA_ID as Ct, isThreadRoot as D, Emitter as Dn, LineEndingSchema as Dt, isReply as E, unionRects as En, InkSchema as Et, styleGroupOf as F, PolygonSchema as Ft, isTextMarkup as G, calloutStroke as Gt, hasCornerHandles as H, StampSchema as Ht, styleGroupOfType as I, PolylineSchema as It, movedTip as J, withCalloutBounds as Jt, keepsAspect as K, freeTextBox as Kt, styleGroupsOf as L, QuadSchema as Lt, STYLE_GROUPS as M, NoteSchema as Mt, createToolStyle as N, PointSchema as Nt, repliesTo as O, InkError as On, LineSchema as Ot, fromAnnotation as P, PolySchema as Pt, translated as Q, parsePdfDate as Qt, supportedProps as R, RectSchema as Rt, defaultEnds as S, rectFromQuads as Sn, GLOSS_MEDIA_TYPE as St, isAbandonedNote as T, translateRect as Tn, IMAGE_DATA_URL as Tt, hasRectGeometry as U, TextMarkupSchema as Ut, hasCallout as V, SquareSchema as Vt, isBubble as W, FringeSchema as Wt, resizedRect as X, normalizeColor as Xt, resizedBox as Y, cssVariableDefaults as Yt, scaled as Z, staticDefaults as Zt, DEFAULT_CORNER_RADIUS as _, quadsFromFlat as _n, CircleSchema as _t, createInkTemplate as a, mergeLineRects as an, importAnnotations as at, SHAPE_KINDS as b, rectFromPoints as bn, FreeTextStyleSchema as bt, createSignatureLibrary as c, readonly as cn, sameValue$1 as ct, placeSignature as d, approxEqual as dn, importEmbeddedPage as dt, LINE_ENDINGS as en, openDocument as et, createLocalStorage as f, invertMatrix as fn, importedId as ft, recogniseStroke as g, quadFromRect as gn, AnnotationSchema as gt, RECOGNISE_THRESHOLD as h, quadContainsPoint as hn, AnnotationDocumentSchema as ht, createImageTemplate as i, pdfNameSet as in, exportAnnotations as it, withoutReplies as j, NoteIconSchema as jt, threadOf as k, isInkError as kn, LoupeSchema as kt, createStorageSignatureStore as l, writable as ln, safeUrl as lt, scopeStorage as m, pointsFromFlat as mn, summarise as mt, DEFAULT_SIGNATURE_WIDTH as n, PDF_NAME$1 as nn, createAnnotationStore as nt, createLocalSignatureStore as o, createRecordStore as on, createHistory as ot, createMemoryStorage as p, multiplyMatrix as pn, pdfColorToHex as pt, movedEndpoint as q, isStandardCallout as qt, SIGNATURE_INK_WIDTH as r, isKnown as rn, deriveRect as rt, createMemorySignatureStore as s, derived as sn, isProduction as st, signField as t, NOTE_ICONS as tn, isBlankQuery as tt, normaliseInk as u, applyMatrix as un, SUPPORTED_SUBTYPES as ut, DEFAULT_SHAPE_OPTIONS as v, quadsFromRect as vn, ExportedAnnotationSchema as vt, shapeInput as w, translateQuad as wn, GLOSS_VERSION$1 as wt, constrain as x, rectFromQuad as xn, GLOSS_FORMAT$1 as xt, DEFAULT_SHAPE_SIZE as y, rectContainsPoint as yn, FreeTextSchema as yt, toFields as z, RedactSchema as zt };
7988
8308
 
7989
- //# sourceMappingURL=sign-DZVDJhqT.mjs.map
8309
+ //# sourceMappingURL=sign-BSP7H3b7.mjs.map