@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.
- package/README.md +136 -34
- package/dist/dom.d.mts +290 -16
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +1538 -85
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{sign-DZVDJhqT.mjs → sign-BSP7H3b7.mjs} +1799 -1479
- package/dist/sign-BSP7H3b7.mjs.map +1 -0
- package/dist/{sign-BTbtPHbM.d.mts → sign-UdMGeqRr.d.mts} +161 -13
- package/dist/sign-UdMGeqRr.d.mts.map +1 -0
- package/package.json +3 -3
- package/dist/sign-BTbtPHbM.d.mts.map +0 -1
- package/dist/sign-DZVDJhqT.mjs.map +0 -1
|
@@ -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
|
|
1547
|
-
*
|
|
1548
|
-
* is
|
|
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
|
|
1586
|
+
async function resolveDestination(proxy, raw) {
|
|
1551
1587
|
try {
|
|
1552
|
-
const
|
|
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
|
-
|
|
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/
|
|
1821
|
-
/**
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
if (
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
-
/**
|
|
1857
|
-
function
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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/
|
|
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
|
|
1885
|
-
*
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
-
*
|
|
1898
|
-
*
|
|
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
|
|
1901
|
-
const
|
|
1902
|
-
|
|
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
|
-
|
|
1930
|
-
|
|
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
|
-
*
|
|
1937
|
-
* `
|
|
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
|
|
1941
|
-
|
|
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
|
-
*
|
|
1959
|
-
*
|
|
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
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
-
*
|
|
1970
|
-
* `
|
|
1971
|
-
*
|
|
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
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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/
|
|
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
|
-
*
|
|
1984
|
-
* (
|
|
1985
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
2010
|
-
*
|
|
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
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
}
|
|
2153
|
+
const FreeTextStyleSchema = z.object({
|
|
2154
|
+
...FreeTextStyle.shape,
|
|
2155
|
+
/** The text colour. */
|
|
2156
|
+
color: Color,
|
|
2157
|
+
align: Refused
|
|
2158
|
+
});
|
|
2018
2159
|
/**
|
|
2019
|
-
*
|
|
2020
|
-
* `
|
|
2021
|
-
*
|
|
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
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2045
|
-
*
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
/**
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
|
2476
|
-
function
|
|
2477
|
-
|
|
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
|
-
/** `
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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 "
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
-
|
|
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 "
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
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
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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
|
-
*
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
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
|
-
|
|
2645
|
-
let root;
|
|
2596
|
+
function safeAddress(address) {
|
|
2646
2597
|
try {
|
|
2647
|
-
|
|
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/
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
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
|
-
|
|
2720
|
-
|
|
2721
|
-
opacity: (type) => staticDefaults.opacity(type)
|
|
2645
|
+
kind: "unsupported",
|
|
2646
|
+
reason: "unknown"
|
|
2722
2647
|
};
|
|
2723
2648
|
}
|
|
2724
|
-
/**
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
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
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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/
|
|
2742
|
-
/**
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
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
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2843
|
+
return {
|
|
2844
|
+
values,
|
|
2845
|
+
rejected
|
|
2846
|
+
};
|
|
2758
2847
|
}
|
|
2759
2848
|
//#endregion
|
|
2760
|
-
//#region src/model/
|
|
2849
|
+
//#region src/model/form/submit.ts
|
|
2761
2850
|
/**
|
|
2762
|
-
* The
|
|
2763
|
-
*
|
|
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
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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
|
-
*
|
|
2770
|
-
*
|
|
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
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
-
*
|
|
2784
|
-
*
|
|
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
|
|
2787
|
-
|
|
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
|
-
*
|
|
2791
|
-
*
|
|
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
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
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
|
-
*
|
|
2801
|
-
*
|
|
2802
|
-
*
|
|
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
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
];
|
|
2817
|
-
const
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
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
|
-
*
|
|
2861
|
-
*
|
|
2862
|
-
*
|
|
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
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
const
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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
|
-
*
|
|
2992
|
-
*
|
|
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
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
});
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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
|
-
|
|
3094
|
-
|
|
3095
|
-
return
|
|
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
|
-
/**
|
|
3098
|
-
function
|
|
3099
|
-
const
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
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
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
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
|
-
|
|
3137
|
-
|
|
3138
|
-
if (
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
if (
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
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
|
|
3153
|
-
const
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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
|
|
3162
|
-
const
|
|
3163
|
-
const
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
case "
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
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 "
|
|
3262
|
-
case "
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
-
|
|
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 "
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
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
|
-
|
|
3298
|
-
|
|
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
|
|
7701
|
-
*
|
|
7702
|
-
*
|
|
7703
|
-
*
|
|
7704
|
-
*
|
|
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
|
|
7707
|
-
/**
|
|
7708
|
-
const
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
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
|
-
/**
|
|
7759
|
-
const
|
|
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
|
-
|
|
7768
|
-
storage.
|
|
8081
|
+
try {
|
|
8082
|
+
await storage.set(key, [...next, ...kept]);
|
|
7769
8083
|
} catch (cause) {
|
|
7770
|
-
|
|
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
|
-
|
|
7779
|
-
return Promise.resolve(items.map(copy));
|
|
8088
|
+
async list() {
|
|
8089
|
+
return (await read()).items.map(copy);
|
|
7780
8090
|
},
|
|
7781
|
-
save(t) {
|
|
7782
|
-
|
|
7783
|
-
|
|
8091
|
+
async save(t) {
|
|
8092
|
+
const { items, unread } = await read();
|
|
8093
|
+
await commit(upsert(items, t), unread);
|
|
7784
8094
|
},
|
|
7785
|
-
remove(id) {
|
|
7786
|
-
|
|
7787
|
-
|
|
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 {
|
|
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-
|
|
8309
|
+
//# sourceMappingURL=sign-BSP7H3b7.mjs.map
|