@embedpdf/engine-core 3.0.0-next.13 → 3.0.0-next.15
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/dist/AnnotationList-CkijRHet.d.cts +27163 -0
- package/dist/AnnotationList-CkijRHet.d.ts +27163 -0
- package/dist/Change-CKoxfTNY.js +889 -0
- package/dist/Change-CKoxfTNY.js.map +1 -0
- package/dist/Change-CjImsrN0.cjs +1020 -0
- package/dist/Change-CjImsrN0.cjs.map +1 -0
- package/dist/DocumentRenderService-BfFdFSwI.cjs +120 -0
- package/dist/DocumentRenderService-BfFdFSwI.cjs.map +1 -0
- package/dist/DocumentRenderService-DV3J9lMz.js +85 -0
- package/dist/DocumentRenderService-DV3J9lMz.js.map +1 -0
- package/dist/{DocumentSecurityService-_JRjqvoI.d.ts → DocumentSecurityService-CU1M6Q7m.d.ts} +100 -112
- package/dist/{DocumentSecurityService-ClTZc0zO.d.cts → DocumentSecurityService-e6vp2de8.d.cts} +100 -112
- package/dist/LocalDocumentHandle-D02FMm-Z.cjs +53 -0
- package/dist/LocalDocumentHandle-D02FMm-Z.cjs.map +1 -0
- package/dist/LocalDocumentHandle-DAPdaOt_.js +30 -0
- package/dist/LocalDocumentHandle-DAPdaOt_.js.map +1 -0
- package/dist/LocalEngine-DFoBm4B3.d.cts +1565 -0
- package/dist/LocalEngine-DPriFujS.d.ts +1565 -0
- package/dist/PageInsertBlankInput-CXHAM7l6.cjs +17 -0
- package/dist/PageInsertBlankInput-CXHAM7l6.cjs.map +1 -0
- package/dist/PageInsertBlankInput-DhVYVJ_S.js +12 -0
- package/dist/PageInsertBlankInput-DhVYVJ_S.js.map +1 -0
- package/dist/authority-B3BklsB4.js +2239 -0
- package/dist/authority-B3BklsB4.js.map +1 -0
- package/dist/authority-DLMNR7ff.cjs +2676 -0
- package/dist/authority-DLMNR7ff.cjs.map +1 -0
- package/dist/builders-CNj0POeY.js +161 -0
- package/dist/builders-CNj0POeY.js.map +1 -0
- package/dist/builders-Cgdp_YCn.cjs +184 -0
- package/dist/builders-Cgdp_YCn.cjs.map +1 -0
- package/dist/calibrate-7T3fnP8-.js +2404 -0
- package/dist/calibrate-7T3fnP8-.js.map +1 -0
- package/dist/calibrate-DJBXnhLE.cjs +2919 -0
- package/dist/calibrate-DJBXnhLE.cjs.map +1 -0
- package/dist/charmap-Cprti1ta.js +239 -0
- package/dist/charmap-Cprti1ta.js.map +1 -0
- package/dist/charmap-aYlefQP9.cjs +304 -0
- package/dist/charmap-aYlefQP9.cjs.map +1 -0
- package/dist/conformance.cjs +16753 -5247
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +928 -165
- package/dist/conformance.d.ts +928 -165
- package/dist/conformance.js +16731 -5268
- package/dist/conformance.js.map +1 -1
- package/dist/format-Bp3QFSH1.cjs +574 -0
- package/dist/format-Bp3QFSH1.cjs.map +1 -0
- package/dist/format-Dgg428Kb.js +461 -0
- package/dist/format-Dgg428Kb.js.map +1 -0
- package/dist/index.cjs +330 -103
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -5
- package/dist/pageRefs-B3ur7Ckm.cjs +51 -0
- package/dist/pageRefs-B3ur7Ckm.cjs.map +1 -0
- package/dist/pageRefs-DkbuxSCV.js +40 -0
- package/dist/pageRefs-DkbuxSCV.js.map +1 -0
- package/dist/public.cjs +57 -0
- package/dist/public.d.cts +5 -0
- package/dist/public.d.ts +5 -0
- package/dist/public.js +9 -0
- package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs +3646 -0
- package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs.map +1 -0
- package/dist/resolveAnnotationDraft-CUpf0SMh.js +3167 -0
- package/dist/resolveAnnotationDraft-CUpf0SMh.js.map +1 -0
- package/dist/{resolver-inAVzsWt.js → resolver-CzxmD9Ck.js} +294 -144
- package/dist/resolver-CzxmD9Ck.js.map +1 -0
- package/dist/{resolver-CYVNV4hn.cjs → resolver-D2OCtE8F.cjs} +347 -143
- package/dist/resolver-D2OCtE8F.cjs.map +1 -0
- package/dist/runtime-Cl0v56mm.d.ts +2032 -0
- package/dist/runtime-p1WDzDo4.d.cts +2032 -0
- package/dist/runtime.cjs +733 -284
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +5 -1348
- package/dist/runtime.d.ts +5 -1348
- package/dist/runtime.js +396 -179
- package/dist/runtime.js.map +1 -1
- package/dist/schema-C-gU4vj7.cjs +3034 -0
- package/dist/schema-C-gU4vj7.cjs.map +1 -0
- package/dist/schema-DzlUNk38.js +2081 -0
- package/dist/schema-DzlUNk38.js.map +1 -0
- package/dist/schemas-Berf1swF.cjs +2481 -0
- package/dist/schemas-Berf1swF.cjs.map +1 -0
- package/dist/schemas-D6xOGjqg.js +1654 -0
- package/dist/schemas-D6xOGjqg.js.map +1 -0
- package/dist/subtype-CG4WpIRj.js +129 -0
- package/dist/subtype-CG4WpIRj.js.map +1 -0
- package/dist/subtype-aDt5TFmK.cjs +158 -0
- package/dist/subtype-aDt5TFmK.cjs.map +1 -0
- package/dist/validation.cjs +578 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +174 -0
- package/dist/validation.d.ts +174 -0
- package/dist/validation.js +571 -0
- package/dist/validation.js.map +1 -0
- package/dist/value-BFS1R8ek.cjs +435 -0
- package/dist/value-BFS1R8ek.cjs.map +1 -0
- package/dist/value-Dhc8eYcx.js +340 -0
- package/dist/value-Dhc8eYcx.js.map +1 -0
- package/dist/viewport-BJW13EGZ.js +540 -0
- package/dist/viewport-BJW13EGZ.js.map +1 -0
- package/dist/viewport-DlL5Bxbg.cjs +671 -0
- package/dist/viewport-DlL5Bxbg.cjs.map +1 -0
- package/dist/wire.cjs +1056 -379
- package/dist/wire.cjs.map +1 -1
- package/dist/wire.d.cts +15069 -725
- package/dist/wire.d.ts +15069 -725
- package/dist/wire.js +825 -239
- package/dist/wire.js.map +1 -1
- package/package.json +10 -1
- package/dist/AbortError-Cq2igpZ6.cjs +0 -38
- package/dist/AbortError-Cq2igpZ6.cjs.map +0 -1
- package/dist/AbortError-DHqPBnYc.js +0 -27
- package/dist/AbortError-DHqPBnYc.js.map +0 -1
- package/dist/AnnotationListSnapshot-D3mFut1e.d.cts +0 -6871
- package/dist/AnnotationListSnapshot-D3mFut1e.d.ts +0 -6871
- package/dist/Engine-Bx0NOfmd.d.cts +0 -1264
- package/dist/Engine-DxtpCYcP.d.ts +0 -1264
- package/dist/PageInsertBlankInput-CqXBmFhN.cjs +0 -449
- package/dist/PageInsertBlankInput-CqXBmFhN.cjs.map +0 -1
- package/dist/PageInsertBlankInput-cHq9-i-z.js +0 -378
- package/dist/PageInsertBlankInput-cHq9-i-z.js.map +0 -1
- package/dist/builders-Bqm8yTpi.cjs +0 -3564
- package/dist/builders-Bqm8yTpi.cjs.map +0 -1
- package/dist/builders-Dqo2X-dt.js +0 -3121
- package/dist/builders-Dqo2X-dt.js.map +0 -1
- package/dist/fold-2V29cxCV.cjs +0 -447
- package/dist/fold-2V29cxCV.cjs.map +0 -1
- package/dist/fold-CdEJTBTn.js +0 -334
- package/dist/fold-CdEJTBTn.js.map +0 -1
- package/dist/resolver-CYVNV4hn.cjs.map +0 -1
- package/dist/resolver-inAVzsWt.js.map +0 -1
- package/dist/schemas-BLARNCgx.js +0 -3113
- package/dist/schemas-BLARNCgx.js.map +0 -1
- package/dist/schemas-DIqFMmrC.cjs +0 -4468
- package/dist/schemas-DIqFMmrC.cjs.map +0 -1
|
@@ -0,0 +1,2404 @@
|
|
|
1
|
+
import { $ as ANNOTATION_FIELD_SPACES_BY_NAME, A as shapeFieldsOf, G as assertRowResources, H as assertKnownFields, J as isBundlePageRef, O as DRAWN_RECT_KINDS, P as semanticEqual, Q as ANNOTATION_FIELD_SPACES, U as assertResourceIds, V as assertBundlePages, W as assertResourcesNamed, Y as isRecord, _t as pdfQuadBounds, dt as pdfPointsBounds, ft as pdfTurnOfDrawn, j as UNBAKED_KINDS, k as pdfShapeForRect, l as pdfResolveRectCommand, lt as pdfPointTurned, mt as isSamePdfRect, n as pdfPredictAnnotationDraft, nt as kindFactsOf, o as pdfPredictAnnotationPatch, ot as DEFAULT_RICH_TEXT_BODY, pt as pdfTurnOfUpright, q as invalidBundle, tt as ANNOTATION_RESOURCE_ROLES, ut as pdfPointUnturned, v as assertWritableMeasure, wt as pdfRectTurnedBounds } from "./resolveAnnotationDraft-CUpf0SMh.js";
|
|
2
|
+
import { h as EngineError, u as encodePageKey, v as _defineProperty, y as EngineErrorCode } from "./format-Dgg428Kb.js";
|
|
3
|
+
import { c as toBase64, o as decodedLengthOf, s as fromBase64 } from "./charmap-Cprti1ta.js";
|
|
4
|
+
//#region src/geometry/renderSize.ts
|
|
5
|
+
/**
|
|
6
|
+
* The pixel size of a render: an area of `width` × `height` points, turned
|
|
7
|
+
* by `rotation` (width and height swap on a quarter turn) and sized by
|
|
8
|
+
* `viewport`. The local worker allocates exactly this; the cloud client
|
|
9
|
+
* reports it without rendering. `InvalidArg` for a viewport that isn't
|
|
10
|
+
* positive.
|
|
11
|
+
*/
|
|
12
|
+
function renderSize(area, rotation, viewport) {
|
|
13
|
+
const swap = rotation === 90 || rotation === 270;
|
|
14
|
+
const baseWidth = swap ? area.height : area.width;
|
|
15
|
+
const baseHeight = swap ? area.width : area.height;
|
|
16
|
+
if (viewport.kind === "width") {
|
|
17
|
+
if (!Number.isFinite(viewport.width) || viewport.width <= 0) throw new EngineError(EngineErrorCode.InvalidArg, "render viewport width must be positive");
|
|
18
|
+
const width = Math.max(1, Math.round(viewport.width));
|
|
19
|
+
return {
|
|
20
|
+
width,
|
|
21
|
+
height: Math.max(1, Math.round(width * baseHeight / baseWidth))
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const scale = viewport.scale ?? 1;
|
|
25
|
+
if (!Number.isFinite(scale) || scale <= 0) throw new EngineError(EngineErrorCode.InvalidArg, "render viewport scale must be positive");
|
|
26
|
+
return {
|
|
27
|
+
width: Math.max(1, Math.round(baseWidth * scale)),
|
|
28
|
+
height: Math.max(1, Math.round(baseHeight * scale))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/geometry/pageTransform.ts
|
|
33
|
+
/**
|
|
34
|
+
* The transform of the image a render with `options` makes of `page`, without
|
|
35
|
+
* rendering: its size, rounding included, and the conversion between page
|
|
36
|
+
* space and its pixels. Takes the page's layout (`doc.pages.list()`).
|
|
37
|
+
* `InvalidArg` for a viewport that isn't positive or a target without area.
|
|
38
|
+
*/
|
|
39
|
+
function pageTransform(page, options = {}) {
|
|
40
|
+
const target = options.target;
|
|
41
|
+
return renderAreaTransform(target?.kind === "rect" ? renderTargetArea(target.rect) : {
|
|
42
|
+
x: 0,
|
|
43
|
+
y: 0,
|
|
44
|
+
width: page.size.width,
|
|
45
|
+
height: page.size.height
|
|
46
|
+
}, options);
|
|
47
|
+
}
|
|
48
|
+
/** The transform of a render of `area` with `options`' turn and size. */
|
|
49
|
+
function renderAreaTransform(area, options) {
|
|
50
|
+
const rotation = options.rotation ?? 0;
|
|
51
|
+
const { width, height } = renderSize(area, rotation, options.viewport ?? { kind: "scale" });
|
|
52
|
+
return renderTransform(area, rotation, width, height);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A render target's area with its sides in order, as the renderer takes it.
|
|
56
|
+
* `InvalidArg` for a rect without area, or one that isn't numbers.
|
|
57
|
+
*/
|
|
58
|
+
function renderTargetArea(rect) {
|
|
59
|
+
const { x, y, width, height } = rect;
|
|
60
|
+
if (![
|
|
61
|
+
x,
|
|
62
|
+
y,
|
|
63
|
+
width,
|
|
64
|
+
height
|
|
65
|
+
].every(Number.isFinite) || width === 0 || height === 0) throw new EngineError(EngineErrorCode.InvalidArg, "render rect must have positive area");
|
|
66
|
+
return {
|
|
67
|
+
x: Math.min(x, x + width),
|
|
68
|
+
y: Math.min(y, y + height),
|
|
69
|
+
width: Math.abs(width),
|
|
70
|
+
height: Math.abs(height)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The matrix that draws `area` of the page, turned clockwise by `rotation`,
|
|
75
|
+
* onto `width` × `height` pixels. The renderer draws with it.
|
|
76
|
+
*/
|
|
77
|
+
function renderMatrix(area, rotation, width, height) {
|
|
78
|
+
const { x, y } = area;
|
|
79
|
+
const w = area.width;
|
|
80
|
+
const h = area.height;
|
|
81
|
+
switch (rotation) {
|
|
82
|
+
case 0: return [
|
|
83
|
+
width / w,
|
|
84
|
+
0,
|
|
85
|
+
0,
|
|
86
|
+
height / h,
|
|
87
|
+
-width / w * x,
|
|
88
|
+
-height / h * y
|
|
89
|
+
];
|
|
90
|
+
case 90: return [
|
|
91
|
+
0,
|
|
92
|
+
height / w,
|
|
93
|
+
-width / h,
|
|
94
|
+
0,
|
|
95
|
+
width / h * (y + h),
|
|
96
|
+
-height / w * x
|
|
97
|
+
];
|
|
98
|
+
case 180: return [
|
|
99
|
+
-width / w,
|
|
100
|
+
0,
|
|
101
|
+
0,
|
|
102
|
+
-height / h,
|
|
103
|
+
width / w * (x + w),
|
|
104
|
+
height / h * (y + h)
|
|
105
|
+
];
|
|
106
|
+
case 270: return [
|
|
107
|
+
0,
|
|
108
|
+
-height / w,
|
|
109
|
+
width / h,
|
|
110
|
+
0,
|
|
111
|
+
-width / h * y,
|
|
112
|
+
height / w * (x + w)
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The transform of a render of `area`, turned by `rotation`, drawn onto `width` × `height` pixels. */
|
|
117
|
+
function renderTransform(area, rotation, width, height) {
|
|
118
|
+
const matrix = renderMatrix(area, rotation, width, height);
|
|
119
|
+
const inverse = inverseOf(matrix);
|
|
120
|
+
return {
|
|
121
|
+
width,
|
|
122
|
+
height,
|
|
123
|
+
matrix,
|
|
124
|
+
pageToPixels: ((value) => mapped(matrix, value)),
|
|
125
|
+
pixelsToPage: ((value) => mapped(inverse, value))
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function inverseOf([a, b, c, d, e, f]) {
|
|
129
|
+
const det = a * d - b * c;
|
|
130
|
+
return [
|
|
131
|
+
d / det,
|
|
132
|
+
-b / det,
|
|
133
|
+
-c / det,
|
|
134
|
+
a / det,
|
|
135
|
+
(c * f - d * e) / det,
|
|
136
|
+
(b * e - a * f) / det
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
function mappedPoint(m, { x, y }) {
|
|
140
|
+
return {
|
|
141
|
+
x: m[0] * x + m[2] * y + m[4],
|
|
142
|
+
y: m[1] * x + m[3] * y + m[5]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** A point, a box or a quad through `m`. A box stays a box: every turn is a quarter turn. */
|
|
146
|
+
function mapped(m, value) {
|
|
147
|
+
if ("upperLeft" in value) return {
|
|
148
|
+
upperLeft: mappedPoint(m, value.upperLeft),
|
|
149
|
+
upperRight: mappedPoint(m, value.upperRight),
|
|
150
|
+
lowerLeft: mappedPoint(m, value.lowerLeft),
|
|
151
|
+
lowerRight: mappedPoint(m, value.lowerRight)
|
|
152
|
+
};
|
|
153
|
+
if ("width" in value) {
|
|
154
|
+
const from = mappedPoint(m, value);
|
|
155
|
+
const to = mappedPoint(m, {
|
|
156
|
+
x: value.x + value.width,
|
|
157
|
+
y: value.y + value.height
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
x: Math.min(from.x, to.x),
|
|
161
|
+
y: Math.min(from.y, to.y),
|
|
162
|
+
width: Math.abs(to.x - from.x),
|
|
163
|
+
height: Math.abs(to.y - from.y)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return mappedPoint(m, value);
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/geometry/pageSpace.ts
|
|
170
|
+
/** A point in the file's coordinates, in page space. `visible` is the page's visible box. */
|
|
171
|
+
function pagePointOf(point, visible) {
|
|
172
|
+
return {
|
|
173
|
+
x: point.x - visible.left,
|
|
174
|
+
y: visible.top - point.y
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** A page-space point in the file's coordinates. */
|
|
178
|
+
function pdfPointOf(point, visible) {
|
|
179
|
+
return {
|
|
180
|
+
x: point.x + visible.left,
|
|
181
|
+
y: visible.top - point.y
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** A normalized rect in the file's coordinates, in page space. */
|
|
185
|
+
function pageBoxOf(rect, visible) {
|
|
186
|
+
return {
|
|
187
|
+
x: rect.left - visible.left,
|
|
188
|
+
y: visible.top - rect.top,
|
|
189
|
+
width: rect.right - rect.left,
|
|
190
|
+
height: rect.top - rect.bottom
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** A page-space box in the file's coordinates, keys in `normalizePdfRect`'s order. */
|
|
194
|
+
function pdfRectOf(box, visible) {
|
|
195
|
+
const left = box.x + visible.left;
|
|
196
|
+
const top = visible.top - box.y;
|
|
197
|
+
return {
|
|
198
|
+
left,
|
|
199
|
+
right: left + box.width,
|
|
200
|
+
bottom: top - box.height,
|
|
201
|
+
top
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function pageQuadOf(quad, visible) {
|
|
205
|
+
return {
|
|
206
|
+
upperLeft: pagePointOf(quad.upperLeft, visible),
|
|
207
|
+
upperRight: pagePointOf(quad.upperRight, visible),
|
|
208
|
+
lowerLeft: pagePointOf(quad.lowerLeft, visible),
|
|
209
|
+
lowerRight: pagePointOf(quad.lowerRight, visible)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function pdfQuadOf(quad, visible) {
|
|
213
|
+
return {
|
|
214
|
+
upperLeft: pdfPointOf(quad.upperLeft, visible),
|
|
215
|
+
upperRight: pdfPointOf(quad.upperRight, visible),
|
|
216
|
+
lowerLeft: pdfPointOf(quad.lowerLeft, visible),
|
|
217
|
+
lowerRight: pdfPointOf(quad.lowerRight, visible)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** A page-space point flipped into y-up coordinates. */
|
|
221
|
+
const mirroredPoint = (point) => ({
|
|
222
|
+
x: point.x,
|
|
223
|
+
y: -point.y
|
|
224
|
+
});
|
|
225
|
+
/** A y-up point flipped back into page space. */
|
|
226
|
+
const unmirroredPoint = (point) => ({
|
|
227
|
+
x: point.x,
|
|
228
|
+
y: -point.y
|
|
229
|
+
});
|
|
230
|
+
/** A page-space box flipped into a y-up rect. */
|
|
231
|
+
const mirroredRect = (box) => ({
|
|
232
|
+
left: box.x,
|
|
233
|
+
right: box.x + box.width,
|
|
234
|
+
bottom: -(box.y + box.height),
|
|
235
|
+
top: -box.y
|
|
236
|
+
});
|
|
237
|
+
/** A y-up rect flipped back into a page-space box. */
|
|
238
|
+
const unmirroredBox = (rect) => ({
|
|
239
|
+
x: rect.left,
|
|
240
|
+
y: -rect.top,
|
|
241
|
+
width: rect.right - rect.left,
|
|
242
|
+
height: rect.top - rect.bottom
|
|
243
|
+
});
|
|
244
|
+
const mirroredQuad = (quad) => ({
|
|
245
|
+
upperLeft: mirroredPoint(quad.upperLeft),
|
|
246
|
+
upperRight: mirroredPoint(quad.upperRight),
|
|
247
|
+
lowerLeft: mirroredPoint(quad.lowerLeft),
|
|
248
|
+
lowerRight: mirroredPoint(quad.lowerRight)
|
|
249
|
+
});
|
|
250
|
+
const unmirroredQuad = (quad) => ({
|
|
251
|
+
upperLeft: unmirroredPoint(quad.upperLeft),
|
|
252
|
+
upperRight: unmirroredPoint(quad.upperRight),
|
|
253
|
+
lowerLeft: unmirroredPoint(quad.lowerLeft),
|
|
254
|
+
lowerRight: unmirroredPoint(quad.lowerRight)
|
|
255
|
+
});
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/dto/PageGeometrySnapshot.ts
|
|
258
|
+
/** Narrowing guard: is this run the rotated (non-upright) variant? */
|
|
259
|
+
function isRotatedGeometryRun(run) {
|
|
260
|
+
return "rotation" in run;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Uniform oriented-cell view over either run variant, in the file's
|
|
264
|
+
* coordinates: the glyph's loose cell as a quad (synthesized from the box
|
|
265
|
+
* corners when the run is upright).
|
|
266
|
+
*/
|
|
267
|
+
function glyphLooseQuad(run, index) {
|
|
268
|
+
if (isRotatedGeometryRun(run)) return run.glyphs[index].loose;
|
|
269
|
+
const b = run.glyphs[index].loose;
|
|
270
|
+
return {
|
|
271
|
+
upperLeft: {
|
|
272
|
+
x: b.left,
|
|
273
|
+
y: b.top
|
|
274
|
+
},
|
|
275
|
+
upperRight: {
|
|
276
|
+
x: b.right,
|
|
277
|
+
y: b.top
|
|
278
|
+
},
|
|
279
|
+
lowerLeft: {
|
|
280
|
+
x: b.left,
|
|
281
|
+
y: b.bottom
|
|
282
|
+
},
|
|
283
|
+
lowerRight: {
|
|
284
|
+
x: b.right,
|
|
285
|
+
y: b.bottom
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Uniform AABB view over either run variant, in the file's coordinates: the
|
|
291
|
+
* glyph's loose box, or the
|
|
292
|
+
* enclosing bounds of its oriented cell when the run is rotated. The
|
|
293
|
+
* conservative envelope for consumers that genuinely want a box (culling,
|
|
294
|
+
* scroll targets) — never a substitute for handling orientation in geometry
|
|
295
|
+
* that gets drawn.
|
|
296
|
+
*/
|
|
297
|
+
function glyphLooseBounds(run, index) {
|
|
298
|
+
if (isRotatedGeometryRun(run)) return pdfQuadBounds(run.glyphs[index].loose);
|
|
299
|
+
return run.glyphs[index].loose;
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/promise/AbortError.ts
|
|
303
|
+
/**
|
|
304
|
+
* What a cancelled call rejects with: an `EngineError` with code `Aborted`,
|
|
305
|
+
* named `AbortError` like the DOM's, so both `EngineError.is(err,
|
|
306
|
+
* EngineErrorCode.Aborted)` and `err.name === 'AbortError'` recognise it.
|
|
307
|
+
*/
|
|
308
|
+
var AbortError = class extends EngineError {
|
|
309
|
+
constructor(reason) {
|
|
310
|
+
super(EngineErrorCode.Aborted, reasonMessage(reason));
|
|
311
|
+
_defineProperty(this, "name", "AbortError");
|
|
312
|
+
_defineProperty(this, "reason", void 0);
|
|
313
|
+
this.reason = reason;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
function isAbortError(value) {
|
|
317
|
+
return value instanceof AbortError || value?.name === "AbortError";
|
|
318
|
+
}
|
|
319
|
+
function reasonMessage(reason) {
|
|
320
|
+
if (reason == null) return "aborted";
|
|
321
|
+
if (typeof reason === "string") return reason;
|
|
322
|
+
if (reason instanceof Error) return reason.message || "aborted";
|
|
323
|
+
try {
|
|
324
|
+
return JSON.stringify(reason);
|
|
325
|
+
} catch {
|
|
326
|
+
return "aborted";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/resource/BinarySource.ts
|
|
331
|
+
function isBlob$1(value) {
|
|
332
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Copy a Uint8Array view into a fresh, exactly-sized ArrayBuffer the call
|
|
336
|
+
* owns. Always copies — even when the view spans its whole buffer — because
|
|
337
|
+
* the result may be transferred to a worker (detaching it), and the view is
|
|
338
|
+
* the caller's. Returning `view.buffer` here would let one transfer zero
|
|
339
|
+
* the caller's bytes (a stamp library's Uint8Array became single-use).
|
|
340
|
+
* Callers wanting a zero-copy hand-off need an explicit "give" form, as
|
|
341
|
+
* `pages.insert` has for bare ArrayBuffers; `BinarySource` has none.
|
|
342
|
+
*/
|
|
343
|
+
function toOwnedArrayBuffer(view) {
|
|
344
|
+
const copy = new ArrayBuffer(view.byteLength);
|
|
345
|
+
new Uint8Array(copy).set(view);
|
|
346
|
+
return copy;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Resolve any accepted `BinarySource` form into a `WireResource`.
|
|
350
|
+
* Async because Blob bytes can only be read asynchronously.
|
|
351
|
+
*/
|
|
352
|
+
async function resolveBinarySource(source) {
|
|
353
|
+
if (source instanceof Uint8Array) return { bytes: toOwnedArrayBuffer(source) };
|
|
354
|
+
if (isBlob$1(source)) {
|
|
355
|
+
const blobName = blobFileName(source);
|
|
356
|
+
return {
|
|
357
|
+
bytes: await source.arrayBuffer(),
|
|
358
|
+
mimeType: source.type || void 0,
|
|
359
|
+
...blobName !== void 0 ? { name: blobName } : {}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const { data, mimeType, name } = source;
|
|
363
|
+
const inner = data instanceof Uint8Array ? {
|
|
364
|
+
bytes: toOwnedArrayBuffer(data),
|
|
365
|
+
mimeType: void 0,
|
|
366
|
+
name: void 0
|
|
367
|
+
} : {
|
|
368
|
+
bytes: await data.arrayBuffer(),
|
|
369
|
+
mimeType: data.type || void 0,
|
|
370
|
+
name: blobFileName(data)
|
|
371
|
+
};
|
|
372
|
+
const resolvedName = name ?? inner.name;
|
|
373
|
+
return {
|
|
374
|
+
bytes: inner.bytes,
|
|
375
|
+
...mimeType ?? inner.mimeType ? { mimeType: mimeType ?? inner.mimeType } : {},
|
|
376
|
+
...resolvedName !== void 0 ? { name: resolvedName } : {}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** A browser `File` is a Blob with a `name` — pick it up so attachment
|
|
380
|
+
* drafts can pass a `File` directly without repeating the file name. */
|
|
381
|
+
function blobFileName(blob) {
|
|
382
|
+
const name = blob.name;
|
|
383
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/resource/binaryMetadata.ts
|
|
387
|
+
/**
|
|
388
|
+
* Detect format (and intrinsic dimensions for rasters) of a binary buffer.
|
|
389
|
+
*
|
|
390
|
+
* @returns metadata, or `null` for unsupported/corrupt data
|
|
391
|
+
*/
|
|
392
|
+
function sniffBinaryMetadata(buffer) {
|
|
393
|
+
if (buffer.byteLength < 4) return null;
|
|
394
|
+
const bytes = new Uint8Array(buffer);
|
|
395
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return parsePng(buffer);
|
|
396
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return parseJpeg(buffer);
|
|
397
|
+
if (bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70) return { mimeType: "application/pdf" };
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function parsePng(buffer) {
|
|
401
|
+
if (buffer.byteLength < 24) return null;
|
|
402
|
+
const view = new DataView(buffer);
|
|
403
|
+
const width = view.getUint32(16);
|
|
404
|
+
const height = view.getUint32(20);
|
|
405
|
+
if (width === 0 || height === 0) return null;
|
|
406
|
+
return {
|
|
407
|
+
mimeType: "image/png",
|
|
408
|
+
width,
|
|
409
|
+
height
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function parseJpeg(buffer) {
|
|
413
|
+
const bytes = new Uint8Array(buffer);
|
|
414
|
+
let offset = 2;
|
|
415
|
+
while (offset + 4 < bytes.byteLength) {
|
|
416
|
+
if (bytes[offset] !== 255) return null;
|
|
417
|
+
const marker = bytes[offset + 1];
|
|
418
|
+
if (marker >= 192 && marker <= 195) {
|
|
419
|
+
if (offset + 9 > bytes.byteLength) return null;
|
|
420
|
+
const view = new DataView(buffer, offset + 5);
|
|
421
|
+
const height = view.getUint16(0);
|
|
422
|
+
const width = view.getUint16(2);
|
|
423
|
+
if (width === 0 || height === 0) return null;
|
|
424
|
+
return {
|
|
425
|
+
mimeType: "image/jpeg",
|
|
426
|
+
width,
|
|
427
|
+
height
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (marker === 216 || marker === 217) offset += 2;
|
|
431
|
+
else if (marker === 255) offset += 1;
|
|
432
|
+
else {
|
|
433
|
+
const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
434
|
+
offset += 2 + segLen;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/annotation/resources.ts
|
|
441
|
+
/**
|
|
442
|
+
* Bytes travel beside an annotation's data, never inside it:
|
|
443
|
+
* `create(data, resources)` and `update(ref, patch, resources)`. A read
|
|
444
|
+
* returns the data only. Each role names what the bytes are for; a kind
|
|
445
|
+
* declares the roles it takes (`kinds/<kind>/declaration.ts`).
|
|
446
|
+
*/
|
|
447
|
+
/**
|
|
448
|
+
* What bytes beside an annotation's data can be for:
|
|
449
|
+
*
|
|
450
|
+
* - `appearance`: what the box shows before the rotation the data describes,
|
|
451
|
+
* PNG, JPEG or a one-page PDF, scaled into the box as the data's `fit` says.
|
|
452
|
+
* A stamp requires it; on update it replaces the drawing.
|
|
453
|
+
* - `file`: an attached file's exact bytes; its name, MIME type and
|
|
454
|
+
* description are the data's `file`. A file attachment requires it; on
|
|
455
|
+
* update it replaces the bytes.
|
|
456
|
+
*/
|
|
457
|
+
const ANNOTATION_RESOURCE_ROLE_NAMES = ["appearance", "file"];
|
|
458
|
+
/**
|
|
459
|
+
* Copy each resource into bytes the write owns: the local engine transfers
|
|
460
|
+
* them to its worker, which detaches the buffer, and the caller's bytes are
|
|
461
|
+
* only borrowed. An appearance that isn't PNG, JPEG or PDF fails here,
|
|
462
|
+
* before anything is sent; the format comes from the bytes.
|
|
463
|
+
*/
|
|
464
|
+
async function resolveAnnotationResources(resources) {
|
|
465
|
+
if (!resources) return {};
|
|
466
|
+
for (const role of Object.keys(resources)) if (!ANNOTATION_RESOURCE_ROLE_NAMES.includes(role)) throw new EngineError(EngineErrorCode.InvalidArg, `unknown annotation resource '${role}'; expected ${ANNOTATION_RESOURCE_ROLE_NAMES.join(" or ")}`);
|
|
467
|
+
const wire = {};
|
|
468
|
+
for (const role of ANNOTATION_RESOURCE_ROLE_NAMES) {
|
|
469
|
+
const data = resources[role];
|
|
470
|
+
if (data === void 0) continue;
|
|
471
|
+
const bytes = await ownedBytes(data);
|
|
472
|
+
if (role === "appearance" && !sniffBinaryMetadata(bytes)) throw new EngineError(EngineErrorCode.InvalidArg, "the appearance resource must be PNG, JPEG or one-page PDF bytes");
|
|
473
|
+
wire[role] = bytes;
|
|
474
|
+
}
|
|
475
|
+
return wire;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A file-attachment create's `file`, completed from its `file` resource when
|
|
479
|
+
* that is a `File` or a `Blob`: the name a `File` brings and the type a
|
|
480
|
+
* `Blob` does, where the data leaves them out. A `Blob` with no type gives
|
|
481
|
+
* none. Other drafts, and bare bytes, pass as they are.
|
|
482
|
+
*/
|
|
483
|
+
function withFileFromResource(draft, resources) {
|
|
484
|
+
const data = resources?.file;
|
|
485
|
+
if (draft.subtype !== "file-attachment" || !isBlob(data)) return draft;
|
|
486
|
+
const name = draft.file?.name ?? blobFileName(data);
|
|
487
|
+
if (name === void 0) return draft;
|
|
488
|
+
return {
|
|
489
|
+
...draft,
|
|
490
|
+
file: {
|
|
491
|
+
...draft.file,
|
|
492
|
+
name,
|
|
493
|
+
mimeType: draft.file?.mimeType !== void 0 ? draft.file.mimeType : data.type || null
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function hasAnnotationResources(resources) {
|
|
498
|
+
return ANNOTATION_RESOURCE_ROLE_NAMES.some((role) => resources[role] !== void 0);
|
|
499
|
+
}
|
|
500
|
+
/** The buffers of a write's resources: what a worker transfer list hands over. */
|
|
501
|
+
function annotationResourceBuffers(resources) {
|
|
502
|
+
return Object.values(resources).filter((bytes) => bytes !== void 0);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Check a write's resources against its kind: a role the kind doesn't take
|
|
506
|
+
* is refused, and so is a create without a role the kind requires.
|
|
507
|
+
*/
|
|
508
|
+
function assertAnnotationResources(subtype, resources, write) {
|
|
509
|
+
const roles = ANNOTATION_RESOURCE_ROLES[subtype] ?? {};
|
|
510
|
+
for (const role of ANNOTATION_RESOURCE_ROLE_NAMES) {
|
|
511
|
+
const given = resources?.[role] !== void 0;
|
|
512
|
+
if (given && roles[role] === void 0) throw new EngineError(EngineErrorCode.InvalidArg, `a ${subtype} annotation takes no '${role}' resource`);
|
|
513
|
+
if (!given && write === "create" && roles[role] === "required") throw new EngineError(EngineErrorCode.InvalidArg, `creating a ${subtype} annotation needs its '${role}' resource`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function isBlob(value) {
|
|
517
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
518
|
+
}
|
|
519
|
+
async function ownedBytes(data) {
|
|
520
|
+
if (isBlob(data)) return data.arrayBuffer();
|
|
521
|
+
const copy = new ArrayBuffer(data.byteLength);
|
|
522
|
+
new Uint8Array(copy).set(data);
|
|
523
|
+
return copy;
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/transfer/bundleLimits.ts
|
|
527
|
+
const DEFAULT_BUNDLE_LIMITS = Object.freeze({
|
|
528
|
+
bundleBytes: 50 * 1024 * 1024,
|
|
529
|
+
manifestBytes: 16 * 1024 * 1024,
|
|
530
|
+
items: 1e4,
|
|
531
|
+
pages: 2e3,
|
|
532
|
+
resources: 2e3,
|
|
533
|
+
resourceBytes: 50 * 1024 * 1024,
|
|
534
|
+
imagePixels: 4e7
|
|
535
|
+
});
|
|
536
|
+
/** The bytes of a bundle's manifest: its pages and rows (`{ pages, items }`) as UTF-8 JSON. */
|
|
537
|
+
function manifestBytesOf(rows) {
|
|
538
|
+
return new TextEncoder().encode(JSON.stringify(rows)).length;
|
|
539
|
+
}
|
|
540
|
+
/** Refuse `value` when it is past the limit named `limit`. */
|
|
541
|
+
function assertWithinLimit(kind, limits, limit, value) {
|
|
542
|
+
const max = limits[limit];
|
|
543
|
+
if (value <= max) return;
|
|
544
|
+
throw new EngineError(EngineErrorCode.PayloadTooLarge, `the ${kind} bundle is past its ${limit} limit: ${value} > ${max}`, { details: {
|
|
545
|
+
limit,
|
|
546
|
+
max,
|
|
547
|
+
value
|
|
548
|
+
} });
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/transfer/codec.ts
|
|
552
|
+
/** A resource's entry in the file besides its base64: its id, quotes and separators. */
|
|
553
|
+
const RESOURCE_ENTRY_CHARACTERS = 81;
|
|
554
|
+
/** The file's own keys and punctuation. */
|
|
555
|
+
const ENVELOPE_CHARACTERS = 256;
|
|
556
|
+
const A_BUNDLE = {
|
|
557
|
+
annotation: "an annotation bundle",
|
|
558
|
+
form: "a form bundle"
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* The JSON file of one family's bundles: a bundle whole, to store or
|
|
562
|
+
* download. HTTP carries bundles as multipart instead, without the third
|
|
563
|
+
* base64 adds.
|
|
564
|
+
*/
|
|
565
|
+
function bundleCodec(spec) {
|
|
566
|
+
const notABundle = (cause) => new EngineError(EngineErrorCode.InvalidArg, `the file is not ${A_BUNDLE[spec.kind]}`, cause === void 0 ? void 0 : { cause });
|
|
567
|
+
return {
|
|
568
|
+
stringify(bundle, options) {
|
|
569
|
+
const limits = options?.limits ?? DEFAULT_BUNDLE_LIMITS;
|
|
570
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
571
|
+
for (const [id, bytes] of Object.entries(bundle.resources)) sizes.set(id, bytes.length);
|
|
572
|
+
spec.assertManifest(bundle, sizes, limits);
|
|
573
|
+
const file = {};
|
|
574
|
+
for (const key of spec.keys) file[key] = bundle[key];
|
|
575
|
+
const resources = {};
|
|
576
|
+
for (const [id, bytes] of Object.entries(bundle.resources)) resources[id] = toBase64(bytes);
|
|
577
|
+
file.resources = resources;
|
|
578
|
+
return JSON.stringify(file);
|
|
579
|
+
},
|
|
580
|
+
async parse(text, options) {
|
|
581
|
+
const limits = options?.limits ?? DEFAULT_BUNDLE_LIMITS;
|
|
582
|
+
const maxLength = Math.ceil(limits.bundleBytes * 4 / 3) + limits.resources * RESOURCE_ENTRY_CHARACTERS + ENVELOPE_CHARACTERS;
|
|
583
|
+
if (text.length > maxLength) throw new EngineError(EngineErrorCode.PayloadTooLarge, `the file is ${text.length} characters, more than a bundle within ${limits.bundleBytes} bytes can be`, { details: {
|
|
584
|
+
limit: "bundleBytes",
|
|
585
|
+
max: limits.bundleBytes,
|
|
586
|
+
fileLength: text.length
|
|
587
|
+
} });
|
|
588
|
+
let file;
|
|
589
|
+
try {
|
|
590
|
+
file = JSON.parse(text);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
throw notABundle(error);
|
|
593
|
+
}
|
|
594
|
+
const encoded = typeof file === "object" && file !== null ? file.resources : null;
|
|
595
|
+
if (typeof encoded !== "object" || encoded === null || Array.isArray(encoded)) throw notABundle();
|
|
596
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const [id, value] of Object.entries(encoded)) {
|
|
598
|
+
const size = typeof value === "string" ? decodedLengthOf(value) : null;
|
|
599
|
+
if (size === null) throw new EngineError(EngineErrorCode.InvalidArg, `resource ${id} is not base64`);
|
|
600
|
+
sizes.set(id, size);
|
|
601
|
+
}
|
|
602
|
+
spec.assertManifest(file, sizes, limits);
|
|
603
|
+
const resources = {};
|
|
604
|
+
for (const [id, value] of Object.entries(encoded)) try {
|
|
605
|
+
resources[id] = fromBase64(value);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
throw new EngineError(EngineErrorCode.InvalidArg, `resource ${id} is not base64`, { cause: error });
|
|
608
|
+
}
|
|
609
|
+
await assertResourceIds(spec.kind, resources);
|
|
610
|
+
const bundle = {};
|
|
611
|
+
for (const key of spec.keys) bundle[key] = file[key];
|
|
612
|
+
bundle.resources = resources;
|
|
613
|
+
return bundle;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/** The manifest's keys, in the order a file writes them. */
|
|
618
|
+
const ANNOTATION_BUNDLE_KEYS = [
|
|
619
|
+
"format",
|
|
620
|
+
"version",
|
|
621
|
+
"pages",
|
|
622
|
+
"items"
|
|
623
|
+
];
|
|
624
|
+
const BUNDLE_FIELDS = [...ANNOTATION_BUNDLE_KEYS, "resources"];
|
|
625
|
+
const ITEM_FIELDS = ["data", "resources"];
|
|
626
|
+
/**
|
|
627
|
+
* Check a bundle before anything uses it, the checks cheapest first: its
|
|
628
|
+
* format, its counts and sizes against `limits` (`PayloadTooLarge`), its
|
|
629
|
+
* shape and references (`InvalidArg`), then every resource against its id.
|
|
630
|
+
*/
|
|
631
|
+
async function assertAnnotationBundle(bundle, limits = DEFAULT_BUNDLE_LIMITS) {
|
|
632
|
+
const resources = isRecord(bundle?.resources) ? bundle.resources : invalidBundle("annotation", "no resources");
|
|
633
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const [id, bytes] of Object.entries(resources)) {
|
|
635
|
+
if (!(bytes instanceof Uint8Array)) invalidBundle("annotation", `resource ${id} is not bytes`);
|
|
636
|
+
sizes.set(id, bytes.length);
|
|
637
|
+
}
|
|
638
|
+
assertAnnotationBundleManifest(bundle, sizes, limits);
|
|
639
|
+
await assertResourceIds("annotation", resources);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* The checks that need no resource bytes, only their sizes by id: format
|
|
643
|
+
* and version, counts, sizes, the shape of pages and items, and that items
|
|
644
|
+
* and resources name each other exactly.
|
|
645
|
+
*/
|
|
646
|
+
function assertAnnotationBundleManifest(manifest, resourceSizes, limits) {
|
|
647
|
+
if (!isRecord(manifest)) invalidBundle("annotation", "not an annotation bundle");
|
|
648
|
+
assertKnownFields("annotation", "the bundle", manifest, BUNDLE_FIELDS);
|
|
649
|
+
if (manifest.format !== "embedpdf/annotations") invalidBundle("annotation", `unknown format ${JSON.stringify(manifest.format)}`);
|
|
650
|
+
if (manifest.version !== 1) invalidBundle("annotation", `unknown version ${JSON.stringify(manifest.version)}`);
|
|
651
|
+
const { pages, items } = manifest;
|
|
652
|
+
if (!Array.isArray(pages)) invalidBundle("annotation", "pages is not a list");
|
|
653
|
+
if (!Array.isArray(items)) invalidBundle("annotation", "items is not a list");
|
|
654
|
+
assertWithinLimit("annotation", limits, "items", items.length);
|
|
655
|
+
assertWithinLimit("annotation", limits, "pages", pages.length);
|
|
656
|
+
assertWithinLimit("annotation", limits, "resources", resourceSizes.size);
|
|
657
|
+
let bundleBytes = manifestBytesOf({
|
|
658
|
+
pages,
|
|
659
|
+
items
|
|
660
|
+
});
|
|
661
|
+
assertWithinLimit("annotation", limits, "manifestBytes", bundleBytes);
|
|
662
|
+
for (const size of resourceSizes.values()) {
|
|
663
|
+
assertWithinLimit("annotation", limits, "resourceBytes", size);
|
|
664
|
+
bundleBytes += size;
|
|
665
|
+
}
|
|
666
|
+
assertWithinLimit("annotation", limits, "bundleBytes", bundleBytes);
|
|
667
|
+
const pageKeys = assertBundlePages("annotation", pages);
|
|
668
|
+
const named = /* @__PURE__ */ new Set();
|
|
669
|
+
items.forEach((item, index) => {
|
|
670
|
+
if (!isRecord(item) || !isRecord(item.data) || typeof item.data.subtype !== "string") invalidBundle("annotation", `item ${index} has no annotation data`);
|
|
671
|
+
assertKnownFields("annotation", `item ${index}`, item, ITEM_FIELDS);
|
|
672
|
+
const ref = item.data.ref;
|
|
673
|
+
const page = isRecord(ref) ? ref.page : void 0;
|
|
674
|
+
if (!isBundlePageRef(page) || !pageKeys.has(encodePageKey(page))) invalidBundle("annotation", `item ${index} is on a page the bundle doesn't list`);
|
|
675
|
+
assertRowResources("annotation", `item ${index}`, item.resources, ANNOTATION_RESOURCE_ROLE_NAMES, resourceSizes, named);
|
|
676
|
+
});
|
|
677
|
+
assertResourcesNamed("annotation", "item", resourceSizes, named);
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/transfer/AnnotationTransfer.ts
|
|
681
|
+
/**
|
|
682
|
+
* An annotation bundle as one JSON file, each resource in base64: a bundle
|
|
683
|
+
* whole, to store or download. HTTP carries bundles as multipart instead,
|
|
684
|
+
* without the third base64 adds.
|
|
685
|
+
*/
|
|
686
|
+
const AnnotationTransfer = bundleCodec({
|
|
687
|
+
kind: "annotation",
|
|
688
|
+
keys: ANNOTATION_BUNDLE_KEYS,
|
|
689
|
+
assertManifest: assertAnnotationBundleManifest
|
|
690
|
+
});
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/dto/Attachment.ts
|
|
693
|
+
/** The ref of an attachment whose name-tree key you already hold. */
|
|
694
|
+
function toAttachmentRef(key) {
|
|
695
|
+
return {
|
|
696
|
+
kind: "key",
|
|
697
|
+
key
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/annotation/appearanceTurn.ts
|
|
702
|
+
/** The kinds drawn about a box that can turn. */
|
|
703
|
+
const BOX_KINDS = /* @__PURE__ */ new Set([
|
|
704
|
+
"square",
|
|
705
|
+
"circle",
|
|
706
|
+
"free-text",
|
|
707
|
+
"stamp",
|
|
708
|
+
"caret"
|
|
709
|
+
]);
|
|
710
|
+
/**
|
|
711
|
+
* The turn an annotation's appearance raster is drawn without, degrees
|
|
712
|
+
* clockwise, or `null` when the raster is drawn as the page shows it. A box
|
|
713
|
+
* kind drawn turned whose drawing stays inside the turned box (its `rect` is
|
|
714
|
+
* the upright box around it) renders upright over its `box`, for the
|
|
715
|
+
* consumer to turn about the middle of `box`: a new turn needs no new
|
|
716
|
+
* raster. A callout (its line isn't turned) and a drawing that reaches past
|
|
717
|
+
* its box (a cloudy border's bumps) render as the page shows them, placed by
|
|
718
|
+
* `rect`.
|
|
719
|
+
*/
|
|
720
|
+
function pdfAppearanceTurnOf(annotation) {
|
|
721
|
+
const { subtype, rect, box, rotation, intent } = annotation;
|
|
722
|
+
if (!BOX_KINDS.has(subtype) || !box || !rotation) return null;
|
|
723
|
+
if (subtype === "free-text" && intent === "free-text-callout") return null;
|
|
724
|
+
return isSamePdfRect(pdfRectTurnedBounds(box, rotation), rect) ? rotation : null;
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
727
|
+
//#region src/annotation/resolve/applyAnnotationPatch.ts
|
|
728
|
+
/**
|
|
729
|
+
* A resolved patch's fields as a read spells them. Most fields read as they
|
|
730
|
+
* are written; these take a different shape on the way in.
|
|
731
|
+
*/
|
|
732
|
+
function readValueOf(name, value, current) {
|
|
733
|
+
if (value === null) return null;
|
|
734
|
+
switch (name) {
|
|
735
|
+
case "reply": {
|
|
736
|
+
const reply = value;
|
|
737
|
+
return {
|
|
738
|
+
to: reply.to,
|
|
739
|
+
type: reply.type ?? "reply"
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
case "richText": {
|
|
743
|
+
const input = value;
|
|
744
|
+
const read = current;
|
|
745
|
+
const body = input.body ? {
|
|
746
|
+
...DEFAULT_RICH_TEXT_BODY,
|
|
747
|
+
...input.body
|
|
748
|
+
} : read?.body ?? DEFAULT_RICH_TEXT_BODY;
|
|
749
|
+
return {
|
|
750
|
+
body: {
|
|
751
|
+
...body,
|
|
752
|
+
color: body.color.toLowerCase()
|
|
753
|
+
},
|
|
754
|
+
paragraphs: input.paragraphs.map((paragraph) => ({
|
|
755
|
+
...paragraph,
|
|
756
|
+
runs: paragraph.runs.filter((run) => run.text !== "").map((run) => run.style?.color ? {
|
|
757
|
+
...run,
|
|
758
|
+
style: {
|
|
759
|
+
...run.style,
|
|
760
|
+
color: run.style.color.toLowerCase()
|
|
761
|
+
}
|
|
762
|
+
} : run)
|
|
763
|
+
}))
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
case "file": {
|
|
767
|
+
const written = value;
|
|
768
|
+
return {
|
|
769
|
+
...current ?? {
|
|
770
|
+
size: null,
|
|
771
|
+
checksum: null,
|
|
772
|
+
createdAt: null,
|
|
773
|
+
modifiedAt: null
|
|
774
|
+
},
|
|
775
|
+
name: written.name,
|
|
776
|
+
mimeType: written.mimeType ?? null,
|
|
777
|
+
description: written.description ?? null
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
default: return value;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* `current` with a resolved patch's data fields in place, each as a read
|
|
785
|
+
* spells it. Fields the engine works out or stamps itself (a drawn kind's
|
|
786
|
+
* `rect`, the modified date and author) keep
|
|
787
|
+
* the value `current` has: the engine's answer brings the new one. Works in
|
|
788
|
+
* either space: it only moves values by field name.
|
|
789
|
+
*/
|
|
790
|
+
function applyResolvedPatch(current, resolved) {
|
|
791
|
+
const owners = kindFactsOf(current.subtype)?.owners ?? {};
|
|
792
|
+
const read = current;
|
|
793
|
+
const next = { ...read };
|
|
794
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
795
|
+
if (name === "subtype" || value === void 0) continue;
|
|
796
|
+
if (owners[name] !== "data") continue;
|
|
797
|
+
next[name] = readValueOf(name, value, read[name]);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
...next,
|
|
801
|
+
...followingReads(current, resolved)
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Fields the engine keeps as found, but rewrites when a data field they
|
|
806
|
+
* mirror changes: a link's `target` is the root of its `/A`, so a new target
|
|
807
|
+
* is a new `actions.activate` (and no target, none).
|
|
808
|
+
*/
|
|
809
|
+
function followingReads(current, resolved) {
|
|
810
|
+
if (current.subtype !== "link" || resolved.subtype !== "link") return {};
|
|
811
|
+
const target = resolved.target;
|
|
812
|
+
if (target === void 0 || target !== null && target.kind !== "uri" && target.kind !== "goto") return {};
|
|
813
|
+
const { activate: _replaced, ...others } = current.actions ?? {};
|
|
814
|
+
const actions = target === null ? others : {
|
|
815
|
+
...others,
|
|
816
|
+
activate: actionTreeOf(target)
|
|
817
|
+
};
|
|
818
|
+
return { actions: Object.keys(actions).length > 0 ? actions : null };
|
|
819
|
+
}
|
|
820
|
+
/** The action tree the engine writes for a link target: one URI or GoTo action. */
|
|
821
|
+
function actionTreeOf(target) {
|
|
822
|
+
return {
|
|
823
|
+
root: target.kind === "uri" ? {
|
|
824
|
+
type: "uri",
|
|
825
|
+
subtype: "URI",
|
|
826
|
+
uri: target.uri,
|
|
827
|
+
isMap: false,
|
|
828
|
+
next: []
|
|
829
|
+
} : {
|
|
830
|
+
type: "goto",
|
|
831
|
+
subtype: "GoTo",
|
|
832
|
+
destination: target.destination,
|
|
833
|
+
next: []
|
|
834
|
+
},
|
|
835
|
+
incomplete: false,
|
|
836
|
+
warningFlags: 0,
|
|
837
|
+
warnings: []
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region src/annotation/defaults.ts
|
|
842
|
+
const RED = "#ff0000";
|
|
843
|
+
const BLACK = "#000000";
|
|
844
|
+
const YELLOW = "#ffff00";
|
|
845
|
+
/** Every kind: printed (as Acrobat creates them), no other flag set, normal blending. A popup doesn't print. */
|
|
846
|
+
const BASE = {
|
|
847
|
+
invisible: false,
|
|
848
|
+
hidden: false,
|
|
849
|
+
print: true,
|
|
850
|
+
noZoom: false,
|
|
851
|
+
noRotate: false,
|
|
852
|
+
noView: false,
|
|
853
|
+
readOnly: false,
|
|
854
|
+
locked: false,
|
|
855
|
+
toggleNoView: false,
|
|
856
|
+
lockedContents: false,
|
|
857
|
+
blendMode: "normal"
|
|
858
|
+
};
|
|
859
|
+
/** Shapes and lines: a solid red line, one point wide. */
|
|
860
|
+
const STROKE = {
|
|
861
|
+
...BASE,
|
|
862
|
+
color: RED,
|
|
863
|
+
opacity: 1,
|
|
864
|
+
strokeWidth: 1,
|
|
865
|
+
borderStyle: "solid"
|
|
866
|
+
};
|
|
867
|
+
const NO_ENDINGS = {
|
|
868
|
+
start: "none",
|
|
869
|
+
end: "none"
|
|
870
|
+
};
|
|
871
|
+
/** Text markup other than a highlight: black. */
|
|
872
|
+
const MARKUP = {
|
|
873
|
+
...BASE,
|
|
874
|
+
color: BLACK,
|
|
875
|
+
opacity: 1
|
|
876
|
+
};
|
|
877
|
+
const ANNOTATION_DEFAULTS = {
|
|
878
|
+
square: STROKE,
|
|
879
|
+
circle: STROKE,
|
|
880
|
+
line: {
|
|
881
|
+
...STROKE,
|
|
882
|
+
lineEndings: NO_ENDINGS,
|
|
883
|
+
captionPosition: "inline"
|
|
884
|
+
},
|
|
885
|
+
polygon: STROKE,
|
|
886
|
+
polyline: {
|
|
887
|
+
...STROKE,
|
|
888
|
+
lineEndings: NO_ENDINGS
|
|
889
|
+
},
|
|
890
|
+
ink: STROKE,
|
|
891
|
+
highlight: {
|
|
892
|
+
...BASE,
|
|
893
|
+
blendMode: "multiply",
|
|
894
|
+
color: YELLOW,
|
|
895
|
+
opacity: 1
|
|
896
|
+
},
|
|
897
|
+
underline: MARKUP,
|
|
898
|
+
squiggly: MARKUP,
|
|
899
|
+
strikeout: MARKUP,
|
|
900
|
+
caret: {
|
|
901
|
+
...BASE,
|
|
902
|
+
color: RED,
|
|
903
|
+
opacity: 1
|
|
904
|
+
},
|
|
905
|
+
"free-text": {
|
|
906
|
+
...BASE,
|
|
907
|
+
contents: "",
|
|
908
|
+
intent: "free-text",
|
|
909
|
+
fontFamily: "helvetica",
|
|
910
|
+
fontSize: 12,
|
|
911
|
+
textAlign: "left",
|
|
912
|
+
verticalAlign: "top",
|
|
913
|
+
color: BLACK,
|
|
914
|
+
fontColor: BLACK,
|
|
915
|
+
opacity: 1,
|
|
916
|
+
strokeWidth: 1,
|
|
917
|
+
borderStyle: "solid"
|
|
918
|
+
},
|
|
919
|
+
text: {
|
|
920
|
+
...BASE,
|
|
921
|
+
icon: "note",
|
|
922
|
+
open: false,
|
|
923
|
+
color: YELLOW,
|
|
924
|
+
opacity: 1
|
|
925
|
+
},
|
|
926
|
+
"file-attachment": {
|
|
927
|
+
...BASE,
|
|
928
|
+
icon: "paperclip",
|
|
929
|
+
color: YELLOW,
|
|
930
|
+
opacity: 1
|
|
931
|
+
},
|
|
932
|
+
link: BASE,
|
|
933
|
+
redact: {
|
|
934
|
+
...BASE,
|
|
935
|
+
quadPoints: [],
|
|
936
|
+
color: RED,
|
|
937
|
+
opacity: 1,
|
|
938
|
+
repeat: false,
|
|
939
|
+
fontFamily: "helvetica",
|
|
940
|
+
fontSize: 12,
|
|
941
|
+
fontColor: BLACK,
|
|
942
|
+
textAlign: "left"
|
|
943
|
+
},
|
|
944
|
+
stamp: {
|
|
945
|
+
...BASE,
|
|
946
|
+
fit: "contain",
|
|
947
|
+
opacity: 1
|
|
948
|
+
},
|
|
949
|
+
widget: {
|
|
950
|
+
...BASE,
|
|
951
|
+
strokeWidth: 1,
|
|
952
|
+
borderStyle: "solid",
|
|
953
|
+
textAlign: "left"
|
|
954
|
+
},
|
|
955
|
+
popup: {
|
|
956
|
+
...BASE,
|
|
957
|
+
print: false,
|
|
958
|
+
open: false
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
/** What a create of `subtype` reads back for each data field it leaves out. */
|
|
962
|
+
function annotationDefaultsOf(subtype) {
|
|
963
|
+
return subtype === "unsupported" ? {} : ANNOTATION_DEFAULTS[subtype];
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/pageSpace/destinations.ts
|
|
967
|
+
const boxFor = (page, box) => typeof box === "function" ? box(page) : box;
|
|
968
|
+
/** An axis value that may keep the current one (`null`) or be left out. */
|
|
969
|
+
const across = (value, convert) => value == null ? value : convert(value);
|
|
970
|
+
/**
|
|
971
|
+
* A destination in the file's coordinates, in page space: for one read by
|
|
972
|
+
* another PDF tool, before handing it to the viewer. `box` is the `pdfCropBox`
|
|
973
|
+
* of the page it goes to.
|
|
974
|
+
*/
|
|
975
|
+
function pageDestinationOf(destination, box) {
|
|
976
|
+
const { left: boxLeft, top: boxTop } = boxFor(destination.page, box);
|
|
977
|
+
const x = (left) => across(left, (value) => value - boxLeft);
|
|
978
|
+
const y = (top) => across(top, (value) => boxTop - value);
|
|
979
|
+
switch (destination.kind) {
|
|
980
|
+
case "xyz": {
|
|
981
|
+
const { left, top, ...rest } = destination;
|
|
982
|
+
return {
|
|
983
|
+
...rest,
|
|
984
|
+
..."left" in destination ? { x: x(left) } : {},
|
|
985
|
+
..."top" in destination ? { y: y(top) } : {}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
case "fitH":
|
|
989
|
+
case "fitBH": {
|
|
990
|
+
const { top, ...rest } = destination;
|
|
991
|
+
return {
|
|
992
|
+
...rest,
|
|
993
|
+
..."top" in destination ? { y: y(top) } : {}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
case "fitV":
|
|
997
|
+
case "fitBV": {
|
|
998
|
+
const { left, ...rest } = destination;
|
|
999
|
+
return {
|
|
1000
|
+
...rest,
|
|
1001
|
+
..."left" in destination ? { x: x(left) } : {}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
case "fitR": {
|
|
1005
|
+
const { left, bottom, right, top, ...rest } = destination;
|
|
1006
|
+
const lower = Math.min(bottom, top);
|
|
1007
|
+
const upper = Math.max(bottom, top);
|
|
1008
|
+
return {
|
|
1009
|
+
...rest,
|
|
1010
|
+
x: Math.min(left, right) - boxLeft,
|
|
1011
|
+
y: boxTop - upper,
|
|
1012
|
+
width: Math.abs(right - left),
|
|
1013
|
+
height: upper - lower
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
case "fit":
|
|
1017
|
+
case "fitB": return destination;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* A page-space destination in the file's coordinates: for handing one to
|
|
1022
|
+
* another PDF tool. `box` is the `pdfCropBox` of the page it goes to.
|
|
1023
|
+
*/
|
|
1024
|
+
function pdfDestinationOf(destination, box) {
|
|
1025
|
+
const { left: boxLeft, top: boxTop } = boxFor(destination.page, box);
|
|
1026
|
+
const left = (x) => across(x, (value) => value + boxLeft);
|
|
1027
|
+
const top = (y) => across(y, (value) => boxTop - value);
|
|
1028
|
+
switch (destination.kind) {
|
|
1029
|
+
case "xyz": {
|
|
1030
|
+
const { x, y, ...rest } = destination;
|
|
1031
|
+
return {
|
|
1032
|
+
...rest,
|
|
1033
|
+
..."x" in destination ? { left: left(x) } : {},
|
|
1034
|
+
..."y" in destination ? { top: top(y) } : {}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
case "fitH":
|
|
1038
|
+
case "fitBH": {
|
|
1039
|
+
const { y, ...rest } = destination;
|
|
1040
|
+
return {
|
|
1041
|
+
...rest,
|
|
1042
|
+
..."y" in destination ? { top: top(y) } : {}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
case "fitV":
|
|
1046
|
+
case "fitBV": {
|
|
1047
|
+
const { x, ...rest } = destination;
|
|
1048
|
+
return {
|
|
1049
|
+
...rest,
|
|
1050
|
+
..."x" in destination ? { left: left(x) } : {}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
case "fitR": {
|
|
1054
|
+
const { x, y, width, height, ...rest } = destination;
|
|
1055
|
+
const upper = boxTop - y;
|
|
1056
|
+
return {
|
|
1057
|
+
...rest,
|
|
1058
|
+
left: x + boxLeft,
|
|
1059
|
+
bottom: upper - height,
|
|
1060
|
+
right: x + boxLeft + width,
|
|
1061
|
+
top: upper
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
case "fit":
|
|
1065
|
+
case "fitB": return destination;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function mapLinkTarget(target, convert) {
|
|
1069
|
+
return target.kind === "goto" ? {
|
|
1070
|
+
...target,
|
|
1071
|
+
destination: convert(target.destination)
|
|
1072
|
+
} : target;
|
|
1073
|
+
}
|
|
1074
|
+
function mapActionNode(node, convert) {
|
|
1075
|
+
const next = node.next.map((child) => mapActionNode(child, convert));
|
|
1076
|
+
return node.type === "goto" ? {
|
|
1077
|
+
...node,
|
|
1078
|
+
next,
|
|
1079
|
+
destination: convert(node.destination)
|
|
1080
|
+
} : {
|
|
1081
|
+
...node,
|
|
1082
|
+
next
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function mapActionTree(tree, convert) {
|
|
1086
|
+
return {
|
|
1087
|
+
...tree,
|
|
1088
|
+
root: tree.root ? mapActionNode(tree.root, convert) : null
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
/** An action to write, its destinations converted. */
|
|
1092
|
+
function mapActionWrite(action, convert) {
|
|
1093
|
+
const next = action.next?.map((child) => mapActionWrite(child, convert));
|
|
1094
|
+
const converted = action.type === "goto" ? {
|
|
1095
|
+
...action,
|
|
1096
|
+
destination: convert(action.destination)
|
|
1097
|
+
} : action;
|
|
1098
|
+
return next ? {
|
|
1099
|
+
...converted,
|
|
1100
|
+
next
|
|
1101
|
+
} : converted;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* A record of triggers as a read has them (trees) or a write takes them
|
|
1105
|
+
* (actions to write, `null` to remove one), its destinations converted.
|
|
1106
|
+
*/
|
|
1107
|
+
function mapTriggerActions(triggers, convert) {
|
|
1108
|
+
return Object.fromEntries(Object.entries(triggers).map(([trigger, value]) => {
|
|
1109
|
+
if (value == null) return [trigger, value];
|
|
1110
|
+
return [trigger, "root" in value ? mapActionTree(value, convert) : mapActionWrite(value, convert)];
|
|
1111
|
+
}));
|
|
1112
|
+
}
|
|
1113
|
+
/** Every action tree in a record of triggers (an annotation's, a page's or a field's). */
|
|
1114
|
+
function mapTriggers(triggers, convert) {
|
|
1115
|
+
return Object.fromEntries(Object.entries(triggers).map(([trigger, tree]) => [trigger, tree ? mapActionTree(tree, convert) : tree]));
|
|
1116
|
+
}
|
|
1117
|
+
function mapAnnotationActions(actions, convert) {
|
|
1118
|
+
return mapTriggers(actions, convert);
|
|
1119
|
+
}
|
|
1120
|
+
function mapPageActions(actions, convert) {
|
|
1121
|
+
return mapTriggers(actions, convert);
|
|
1122
|
+
}
|
|
1123
|
+
function mapFieldActions(actions, convert) {
|
|
1124
|
+
return mapTriggers(actions, convert);
|
|
1125
|
+
}
|
|
1126
|
+
function mapDocumentActions(snapshot, convert) {
|
|
1127
|
+
const { nameTreeScripts, openAction, openDestination, ...triggers } = snapshot;
|
|
1128
|
+
return {
|
|
1129
|
+
...mapTriggers(triggers, convert),
|
|
1130
|
+
nameTreeScripts: nameTreeScripts.map((script) => ({
|
|
1131
|
+
...script,
|
|
1132
|
+
action: mapActionTree(script.action, convert)
|
|
1133
|
+
})),
|
|
1134
|
+
openAction: openAction ? mapActionTree(openAction, convert) : null,
|
|
1135
|
+
..."openDestination" in snapshot ? { openDestination: openDestination ? convert(openDestination) : openDestination } : {}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/pageSpace/measure.ts
|
|
1140
|
+
const withOrigin = (measure, convert) => measure.subtype === "rectilinear" && measure.origin ? {
|
|
1141
|
+
...measure,
|
|
1142
|
+
origin: convert(measure.origin)
|
|
1143
|
+
} : measure;
|
|
1144
|
+
/** A scale in page space: its origin (`/O`) is a place on the page. */
|
|
1145
|
+
function pageMeasureOf(measure, visible) {
|
|
1146
|
+
return withOrigin(measure, (origin) => pagePointOf(origin, visible));
|
|
1147
|
+
}
|
|
1148
|
+
/** A page-space scale in the file's coordinates. */
|
|
1149
|
+
function pdfMeasureOf(measure, visible) {
|
|
1150
|
+
return withOrigin(measure, (origin) => pdfPointOf(origin, visible));
|
|
1151
|
+
}
|
|
1152
|
+
/** A page's measurement viewports in page space. */
|
|
1153
|
+
function pageViewportsOf(viewports, visible) {
|
|
1154
|
+
return viewports.map((viewport) => ({
|
|
1155
|
+
...viewport,
|
|
1156
|
+
bbox: pageBoxOf(viewport.bbox, visible),
|
|
1157
|
+
measure: viewport.measure ? pageMeasureOf(viewport.measure, visible) : null
|
|
1158
|
+
}));
|
|
1159
|
+
}
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/pageSpace/annotations.ts
|
|
1162
|
+
const TO_PAGE = {
|
|
1163
|
+
length: (value) => value,
|
|
1164
|
+
point: (point, { visible }) => pagePointOf(point, visible),
|
|
1165
|
+
points: (points, { visible }) => points.map((point) => pagePointOf(point, visible)),
|
|
1166
|
+
strokes: (strokes, { visible }) => strokes.map((stroke) => stroke.map((point) => pagePointOf(point, visible))),
|
|
1167
|
+
box: (rect, { visible }) => pageBoxOf(rect, visible),
|
|
1168
|
+
quads: (quads, { visible }) => quads.map((quad) => pageQuadOf(quad, visible)),
|
|
1169
|
+
linePoints: (line, { visible }) => ({
|
|
1170
|
+
start: pagePointOf(line.start, visible),
|
|
1171
|
+
end: pagePointOf(line.end, visible)
|
|
1172
|
+
}),
|
|
1173
|
+
calloutLine: (points, { visible }) => points.map((point) => pagePointOf(point, visible)),
|
|
1174
|
+
measure: (measure, { visible }) => pageMeasureOf(measure, visible),
|
|
1175
|
+
linkTarget: (target, { boxOf }) => mapLinkTarget(target, (destination) => pageDestinationOf(destination, boxOf)),
|
|
1176
|
+
actions: (actions, { boxOf }) => mapTriggerActions(actions, (destination) => pageDestinationOf(destination, boxOf))
|
|
1177
|
+
};
|
|
1178
|
+
const TO_PDF = {
|
|
1179
|
+
length: (value) => value,
|
|
1180
|
+
point: (point, { visible }) => pdfPointOf(point, visible),
|
|
1181
|
+
points: (points, { visible }) => points.map((point) => pdfPointOf(point, visible)),
|
|
1182
|
+
strokes: (strokes, { visible }) => strokes.map((stroke) => stroke.map((point) => pdfPointOf(point, visible))),
|
|
1183
|
+
box: (box, { visible }) => pdfRectOf(box, visible),
|
|
1184
|
+
quads: (quads, { visible }) => quads.map((quad) => pdfQuadOf(quad, visible)),
|
|
1185
|
+
linePoints: (line, { visible }) => ({
|
|
1186
|
+
start: pdfPointOf(line.start, visible),
|
|
1187
|
+
end: pdfPointOf(line.end, visible)
|
|
1188
|
+
}),
|
|
1189
|
+
calloutLine: (points, { visible }) => points.map((point) => pdfPointOf(point, visible)),
|
|
1190
|
+
measure: (measure, { visible }) => pdfMeasureOf(measure, visible),
|
|
1191
|
+
linkTarget: (target, { boxOf }) => mapLinkTarget(target, (destination) => pdfDestinationOf(destination, boxOf)),
|
|
1192
|
+
actions: (actions, { boxOf }) => mapTriggerActions(actions, (destination) => pdfDestinationOf(destination, boxOf))
|
|
1193
|
+
};
|
|
1194
|
+
/**
|
|
1195
|
+
* The measured fields of a kind; a kind the engine doesn't know has the base
|
|
1196
|
+
* fields. An update may leave out its kind: a field's name says what it
|
|
1197
|
+
* holds, the same in every kind.
|
|
1198
|
+
*/
|
|
1199
|
+
const spacesOf = (subtype) => subtype === void 0 ? ANNOTATION_FIELD_SPACES_BY_NAME : ANNOTATION_FIELD_SPACES[subtype] ?? ANNOTATION_FIELD_SPACES.unsupported;
|
|
1200
|
+
function convertFields(subtype, value, frame, converters) {
|
|
1201
|
+
const spaces = spacesOf(subtype);
|
|
1202
|
+
return Object.fromEntries(Object.entries(value).map(([name, field]) => {
|
|
1203
|
+
const space = spaces[name];
|
|
1204
|
+
if (!space || field == null) return [name, field];
|
|
1205
|
+
const converted = converters[space](field, frame);
|
|
1206
|
+
if (converters === TO_PDF && hasNaN(converted)) throw notInPageSpace(name, space);
|
|
1207
|
+
return [name, converted];
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
/** Whether a converted value has a number that isn't one: a place not given in page space. */
|
|
1211
|
+
function hasNaN(value) {
|
|
1212
|
+
if (typeof value === "number") return Number.isNaN(value);
|
|
1213
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1214
|
+
return Object.values(value).some(hasNaN);
|
|
1215
|
+
}
|
|
1216
|
+
function notInPageSpace(name, space) {
|
|
1217
|
+
const shape = space === "box" ? "{ x, y, width, height }" : "points as { x, y }";
|
|
1218
|
+
return new EngineError(EngineErrorCode.InvalidArg, `field '${name}': expected page space, ${shape} from the page's top-left`, { details: { field: name } });
|
|
1219
|
+
}
|
|
1220
|
+
/** A read in page space. `visible` is its page's visible box, in the file's coordinates. */
|
|
1221
|
+
function pageAnnotationOf(annotation, visible, boxOf) {
|
|
1222
|
+
return convertFields(annotation.subtype, annotation, {
|
|
1223
|
+
visible,
|
|
1224
|
+
boxOf
|
|
1225
|
+
}, TO_PAGE);
|
|
1226
|
+
}
|
|
1227
|
+
/** A page-space read in the file's coordinates. */
|
|
1228
|
+
function pdfAnnotationOf(annotation, visible, boxOf) {
|
|
1229
|
+
return convertFields(annotation.subtype, annotation, {
|
|
1230
|
+
visible,
|
|
1231
|
+
boxOf
|
|
1232
|
+
}, TO_PDF);
|
|
1233
|
+
}
|
|
1234
|
+
/** A page-space create in the file's coordinates. */
|
|
1235
|
+
function pdfAnnotationDraftOf(draft, visible, boxOf) {
|
|
1236
|
+
return convertFields(draft.subtype, draft, {
|
|
1237
|
+
visible,
|
|
1238
|
+
boxOf
|
|
1239
|
+
}, TO_PDF);
|
|
1240
|
+
}
|
|
1241
|
+
/** A create in the file's coordinates, in page space. */
|
|
1242
|
+
function pageAnnotationDraftOf(draft, visible, boxOf) {
|
|
1243
|
+
return convertFields(draft.subtype, draft, {
|
|
1244
|
+
visible,
|
|
1245
|
+
boxOf
|
|
1246
|
+
}, TO_PAGE);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* A page-space update in the file's coordinates. An update may leave out its
|
|
1250
|
+
* `subtype`; its fields' names say what they hold.
|
|
1251
|
+
*/
|
|
1252
|
+
function pdfAnnotationPatchOf(patch, visible, boxOf) {
|
|
1253
|
+
return convertFields(patch.subtype, patch, {
|
|
1254
|
+
visible,
|
|
1255
|
+
boxOf
|
|
1256
|
+
}, TO_PDF);
|
|
1257
|
+
}
|
|
1258
|
+
/** An update in the file's coordinates, in page space. */
|
|
1259
|
+
function pageAnnotationPatchOf(patch, visible, boxOf) {
|
|
1260
|
+
return convertFields(patch.subtype, patch, {
|
|
1261
|
+
visible,
|
|
1262
|
+
boxOf
|
|
1263
|
+
}, TO_PAGE);
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/text/layout.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* The canonical text-interaction layout — the one place glyph geometry
|
|
1269
|
+
* becomes hit targets, word/line ranges, and visual-line segments.
|
|
1270
|
+
*
|
|
1271
|
+
* Selection owns gestures and state; search owns matching and cursors;
|
|
1272
|
+
* neither owns segmentation. Both consume this module, so a text range has
|
|
1273
|
+
* exactly one canonical segmentation regardless of how it was produced.
|
|
1274
|
+
*
|
|
1275
|
+
* Everything here works in the file's coordinates (y up), as the engine reads
|
|
1276
|
+
* them. The public layout (`createTextLayout`, `pageSpace/text.ts`) runs this
|
|
1277
|
+
* one on page-space geometry flipped top to bottom and flips its answers
|
|
1278
|
+
* back. The line-merge is adapted from Chromium's
|
|
1279
|
+
* pdf/pdfium/pdfium_range.cc `MergeAdjacentRects` (BSD-licensed, Copyright
|
|
1280
|
+
* 2010 The Chromium Authors); hit-testing mirrors PDFium `GetIndexAtPos`
|
|
1281
|
+
* (exact tight box first, then a tolerance pass).
|
|
1282
|
+
*
|
|
1283
|
+
* Orientation model — frames. Every run belongs to a frame: frame 0 is page
|
|
1284
|
+
* space itself (all upright runs — the dominant case takes a byte-identical
|
|
1285
|
+
* fast path), and each rotated orientation cluster gets an orthonormal frame
|
|
1286
|
+
* in which its text reads upright (baseline +x, ascent +y). Frames are
|
|
1287
|
+
* derived from the semantic edges of the first classifiable glyph's quad —
|
|
1288
|
+
* never from advisory wire fields — and keyed by (baseline direction,
|
|
1289
|
+
* ascent handedness):
|
|
1290
|
+
*
|
|
1291
|
+
* - Rotated and mirrored text become upright inside their frame.
|
|
1292
|
+
* - Shear (fake italic) is deliberately not part of the frame key or the
|
|
1293
|
+
* basis: a sheared run shares the frame of its unsheared neighbours, so
|
|
1294
|
+
* a mixed roman/italic line merges into one segment (Acrobat parity),
|
|
1295
|
+
* and the shear is absorbed as a small in-frame AABB residue. This is
|
|
1296
|
+
* the deterministic, order-independent contract — an affine
|
|
1297
|
+
* (shear-preserving) basis would either fragment mixed lines or make
|
|
1298
|
+
* the roman body's geometry depend on whether an italic run came first.
|
|
1299
|
+
* - Runs of different frames never merge into one line.
|
|
1300
|
+
*
|
|
1301
|
+
* All algorithms run on frame-local boxes; every participating run of a
|
|
1302
|
+
* cluster is transformed through the same frame, so merged coordinates are
|
|
1303
|
+
* always commensurable. Only the output maps back to page space, as
|
|
1304
|
+
* oriented quads.
|
|
1305
|
+
*/
|
|
1306
|
+
/** A space or a character with no box ends a word. */
|
|
1307
|
+
const isBoundary = (g) => !!(g.space || g.empty);
|
|
1308
|
+
const isEmpty = (g) => !!g.empty;
|
|
1309
|
+
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
1310
|
+
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
1311
|
+
const VERTICAL_OVERLAP_THRESHOLD = .8;
|
|
1312
|
+
const LINE_OVERLAP_THRESHOLD = .5;
|
|
1313
|
+
/** Baseline directions within ~0.5° share a frame. */
|
|
1314
|
+
const FRAME_DOT_TOLERANCE = Math.cos(.0087);
|
|
1315
|
+
const EDGE_EPSILON = 1e-6;
|
|
1316
|
+
/**
|
|
1317
|
+
* The layout of a page's geometry in the file's coordinates, for the engine's
|
|
1318
|
+
* own readers. Everything else uses `createTextLayout`, in page space.
|
|
1319
|
+
*/
|
|
1320
|
+
function createPdfTextLayout(snapshot) {
|
|
1321
|
+
const layout = buildPageTextLayout(snapshot);
|
|
1322
|
+
const charCount = layout.glyphs.length;
|
|
1323
|
+
const indexOf = (at) => {
|
|
1324
|
+
if (typeof at !== "number") return textGlyphAt(layout, at);
|
|
1325
|
+
return Number.isInteger(at) && at >= 0 && at < charCount ? at : null;
|
|
1326
|
+
};
|
|
1327
|
+
const rangeOf = ([from, to]) => ({
|
|
1328
|
+
start: from,
|
|
1329
|
+
count: to - from + 1
|
|
1330
|
+
});
|
|
1331
|
+
return {
|
|
1332
|
+
charCount,
|
|
1333
|
+
runs: snapshot.runs,
|
|
1334
|
+
charAt: (point) => textGlyphAt(layout, point),
|
|
1335
|
+
wordAt: (at) => {
|
|
1336
|
+
const index = indexOf(at);
|
|
1337
|
+
return index === null ? null : rangeOf(expandTextRangeToWord(layout, index));
|
|
1338
|
+
},
|
|
1339
|
+
lineAt: (at) => {
|
|
1340
|
+
const index = indexOf(at);
|
|
1341
|
+
return index === null ? null : rangeOf(expandTextRangeToLine(layout, index));
|
|
1342
|
+
},
|
|
1343
|
+
segments: (range) => textSegmentsForRange(layout, range.start, range.count),
|
|
1344
|
+
charQuad: (index) => textGlyphQuad(layout, index)
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const IDENTITY_FRAME = {
|
|
1348
|
+
baseline: {
|
|
1349
|
+
x: 1,
|
|
1350
|
+
y: 0
|
|
1351
|
+
},
|
|
1352
|
+
ascent: {
|
|
1353
|
+
x: 0,
|
|
1354
|
+
y: 1
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const toFrame = (f, p) => ({
|
|
1358
|
+
x: p.x * f.baseline.x + p.y * f.baseline.y,
|
|
1359
|
+
y: p.x * f.ascent.x + p.y * f.ascent.y
|
|
1360
|
+
});
|
|
1361
|
+
const fromFrame = (f, p) => ({
|
|
1362
|
+
x: f.baseline.x * p.x + f.ascent.x * p.y,
|
|
1363
|
+
y: f.baseline.y * p.x + f.ascent.y * p.y
|
|
1364
|
+
});
|
|
1365
|
+
const ZERO_RECT = {
|
|
1366
|
+
left: 0,
|
|
1367
|
+
bottom: 0,
|
|
1368
|
+
right: 0,
|
|
1369
|
+
top: 0
|
|
1370
|
+
};
|
|
1371
|
+
/** Frame-local AABB of a page-space cell (exact under the frame's rotation;
|
|
1372
|
+
* shear residue is the documented in-frame envelope). */
|
|
1373
|
+
function frameBoxOfQuad(f, q) {
|
|
1374
|
+
const pts = [
|
|
1375
|
+
q.upperLeft,
|
|
1376
|
+
q.upperRight,
|
|
1377
|
+
q.lowerLeft,
|
|
1378
|
+
q.lowerRight
|
|
1379
|
+
].map((p) => toFrame(f, p));
|
|
1380
|
+
const xs = pts.map((p) => p.x);
|
|
1381
|
+
const ys = pts.map((p) => p.y);
|
|
1382
|
+
return {
|
|
1383
|
+
left: Math.min(...xs),
|
|
1384
|
+
bottom: Math.min(...ys),
|
|
1385
|
+
right: Math.max(...xs),
|
|
1386
|
+
top: Math.max(...ys)
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/** Find-or-create the frame for a rotated run, keyed by the baseline
|
|
1390
|
+
* direction + ascent handedness of its first classifiable glyph's edges. */
|
|
1391
|
+
function frameForRun(frames, run) {
|
|
1392
|
+
const seed = run.glyphs.find((g) => !isEmpty(g));
|
|
1393
|
+
if (!seed) return 0;
|
|
1394
|
+
const q = seed.loose;
|
|
1395
|
+
const bx = q.upperRight.x - q.upperLeft.x;
|
|
1396
|
+
const by = q.upperRight.y - q.upperLeft.y;
|
|
1397
|
+
const len = Math.hypot(bx, by);
|
|
1398
|
+
if (len <= EDGE_EPSILON) return 0;
|
|
1399
|
+
const baseline = {
|
|
1400
|
+
x: bx / len,
|
|
1401
|
+
y: by / len
|
|
1402
|
+
};
|
|
1403
|
+
const ax = q.upperLeft.x - q.lowerLeft.x;
|
|
1404
|
+
const ay = q.upperLeft.y - q.lowerLeft.y;
|
|
1405
|
+
const sign = baseline.x * ay - baseline.y * ax >= 0 ? 1 : -1;
|
|
1406
|
+
const ascent = {
|
|
1407
|
+
x: -baseline.y * sign,
|
|
1408
|
+
y: baseline.x * sign
|
|
1409
|
+
};
|
|
1410
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1411
|
+
const f = frames[i];
|
|
1412
|
+
const dot = f.baseline.x * baseline.x + f.baseline.y * baseline.y;
|
|
1413
|
+
const sameHandedness = f.ascent.x * ascent.x + f.ascent.y * ascent.y > 0;
|
|
1414
|
+
if (dot >= FRAME_DOT_TOLERANCE && sameHandedness) return i;
|
|
1415
|
+
}
|
|
1416
|
+
frames.push({
|
|
1417
|
+
baseline,
|
|
1418
|
+
ascent
|
|
1419
|
+
});
|
|
1420
|
+
return frames.length - 1;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Flatten a page's geometry snapshot into the canonical layout. Upright runs
|
|
1424
|
+
* copy their wire boxes verbatim (frame 0, wire run rects included); rotated
|
|
1425
|
+
* runs project their glyph cells into their cluster's frame.
|
|
1426
|
+
*/
|
|
1427
|
+
function buildPageTextLayout(snapshot) {
|
|
1428
|
+
const glyphs = [];
|
|
1429
|
+
const runs = [];
|
|
1430
|
+
const frames = [IDENTITY_FRAME];
|
|
1431
|
+
for (const run of snapshot.runs) {
|
|
1432
|
+
if (!isRotatedGeometryRun(run)) {
|
|
1433
|
+
for (const g of run.glyphs) glyphs.push({
|
|
1434
|
+
loose: g.loose,
|
|
1435
|
+
tight: g.tight,
|
|
1436
|
+
space: g.space,
|
|
1437
|
+
empty: g.empty
|
|
1438
|
+
});
|
|
1439
|
+
runs.push({
|
|
1440
|
+
start: run.start,
|
|
1441
|
+
count: run.glyphs.length,
|
|
1442
|
+
rect: run.rect,
|
|
1443
|
+
fontSize: run.fontSize,
|
|
1444
|
+
frame: 0
|
|
1445
|
+
});
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
const frame = frameForRun(frames, run);
|
|
1449
|
+
const f = frames[frame];
|
|
1450
|
+
let runBox = null;
|
|
1451
|
+
for (const g of run.glyphs) {
|
|
1452
|
+
if (isEmpty(g)) {
|
|
1453
|
+
glyphs.push({
|
|
1454
|
+
loose: ZERO_RECT,
|
|
1455
|
+
space: g.space,
|
|
1456
|
+
empty: true
|
|
1457
|
+
});
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const loose = frameBoxOfQuad(f, g.loose);
|
|
1461
|
+
const tight = g.tight ? frameBoxOfQuad(f, g.tight) : void 0;
|
|
1462
|
+
glyphs.push({
|
|
1463
|
+
loose,
|
|
1464
|
+
tight,
|
|
1465
|
+
space: g.space
|
|
1466
|
+
});
|
|
1467
|
+
runBox = runBox ? {
|
|
1468
|
+
left: Math.min(runBox.left, loose.left),
|
|
1469
|
+
bottom: Math.min(runBox.bottom, loose.bottom),
|
|
1470
|
+
right: Math.max(runBox.right, loose.right),
|
|
1471
|
+
top: Math.max(runBox.top, loose.top)
|
|
1472
|
+
} : loose;
|
|
1473
|
+
}
|
|
1474
|
+
runs.push({
|
|
1475
|
+
start: run.start,
|
|
1476
|
+
count: run.glyphs.length,
|
|
1477
|
+
rect: runBox ?? ZERO_RECT,
|
|
1478
|
+
fontSize: run.fontSize,
|
|
1479
|
+
frame
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
glyphs,
|
|
1484
|
+
runs,
|
|
1485
|
+
frames
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const inRect = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.bottom && p.y <= r.top;
|
|
1489
|
+
function avgGlyphHeight(layout) {
|
|
1490
|
+
let total = 0;
|
|
1491
|
+
let count = 0;
|
|
1492
|
+
for (const g of layout.glyphs) {
|
|
1493
|
+
if (isEmpty(g)) continue;
|
|
1494
|
+
total += g.loose.top - g.loose.bottom;
|
|
1495
|
+
count++;
|
|
1496
|
+
}
|
|
1497
|
+
return count === 0 ? 0 : total / count;
|
|
1498
|
+
}
|
|
1499
|
+
/** The point expressed in every frame (index-aligned with `layout.frames`).
|
|
1500
|
+
* Frame 0 returns the point itself — no float round-trip on the fast path. */
|
|
1501
|
+
function pointPerFrame(layout, p) {
|
|
1502
|
+
return layout.frames.map((f, i) => i === 0 ? p : toFrame(f, p));
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* The glyph (char) index at a page-space point, or null when nothing is near
|
|
1506
|
+
* (so callers can show the pointer cursor off-text). PDFium `GetIndexAtPos`:
|
|
1507
|
+
* exact tight-box containment first, then a tolerance pass (closest by
|
|
1508
|
+
* Manhattan distance within `toleranceFactor × average glyph height`). Each
|
|
1509
|
+
* run tests the point in its own frame, so rotated text hit-tests exactly.
|
|
1510
|
+
*/
|
|
1511
|
+
function textGlyphAt(layout, point, toleranceFactor = 1.5) {
|
|
1512
|
+
const pts = pointPerFrame(layout, point);
|
|
1513
|
+
for (const run of layout.runs) {
|
|
1514
|
+
const q = pts[run.frame];
|
|
1515
|
+
if (!inRect(run.rect, q)) continue;
|
|
1516
|
+
for (let i = 0; i < run.count; i++) {
|
|
1517
|
+
const g = layout.glyphs[run.start + i];
|
|
1518
|
+
if (inRect(g.tight ?? g.loose, q)) return run.start + i;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (toleranceFactor <= 0) return null;
|
|
1522
|
+
const half = avgGlyphHeight(layout) * toleranceFactor / 2;
|
|
1523
|
+
let best = -1;
|
|
1524
|
+
let bestDist = Infinity;
|
|
1525
|
+
for (const run of layout.runs) {
|
|
1526
|
+
const q = pts[run.frame];
|
|
1527
|
+
const r = run.rect;
|
|
1528
|
+
if (q.x < r.left - half || q.x > r.right + half || q.y < r.bottom - half || q.y > r.top + half) continue;
|
|
1529
|
+
for (let i = 0; i < run.count; i++) {
|
|
1530
|
+
const g = layout.glyphs[run.start + i];
|
|
1531
|
+
if (isEmpty(g)) continue;
|
|
1532
|
+
const b = g.tight ?? g.loose;
|
|
1533
|
+
if (q.x < b.left - half || q.x > b.right + half || q.y < b.bottom - half || q.y > b.top + half) continue;
|
|
1534
|
+
const dx = Math.min(Math.abs(q.x - b.left), Math.abs(q.x - b.right));
|
|
1535
|
+
const dy = Math.min(Math.abs(q.y - b.bottom), Math.abs(q.y - b.top));
|
|
1536
|
+
if (dx + dy < bestDist - EDGE_EPSILON) {
|
|
1537
|
+
bestDist = dx + dy;
|
|
1538
|
+
best = run.start + i;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return best >= 0 ? best : null;
|
|
1543
|
+
}
|
|
1544
|
+
/** Double-click: the word around `glyph` (walk to space/empty glyphs both ways). */
|
|
1545
|
+
function expandTextRangeToWord(layout, glyph) {
|
|
1546
|
+
const n = layout.glyphs.length;
|
|
1547
|
+
if (glyph < 0 || glyph >= n) return [glyph, glyph];
|
|
1548
|
+
let from = glyph;
|
|
1549
|
+
while (from > 0 && !isBoundary(layout.glyphs[from - 1])) from--;
|
|
1550
|
+
let to = glyph;
|
|
1551
|
+
while (to < n - 1 && !isBoundary(layout.glyphs[to + 1])) to++;
|
|
1552
|
+
return [from, to];
|
|
1553
|
+
}
|
|
1554
|
+
/** Triple-click: the full visual line — same-frame runs whose vertical extent
|
|
1555
|
+
* overlaps the anchor run's. A differently-oriented run is a line boundary. */
|
|
1556
|
+
function expandTextRangeToLine(layout, glyph) {
|
|
1557
|
+
const ri = layout.runs.findIndex((r) => glyph >= r.start && glyph < r.start + r.count);
|
|
1558
|
+
if (ri < 0) return [glyph, glyph];
|
|
1559
|
+
const anchor = layout.runs[ri];
|
|
1560
|
+
const bottom = anchor.rect.bottom;
|
|
1561
|
+
const top = anchor.rect.top;
|
|
1562
|
+
let from = anchor.start;
|
|
1563
|
+
let to = anchor.start + anchor.count - 1;
|
|
1564
|
+
for (let r = ri - 1; r >= 0; r--) {
|
|
1565
|
+
const run = layout.runs[r];
|
|
1566
|
+
if (isZero(run.rect)) continue;
|
|
1567
|
+
if (run.frame !== anchor.frame) break;
|
|
1568
|
+
if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
|
|
1569
|
+
from = run.start;
|
|
1570
|
+
}
|
|
1571
|
+
for (let r = ri + 1; r < layout.runs.length; r++) {
|
|
1572
|
+
const run = layout.runs[r];
|
|
1573
|
+
if (isZero(run.rect)) continue;
|
|
1574
|
+
if (run.frame !== anchor.frame) break;
|
|
1575
|
+
if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
|
|
1576
|
+
to = run.start + run.count - 1;
|
|
1577
|
+
}
|
|
1578
|
+
return [from, to];
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* A single glyph's oriented cell in page space, or null for degenerate
|
|
1582
|
+
* glyphs — the anchor for caret placement at a selection boundary.
|
|
1583
|
+
*/
|
|
1584
|
+
function textGlyphQuad(layout, glyph) {
|
|
1585
|
+
const g = layout.glyphs[glyph];
|
|
1586
|
+
if (!g || isEmpty(g)) return null;
|
|
1587
|
+
const b = g.loose;
|
|
1588
|
+
if (b.right <= b.left || b.top <= b.bottom) return null;
|
|
1589
|
+
const run = layout.runs.find((r) => glyph >= r.start && glyph < r.start + r.count);
|
|
1590
|
+
const f = layout.frames[run?.frame ?? 0];
|
|
1591
|
+
return quadOfFrameRect(f, run?.frame ?? 0, b);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Merged visual-line segments for the half-open char range
|
|
1595
|
+
* `[start, start + count)` — the canonical segmentation. Chars
|
|
1596
|
+
* outside the layout are ignored.
|
|
1597
|
+
*/
|
|
1598
|
+
function textSegmentsForRange(layout, start, count) {
|
|
1599
|
+
if (count <= 0 || layout.glyphs.length === 0) return [];
|
|
1600
|
+
const lo = Math.max(0, start);
|
|
1601
|
+
const hi = Math.min(layout.glyphs.length - 1, start + count - 1);
|
|
1602
|
+
if (hi < lo) return [];
|
|
1603
|
+
const subRuns = [];
|
|
1604
|
+
for (const run of layout.runs) {
|
|
1605
|
+
const runEnd = run.start + run.count - 1;
|
|
1606
|
+
if (runEnd < lo || run.start > hi) continue;
|
|
1607
|
+
const s = Math.max(lo, run.start);
|
|
1608
|
+
const e = Math.min(hi, runEnd);
|
|
1609
|
+
let left = Infinity;
|
|
1610
|
+
let right = -Infinity;
|
|
1611
|
+
let bottom = Infinity;
|
|
1612
|
+
let top = -Infinity;
|
|
1613
|
+
let charCountAcc = 0;
|
|
1614
|
+
let widthSum = 0;
|
|
1615
|
+
let prevRight = -Infinity;
|
|
1616
|
+
let firstX = 0;
|
|
1617
|
+
let lastX = 0;
|
|
1618
|
+
const flush = () => {
|
|
1619
|
+
if (charCountAcc > 0 && left !== Infinity) subRuns.push({
|
|
1620
|
+
rect: {
|
|
1621
|
+
left,
|
|
1622
|
+
bottom,
|
|
1623
|
+
right,
|
|
1624
|
+
top
|
|
1625
|
+
},
|
|
1626
|
+
charCount: charCountAcc,
|
|
1627
|
+
fontSize: run.fontSize,
|
|
1628
|
+
frame: run.frame,
|
|
1629
|
+
firstX,
|
|
1630
|
+
lastX
|
|
1631
|
+
});
|
|
1632
|
+
left = Infinity;
|
|
1633
|
+
right = -Infinity;
|
|
1634
|
+
bottom = Infinity;
|
|
1635
|
+
top = -Infinity;
|
|
1636
|
+
charCountAcc = 0;
|
|
1637
|
+
widthSum = 0;
|
|
1638
|
+
prevRight = -Infinity;
|
|
1639
|
+
};
|
|
1640
|
+
for (let ci = s; ci <= e; ci++) {
|
|
1641
|
+
const g = layout.glyphs[ci];
|
|
1642
|
+
if (isEmpty(g)) continue;
|
|
1643
|
+
const b = g.loose;
|
|
1644
|
+
if (charCountAcc > 0 && prevRight > -Infinity) {
|
|
1645
|
+
const avg = widthSum / charCountAcc;
|
|
1646
|
+
if (avg > 0 && Math.abs(b.left - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
|
|
1647
|
+
}
|
|
1648
|
+
const centerX = (b.left + b.right) / 2;
|
|
1649
|
+
if (charCountAcc === 0) firstX = centerX;
|
|
1650
|
+
lastX = centerX;
|
|
1651
|
+
left = Math.min(left, b.left);
|
|
1652
|
+
right = Math.max(right, b.right);
|
|
1653
|
+
bottom = Math.min(bottom, b.bottom);
|
|
1654
|
+
top = Math.max(top, b.top);
|
|
1655
|
+
charCountAcc++;
|
|
1656
|
+
widthSum += b.right - b.left;
|
|
1657
|
+
prevRight = b.right;
|
|
1658
|
+
}
|
|
1659
|
+
flush();
|
|
1660
|
+
}
|
|
1661
|
+
return mergeAdjacentSubRuns(subRuns).map((m) => materializeSegment(layout, m));
|
|
1662
|
+
}
|
|
1663
|
+
function mergeAdjacentSubRuns(runs) {
|
|
1664
|
+
const out = [];
|
|
1665
|
+
let prev = null;
|
|
1666
|
+
let cur = null;
|
|
1667
|
+
for (const run of runs) {
|
|
1668
|
+
if (prev && cur && prev.frame === run.frame && shouldMerge(prev, run)) cur = {
|
|
1669
|
+
rect: {
|
|
1670
|
+
left: Math.min(cur.rect.left, run.rect.left),
|
|
1671
|
+
bottom: Math.min(cur.rect.bottom, run.rect.bottom),
|
|
1672
|
+
right: Math.max(cur.rect.right, run.rect.right),
|
|
1673
|
+
top: Math.max(cur.rect.top, run.rect.top)
|
|
1674
|
+
},
|
|
1675
|
+
frame: cur.frame,
|
|
1676
|
+
firstX: cur.firstX,
|
|
1677
|
+
lastX: run.lastX
|
|
1678
|
+
};
|
|
1679
|
+
else {
|
|
1680
|
+
if (cur) out.push(cur);
|
|
1681
|
+
cur = {
|
|
1682
|
+
rect: run.rect,
|
|
1683
|
+
frame: run.frame,
|
|
1684
|
+
firstX: run.firstX,
|
|
1685
|
+
lastX: run.lastX
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
prev = run;
|
|
1689
|
+
}
|
|
1690
|
+
if (cur && cur.rect.right > cur.rect.left && cur.rect.top > cur.rect.bottom) out.push(cur);
|
|
1691
|
+
return out;
|
|
1692
|
+
}
|
|
1693
|
+
/** Segment quad for a frame-local rect (frame 0: the rect's own corners,
|
|
1694
|
+
* no float round-trip). The frame is y-up, so the upper corners sit at the
|
|
1695
|
+
* frame top. */
|
|
1696
|
+
function quadOfFrameRect(f, frameIndex, r) {
|
|
1697
|
+
if (frameIndex === 0) return {
|
|
1698
|
+
upperLeft: {
|
|
1699
|
+
x: r.left,
|
|
1700
|
+
y: r.top
|
|
1701
|
+
},
|
|
1702
|
+
upperRight: {
|
|
1703
|
+
x: r.right,
|
|
1704
|
+
y: r.top
|
|
1705
|
+
},
|
|
1706
|
+
lowerLeft: {
|
|
1707
|
+
x: r.left,
|
|
1708
|
+
y: r.bottom
|
|
1709
|
+
},
|
|
1710
|
+
lowerRight: {
|
|
1711
|
+
x: r.right,
|
|
1712
|
+
y: r.bottom
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
return {
|
|
1716
|
+
upperLeft: fromFrame(f, {
|
|
1717
|
+
x: r.left,
|
|
1718
|
+
y: r.top
|
|
1719
|
+
}),
|
|
1720
|
+
upperRight: fromFrame(f, {
|
|
1721
|
+
x: r.right,
|
|
1722
|
+
y: r.top
|
|
1723
|
+
}),
|
|
1724
|
+
lowerLeft: fromFrame(f, {
|
|
1725
|
+
x: r.left,
|
|
1726
|
+
y: r.bottom
|
|
1727
|
+
}),
|
|
1728
|
+
lowerRight: fromFrame(f, {
|
|
1729
|
+
x: r.right,
|
|
1730
|
+
y: r.bottom
|
|
1731
|
+
})
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
function materializeSegment(layout, m) {
|
|
1735
|
+
const frame = layout.frames[m.frame];
|
|
1736
|
+
const quad = quadOfFrameRect(frame, m.frame, m.rect);
|
|
1737
|
+
return {
|
|
1738
|
+
quad,
|
|
1739
|
+
rect: m.frame === 0 ? m.rect : {
|
|
1740
|
+
left: Math.min(quad.upperLeft.x, quad.upperRight.x, quad.lowerLeft.x, quad.lowerRight.x),
|
|
1741
|
+
bottom: Math.min(quad.upperLeft.y, quad.upperRight.y, quad.lowerLeft.y, quad.lowerRight.y),
|
|
1742
|
+
right: Math.max(quad.upperLeft.x, quad.upperRight.x, quad.lowerLeft.x, quad.lowerRight.x),
|
|
1743
|
+
top: Math.max(quad.upperLeft.y, quad.upperRight.y, quad.lowerLeft.y, quad.lowerRight.y)
|
|
1744
|
+
},
|
|
1745
|
+
advance: m.lastX >= m.firstX ? 1 : -1
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function shouldMerge(a, b) {
|
|
1749
|
+
if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
|
|
1750
|
+
if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
|
|
1751
|
+
}
|
|
1752
|
+
if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;
|
|
1753
|
+
const aw = (a.rect.right - a.rect.left) / a.charCount;
|
|
1754
|
+
const bw = (b.rect.right - b.rect.left) / b.charCount;
|
|
1755
|
+
const aL = a.rect.left - aw;
|
|
1756
|
+
const aR = a.rect.right + aw;
|
|
1757
|
+
const bL = b.rect.left - bw;
|
|
1758
|
+
return aL < b.rect.right + bw && aR > bL;
|
|
1759
|
+
}
|
|
1760
|
+
function verticalOverlap(a, b) {
|
|
1761
|
+
const ah = a.top - a.bottom;
|
|
1762
|
+
const bh = b.top - b.bottom;
|
|
1763
|
+
if (ah <= 0 || bh <= 0) return 0;
|
|
1764
|
+
const u = Math.max(a.top, b.top) - Math.min(a.bottom, b.bottom);
|
|
1765
|
+
if (u === ah || u === bh) return 1;
|
|
1766
|
+
return Math.max(0, Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom)) / u;
|
|
1767
|
+
}
|
|
1768
|
+
const isZero = (r) => r.right === r.left && r.top === r.bottom;
|
|
1769
|
+
function overlapV(bottom1, top1, bottom2, top2) {
|
|
1770
|
+
const u = Math.max(top1, top2) - Math.min(bottom1, bottom2);
|
|
1771
|
+
if (u === 0) return false;
|
|
1772
|
+
return Math.max(0, Math.min(top1, top2) - Math.max(bottom1, bottom2)) / u >= LINE_OVERLAP_THRESHOLD;
|
|
1773
|
+
}
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region src/pageSpace/text.ts
|
|
1776
|
+
/**
|
|
1777
|
+
* A character with no box of its own (`empty`) carries a zeroed box or quad
|
|
1778
|
+
* that says so, in either space, and so does a run of nothing but such
|
|
1779
|
+
* characters; it is not a place, so it never converts.
|
|
1780
|
+
*/
|
|
1781
|
+
const NO_BOX$1 = {
|
|
1782
|
+
x: 0,
|
|
1783
|
+
y: 0,
|
|
1784
|
+
width: 0,
|
|
1785
|
+
height: 0
|
|
1786
|
+
};
|
|
1787
|
+
const NO_RECT = {
|
|
1788
|
+
left: 0,
|
|
1789
|
+
bottom: 0,
|
|
1790
|
+
right: 0,
|
|
1791
|
+
top: 0
|
|
1792
|
+
};
|
|
1793
|
+
const NO_QUAD = {
|
|
1794
|
+
upperLeft: {
|
|
1795
|
+
x: 0,
|
|
1796
|
+
y: 0
|
|
1797
|
+
},
|
|
1798
|
+
upperRight: {
|
|
1799
|
+
x: 0,
|
|
1800
|
+
y: 0
|
|
1801
|
+
},
|
|
1802
|
+
lowerLeft: {
|
|
1803
|
+
x: 0,
|
|
1804
|
+
y: 0
|
|
1805
|
+
},
|
|
1806
|
+
lowerRight: {
|
|
1807
|
+
x: 0,
|
|
1808
|
+
y: 0
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
/** Whether a run has a character with a box of its own. */
|
|
1812
|
+
const hasBox = (run) => run.glyphs.some((glyph) => !glyph.empty);
|
|
1813
|
+
/** A page's text geometry in page space. `visible` is the page's visible box. */
|
|
1814
|
+
function pageGeometryOf(snapshot, visible) {
|
|
1815
|
+
return {
|
|
1816
|
+
...snapshot,
|
|
1817
|
+
runs: snapshot.runs.map((run) => {
|
|
1818
|
+
const rect = hasBox(run) ? pageBoxOf(run.rect, visible) : NO_BOX$1;
|
|
1819
|
+
if (isRotatedGeometryRun(run)) return {
|
|
1820
|
+
...run,
|
|
1821
|
+
rect,
|
|
1822
|
+
glyphs: run.glyphs.map(({ loose, tight, ...glyph }) => ({
|
|
1823
|
+
...glyph,
|
|
1824
|
+
loose: glyph.empty ? NO_QUAD : pageQuadOf(loose, visible),
|
|
1825
|
+
...tight ? { tight: pageQuadOf(tight, visible) } : {}
|
|
1826
|
+
}))
|
|
1827
|
+
};
|
|
1828
|
+
return {
|
|
1829
|
+
...run,
|
|
1830
|
+
rect,
|
|
1831
|
+
glyphs: run.glyphs.map(({ loose, tight, ...glyph }) => ({
|
|
1832
|
+
...glyph,
|
|
1833
|
+
loose: glyph.empty ? NO_BOX$1 : pageBoxOf(loose, visible),
|
|
1834
|
+
...tight ? { tight: pageBoxOf(tight, visible) } : {}
|
|
1835
|
+
}))
|
|
1836
|
+
};
|
|
1837
|
+
})
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
/** A page-space run flipped top to bottom, for the y-up math of the text layout and its helpers. */
|
|
1841
|
+
function mirroredRun(run) {
|
|
1842
|
+
const rect = hasBox(run) ? mirroredRect(run.rect) : NO_RECT;
|
|
1843
|
+
if (isRotatedGeometryRun(run)) return {
|
|
1844
|
+
...run,
|
|
1845
|
+
rect,
|
|
1846
|
+
glyphs: run.glyphs.map(({ loose, tight, ...glyph }) => ({
|
|
1847
|
+
...glyph,
|
|
1848
|
+
loose: glyph.empty ? NO_QUAD : mirroredQuad(loose),
|
|
1849
|
+
...tight ? { tight: mirroredQuad(tight) } : {}
|
|
1850
|
+
}))
|
|
1851
|
+
};
|
|
1852
|
+
return {
|
|
1853
|
+
...run,
|
|
1854
|
+
rect,
|
|
1855
|
+
glyphs: run.glyphs.map(({ loose, tight, ...glyph }) => ({
|
|
1856
|
+
...glyph,
|
|
1857
|
+
loose: glyph.empty ? NO_RECT : mirroredRect(loose),
|
|
1858
|
+
...tight ? { tight: mirroredRect(tight) } : {}
|
|
1859
|
+
}))
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function pageTextSegmentOf(segment, visible) {
|
|
1863
|
+
return {
|
|
1864
|
+
...segment,
|
|
1865
|
+
quad: pageQuadOf(segment.quad, visible),
|
|
1866
|
+
rect: pageBoxOf(segment.rect, visible)
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
/** A search batch in page space: each match measured on its own page. */
|
|
1870
|
+
function pageSearchSliceOf(slice, boxOf) {
|
|
1871
|
+
return {
|
|
1872
|
+
...slice,
|
|
1873
|
+
matches: slice.matches.map((match) => {
|
|
1874
|
+
const visible = boxOf(match.page);
|
|
1875
|
+
return {
|
|
1876
|
+
...match,
|
|
1877
|
+
segments: match.segments.map((segment) => pageTextSegmentOf(segment, visible))
|
|
1878
|
+
};
|
|
1879
|
+
})
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* The layout of a page's text geometry in page space. It runs the same
|
|
1884
|
+
* layout as the file's coordinates do, on the geometry flipped top to bottom,
|
|
1885
|
+
* and flips each answer back: the flip is exact and the layout doesn't depend
|
|
1886
|
+
* on where the origin is.
|
|
1887
|
+
*/
|
|
1888
|
+
function createTextLayout(snapshot) {
|
|
1889
|
+
const layout = createPdfTextLayout({
|
|
1890
|
+
...snapshot,
|
|
1891
|
+
runs: snapshot.runs.map(mirroredRun)
|
|
1892
|
+
});
|
|
1893
|
+
const at = (value) => typeof value === "number" ? value : mirroredPoint(value);
|
|
1894
|
+
return {
|
|
1895
|
+
charCount: layout.charCount,
|
|
1896
|
+
runs: snapshot.runs,
|
|
1897
|
+
charAt: (point) => layout.charAt(mirroredPoint(point)),
|
|
1898
|
+
wordAt: (value) => layout.wordAt(at(value)),
|
|
1899
|
+
lineAt: (value) => layout.lineAt(at(value)),
|
|
1900
|
+
segments: (range) => layout.segments(range).map((segment) => ({
|
|
1901
|
+
...segment,
|
|
1902
|
+
quad: unmirroredQuad(segment.quad),
|
|
1903
|
+
rect: unmirroredBox(segment.rect)
|
|
1904
|
+
})),
|
|
1905
|
+
charQuad: (index) => {
|
|
1906
|
+
const quad = layout.charQuad(index);
|
|
1907
|
+
return quad ? unmirroredQuad(quad) : null;
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
//#endregion
|
|
1912
|
+
//#region src/annotation/drawnPoints.ts
|
|
1913
|
+
/**
|
|
1914
|
+
* The points of a line, polyline, polygon or ink as the page shows them: the
|
|
1915
|
+
* upright points a read gives, turned by `rotation` about the middle of their
|
|
1916
|
+
* box. One list for a line (its two ends) or a polygon, one per ink stroke;
|
|
1917
|
+
* `null` for any other kind.
|
|
1918
|
+
*/
|
|
1919
|
+
function pdfDrawnPointsOf(annotation) {
|
|
1920
|
+
let sets;
|
|
1921
|
+
switch (annotation.subtype) {
|
|
1922
|
+
case "line":
|
|
1923
|
+
sets = [[annotation.linePoints.start, annotation.linePoints.end]];
|
|
1924
|
+
break;
|
|
1925
|
+
case "polyline":
|
|
1926
|
+
case "polygon":
|
|
1927
|
+
sets = [annotation.vertices];
|
|
1928
|
+
break;
|
|
1929
|
+
case "ink":
|
|
1930
|
+
sets = annotation.inkList.map((stroke) => [...stroke]);
|
|
1931
|
+
break;
|
|
1932
|
+
default: return null;
|
|
1933
|
+
}
|
|
1934
|
+
if (!annotation.rotation) return sets.map((set) => set.map((point) => ({ ...point })));
|
|
1935
|
+
const turn = pdfTurnOfUpright(sets.flat(), annotation.rotation);
|
|
1936
|
+
return sets.map((set) => set.map((point) => pdfPointTurned(point, turn)));
|
|
1937
|
+
}
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/annotation/resolve/annotationOfDraft.ts
|
|
1940
|
+
const NO_BOX = {
|
|
1941
|
+
x: 0,
|
|
1942
|
+
y: 0,
|
|
1943
|
+
width: 0,
|
|
1944
|
+
height: 0
|
|
1945
|
+
};
|
|
1946
|
+
/**
|
|
1947
|
+
* A free text's rich text as a read spells it, and the text style it reads
|
|
1948
|
+
* back with: the resolved body over the engine's defaults, and its size,
|
|
1949
|
+
* text color and alignment beside it. The font keeps the name the draft
|
|
1950
|
+
* gave, or the default.
|
|
1951
|
+
*/
|
|
1952
|
+
function freeTextReadOf(given, read) {
|
|
1953
|
+
const richText = given.richText;
|
|
1954
|
+
const body = {
|
|
1955
|
+
...DEFAULT_RICH_TEXT_BODY,
|
|
1956
|
+
...richText?.body
|
|
1957
|
+
};
|
|
1958
|
+
body.color = body.color.toLowerCase();
|
|
1959
|
+
read.richText = {
|
|
1960
|
+
...read.richText,
|
|
1961
|
+
body
|
|
1962
|
+
};
|
|
1963
|
+
read.fontSize = body.size;
|
|
1964
|
+
read.fontColor = body.color;
|
|
1965
|
+
read.textAlign = body.align;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* The annotation a resolved draft creates, as a read spells it: the draft's
|
|
1969
|
+
* data fields, the kind's defaults for those it leaves out, and what
|
|
1970
|
+
* `context` says for the fields the engine owns or stamps. Works in either
|
|
1971
|
+
* space: it only moves values by field name.
|
|
1972
|
+
*/
|
|
1973
|
+
function annotationOfResolvedDraft(resolved, context) {
|
|
1974
|
+
const subtype = resolved.subtype;
|
|
1975
|
+
const owners = kindFactsOf(subtype)?.owners ?? {};
|
|
1976
|
+
const defaults = annotationDefaultsOf(subtype);
|
|
1977
|
+
const given = resolved;
|
|
1978
|
+
const read = { subtype };
|
|
1979
|
+
for (const [name, owner] of Object.entries(owners)) if (owner === "data" && given[name] !== void 0) read[name] = readValueOf(name, given[name], void 0);
|
|
1980
|
+
else if (owner === "data") read[name] = defaults[name] ?? null;
|
|
1981
|
+
else if (owner === "attribution") read[name] = context.attribution?.[name] ?? null;
|
|
1982
|
+
else read[name] = null;
|
|
1983
|
+
if (read.nm === null && context.nm !== void 0) read.nm = context.nm;
|
|
1984
|
+
read.ref = context.ref;
|
|
1985
|
+
read.page = context.ref.page;
|
|
1986
|
+
read.hasAppearance = !UNBAKED_KINDS.has(subtype);
|
|
1987
|
+
if (read.rect === null) read.rect = context.rect ?? given.rect ?? given.box ?? NO_BOX;
|
|
1988
|
+
if (subtype === "free-text") freeTextReadOf(given, read);
|
|
1989
|
+
if (subtype === "link") {
|
|
1990
|
+
const target = given.target;
|
|
1991
|
+
read.actions = target && (target.kind === "uri" || target.kind === "goto") ? { activate: actionTreeOf(target) } : null;
|
|
1992
|
+
}
|
|
1993
|
+
if (subtype === "widget") read.fieldFamily = "unknown";
|
|
1994
|
+
return read;
|
|
1995
|
+
}
|
|
1996
|
+
//#endregion
|
|
1997
|
+
//#region src/pageSpace/helpers.ts
|
|
1998
|
+
/**
|
|
1999
|
+
* The engine's geometry helpers, for page-space values. Each flips its
|
|
2000
|
+
* values top to bottom, runs the helper written for the file's coordinates
|
|
2001
|
+
* (y up), and flips the answer back: exact, and needing no page box, since
|
|
2002
|
+
* none of that math depends on where the origin is.
|
|
2003
|
+
*/
|
|
2004
|
+
const mirroredTurn = (turn) => ({
|
|
2005
|
+
degrees: turn.degrees,
|
|
2006
|
+
center: mirroredPoint(turn.center)
|
|
2007
|
+
});
|
|
2008
|
+
const unmirroredTurn = (turn) => ({
|
|
2009
|
+
degrees: turn.degrees,
|
|
2010
|
+
center: unmirroredPoint(turn.center)
|
|
2011
|
+
});
|
|
2012
|
+
/** The box around `points`; a zero box at the origin when there are none. */
|
|
2013
|
+
const pagePointsBounds = (points) => unmirroredBox(pdfPointsBounds(points.map(mirroredPoint)));
|
|
2014
|
+
/** `point` turned by `turn`; a negative angle turns it back. */
|
|
2015
|
+
const pagePointTurned = (point, turn) => unmirroredPoint(pdfPointTurned(mirroredPoint(point), mirroredTurn(turn)));
|
|
2016
|
+
/** `point` turned back by `turn`. */
|
|
2017
|
+
const pagePointUnturned = (point, turn) => unmirroredPoint(pdfPointUnturned(mirroredPoint(point), mirroredTurn(turn)));
|
|
2018
|
+
/** The turn that draws upright `points`: `degrees` about the middle of their box. */
|
|
2019
|
+
const pageTurnOfUpright = (points, degrees) => unmirroredTurn(pdfTurnOfUpright(points.map(mirroredPoint), degrees));
|
|
2020
|
+
/** The turn drawn `points` were made with: `degrees` about the middle of their upright box. */
|
|
2021
|
+
const pageTurnOfDrawn = (points, degrees) => unmirroredTurn(pdfTurnOfDrawn(points.map(mirroredPoint), degrees));
|
|
2022
|
+
/** The upright box around a quad, whatever its turn or skew. */
|
|
2023
|
+
const pageQuadBounds = (quad) => unmirroredBox(pdfQuadBounds(mirroredQuad(quad)));
|
|
2024
|
+
/** A glyph's loose cell as a quad. */
|
|
2025
|
+
const pageGlyphLooseQuad = (run, index) => unmirroredQuad(glyphLooseQuad(mirroredRun(run), index));
|
|
2026
|
+
/** A glyph's loose cell as an upright box. */
|
|
2027
|
+
const pageGlyphLooseBounds = (run, index) => unmirroredBox(glyphLooseBounds(mirroredRun(run), index));
|
|
2028
|
+
/**
|
|
2029
|
+
* The points of a line, polyline, polygon or ink as the page shows them: the
|
|
2030
|
+
* upright points a read gives, turned by `rotation` about the middle of their
|
|
2031
|
+
* box. One list for a line (its two ends) or a polygon, one per ink stroke;
|
|
2032
|
+
* `null` for any other kind.
|
|
2033
|
+
*/
|
|
2034
|
+
function drawnPointsOf(annotation) {
|
|
2035
|
+
const flip = (points) => points.map(mirroredPoint);
|
|
2036
|
+
let mirrored;
|
|
2037
|
+
switch (annotation.subtype) {
|
|
2038
|
+
case "line":
|
|
2039
|
+
mirrored = {
|
|
2040
|
+
subtype: "line",
|
|
2041
|
+
rotation: annotation.rotation,
|
|
2042
|
+
linePoints: {
|
|
2043
|
+
start: mirroredPoint(annotation.linePoints.start),
|
|
2044
|
+
end: mirroredPoint(annotation.linePoints.end)
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
break;
|
|
2048
|
+
case "polyline":
|
|
2049
|
+
case "polygon":
|
|
2050
|
+
mirrored = {
|
|
2051
|
+
subtype: annotation.subtype,
|
|
2052
|
+
rotation: annotation.rotation,
|
|
2053
|
+
vertices: flip(annotation.vertices)
|
|
2054
|
+
};
|
|
2055
|
+
break;
|
|
2056
|
+
case "ink":
|
|
2057
|
+
mirrored = {
|
|
2058
|
+
subtype: "ink",
|
|
2059
|
+
rotation: annotation.rotation,
|
|
2060
|
+
inkList: annotation.inkList.map(flip)
|
|
2061
|
+
};
|
|
2062
|
+
break;
|
|
2063
|
+
default: return null;
|
|
2064
|
+
}
|
|
2065
|
+
const sets = pdfDrawnPointsOf(mirrored);
|
|
2066
|
+
return sets && sets.map((set) => set.map(unmirroredPoint));
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* The turn an annotation's appearance raster is drawn without, degrees
|
|
2070
|
+
* clockwise, or `null` when the raster is drawn as the page shows it. A box
|
|
2071
|
+
* kind drawn turned whose drawing stays inside the turned box (its `rect` is
|
|
2072
|
+
* the upright box around it) renders upright over its `box`, for the
|
|
2073
|
+
* consumer to turn about the middle of `box`: a new turn needs no new
|
|
2074
|
+
* raster. A callout (its line isn't turned) and a drawing that reaches past
|
|
2075
|
+
* its box (a cloudy border's bumps) render as the page shows them, placed by
|
|
2076
|
+
* `rect`.
|
|
2077
|
+
*/
|
|
2078
|
+
function appearanceTurnOf(annotation) {
|
|
2079
|
+
return pdfAppearanceTurnOf({
|
|
2080
|
+
...annotation,
|
|
2081
|
+
rect: mirroredRect(annotation.rect),
|
|
2082
|
+
box: annotation.box ? mirroredRect(annotation.box) : annotation.box
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
/** Page space with its origin as the file's: turning one into the other is the mirror. */
|
|
2086
|
+
const MIRROR = {
|
|
2087
|
+
left: 0,
|
|
2088
|
+
bottom: 0,
|
|
2089
|
+
right: 0,
|
|
2090
|
+
top: 0
|
|
2091
|
+
};
|
|
2092
|
+
/** Shape fields hold no destinations, so nothing asks for another page's box. */
|
|
2093
|
+
const noOtherPage = () => {
|
|
2094
|
+
throw new Error("a shape field holds no destination");
|
|
2095
|
+
};
|
|
2096
|
+
/**
|
|
2097
|
+
* The shape fields that put `annotation` at `rect`, the rule an update's
|
|
2098
|
+
* `rect` follows (`create({ ...copy, ...shapeForRect(copy, target) })` puts a
|
|
2099
|
+
* copy there).
|
|
2100
|
+
*
|
|
2101
|
+
* - A kind whose shape is its rect (note, file attachment, link, popup,
|
|
2102
|
+
* redaction) takes `rect` as it is.
|
|
2103
|
+
* - A drawn kind has every place it holds mapped from its current `rect` onto
|
|
2104
|
+
* `rect`, each axis on its own, as Acrobat does when a script sets
|
|
2105
|
+
* `annot.rect`: a rect of the same size moves the shape, a bigger one
|
|
2106
|
+
* stretches it. The engine then works out the rect from the shape, so it
|
|
2107
|
+
* can differ a little from `rect` (a stroke keeps its width). A box at a
|
|
2108
|
+
* quarter turn stretches along its own sides, as it stands on the page.
|
|
2109
|
+
*
|
|
2110
|
+
* A drawn kind refuses a rect that would squash it to no width or height, a
|
|
2111
|
+
* rect of another size when it's turned (it can only move) unless it is a
|
|
2112
|
+
* box at a quarter turn, and one when its own rect has no width or height to
|
|
2113
|
+
* stretch. Each refusal is `InvalidArg` on `rect`.
|
|
2114
|
+
*/
|
|
2115
|
+
function shapeForRect(annotation, rect) {
|
|
2116
|
+
if (!DRAWN_RECT_KINDS.has(annotation.subtype)) return { rect };
|
|
2117
|
+
const read = annotation;
|
|
2118
|
+
const placed = pdfShapeForRect(pdfAnnotationOf(Object.fromEntries([
|
|
2119
|
+
"subtype",
|
|
2120
|
+
"rect",
|
|
2121
|
+
...shapeFieldsOf(annotation.subtype)
|
|
2122
|
+
].filter((name) => read[name] !== void 0).map((name) => [name, read[name]])), MIRROR, noOtherPage), mirroredRect(rect));
|
|
2123
|
+
const { subtype: _kind, ...fields } = pageAnnotationPatchOf({
|
|
2124
|
+
subtype: annotation.subtype,
|
|
2125
|
+
...placed
|
|
2126
|
+
}, MIRROR, noOtherPage);
|
|
2127
|
+
return fields;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* `patch` with its `rect` resolved as the engine resolves it on update (the
|
|
2131
|
+
* same step, `pdfResolveRectCommand`):
|
|
2132
|
+
*
|
|
2133
|
+
* - the rect the annotation reads, sent back, is dropped: nothing moves;
|
|
2134
|
+
* - on a drawn kind, another rect becomes the shape fields that put the
|
|
2135
|
+
* drawing there (`shapeForRect`);
|
|
2136
|
+
* - a new rect with a changed shape field is refused (`InvalidArg` on
|
|
2137
|
+
* `rect`): which of the two to follow would be a guess.
|
|
2138
|
+
*
|
|
2139
|
+
* A kind whose shape is its rect keeps it as a plain field. A viewer runs
|
|
2140
|
+
* this before it merges fields it writes itself, so a `rect` behaves the same
|
|
2141
|
+
* from every door. Values `patch` gives come back as given.
|
|
2142
|
+
*/
|
|
2143
|
+
function resolveRectCommand(annotation, patch) {
|
|
2144
|
+
const given = patch;
|
|
2145
|
+
if (given.rect === void 0 || !DRAWN_RECT_KINDS.has(annotation.subtype)) return patch;
|
|
2146
|
+
const read = annotation;
|
|
2147
|
+
const shapeNames = shapeFieldsOf(annotation.subtype);
|
|
2148
|
+
const pick = (from, names) => Object.fromEntries(names.filter((name) => from[name] !== void 0).map((name) => [name, from[name]]));
|
|
2149
|
+
const { subtype: _kind, ...placed } = pageAnnotationPatchOf(pdfResolveRectCommand(pdfAnnotationOf({
|
|
2150
|
+
subtype: annotation.subtype,
|
|
2151
|
+
...pick(read, ["rect", ...shapeNames])
|
|
2152
|
+
}, MIRROR, noOtherPage), pdfAnnotationPatchOf({
|
|
2153
|
+
subtype: annotation.subtype,
|
|
2154
|
+
...pick(given, ["rect", ...shapeNames])
|
|
2155
|
+
}, MIRROR, noOtherPage)), MIRROR, noOtherPage);
|
|
2156
|
+
const { rect: _rect, ...rest } = given;
|
|
2157
|
+
const out = { ...rest };
|
|
2158
|
+
for (const [name, value] of Object.entries(placed)) out[name] = given[name] !== void 0 && semanticEqual(value, given[name]) ? given[name] : value;
|
|
2159
|
+
return out;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Every page's box is the mirror, so a link's destination makes the round
|
|
2163
|
+
* trip into the file's coordinates and back unchanged, with no real page.
|
|
2164
|
+
*/
|
|
2165
|
+
const mirrorEveryPage = () => MIRROR;
|
|
2166
|
+
/**
|
|
2167
|
+
* The patch the engine writes for `patch` on `current`, as a viewer predicts
|
|
2168
|
+
* it: its subtype filled in, and every field that follows from it stated. A
|
|
2169
|
+
* drawn kind's new `rect` becomes the shape fields that put it there, values
|
|
2170
|
+
* sent back as read are dropped, a note's standard review state brings its
|
|
2171
|
+
* state model, a free text's contents and rich text follow each other, a
|
|
2172
|
+
* callout's line stays attached to its moved box, and a measurement's label
|
|
2173
|
+
* follows its points and scale. Values `patch` gives come back as given.
|
|
2174
|
+
*
|
|
2175
|
+
* Throws `InvalidArg` for what the engine refuses without its schemas: a
|
|
2176
|
+
* field the kind doesn't declare, a changed subtype or name, a rect with a
|
|
2177
|
+
* new shape or that isn't finite, and where nothing can follow. The engine
|
|
2178
|
+
* checks the values against the kind's schema when the update reaches it,
|
|
2179
|
+
* and refuses there what this lets through.
|
|
2180
|
+
*/
|
|
2181
|
+
function resolveAnnotationPatch(current, patch, options = {}) {
|
|
2182
|
+
const back = pageAnnotationPatchOf(pdfPredictAnnotationPatch(pdfAnnotationOf(current, MIRROR, mirrorEveryPage), pdfAnnotationPatchOf({
|
|
2183
|
+
...patch,
|
|
2184
|
+
subtype: patch.subtype ?? current.subtype
|
|
2185
|
+
}, MIRROR, mirrorEveryPage), options), MIRROR, mirrorEveryPage);
|
|
2186
|
+
const given = patch;
|
|
2187
|
+
for (const name of Object.keys(back)) if (given[name] !== void 0 && semanticEqual(back[name], given[name])) back[name] = given[name];
|
|
2188
|
+
return back;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* `current` as the engine will read it after `patch`: what a viewer shows
|
|
2192
|
+
* while the write is on its way. Fields the engine works out or stamps itself
|
|
2193
|
+
* (a drawn kind's `rect`, the modified date and author) keep their current
|
|
2194
|
+
* value until the engine's answer brings the new one. A field `patch` leaves
|
|
2195
|
+
* alone keeps its very value.
|
|
2196
|
+
*/
|
|
2197
|
+
function applyAnnotationPatch(current, patch, options = {}) {
|
|
2198
|
+
return applyResolvedPatch(current, resolveAnnotationPatch(current, patch, options));
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* The draft the engine writes for `draft`, as a viewer predicts it: every
|
|
2202
|
+
* field that follows from it stated. A box kind placed by its `rect` states
|
|
2203
|
+
* its `box`, a note's standard review state brings its state model, a free
|
|
2204
|
+
* text's rich text and contents follow each other, and a measurement's label
|
|
2205
|
+
* follows its points and scale. Fields left out stay out: the kind's
|
|
2206
|
+
* defaults (`ANNOTATION_DEFAULTS`) say what they read back. Values `draft`
|
|
2207
|
+
* gives come back as given.
|
|
2208
|
+
*
|
|
2209
|
+
* Throws `InvalidArg` for what the engine refuses without its schemas: a
|
|
2210
|
+
* field the kind doesn't declare, a box kind it can't place, and where
|
|
2211
|
+
* nothing can follow. The engine checks the values against the kind's schema
|
|
2212
|
+
* when the create reaches it, and refuses there what this lets through.
|
|
2213
|
+
*/
|
|
2214
|
+
function resolveAnnotationDraft(draft, options = {}) {
|
|
2215
|
+
const back = pageAnnotationDraftOf(pdfPredictAnnotationDraft(pdfAnnotationDraftOf(draft, MIRROR, mirrorEveryPage), options), MIRROR, mirrorEveryPage);
|
|
2216
|
+
const given = draft;
|
|
2217
|
+
for (const name of Object.keys(back)) if (given[name] !== void 0 && semanticEqual(back[name], given[name])) back[name] = given[name];
|
|
2218
|
+
return back;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* The annotation the engine will read back after creating `draft`: what a
|
|
2222
|
+
* viewer shows while the create is on its way. `context` says what the draft
|
|
2223
|
+
* doesn't: the ref and place it will have, who creates it, and, for a kind
|
|
2224
|
+
* whose `rect` the engine works out from its drawing, that box. The draft is
|
|
2225
|
+
* resolved as `resolveAnnotationDraft` does it, without the engine's schema
|
|
2226
|
+
* check: a create the engine refuses leaves the view when it answers.
|
|
2227
|
+
*/
|
|
2228
|
+
function annotationOfDraft(draft, context) {
|
|
2229
|
+
return annotationOfResolvedDraft(resolveAnnotationDraft(draft, { describeFont: context.describeFont }), context);
|
|
2230
|
+
}
|
|
2231
|
+
//#endregion
|
|
2232
|
+
//#region src/search/fold.ts
|
|
2233
|
+
/**
|
|
2234
|
+
* Text folding for literal search: the deterministic normalization applied
|
|
2235
|
+
* to both the page text and the needle so that "Café" finds "cafe" and a
|
|
2236
|
+
* line-wrapped "hello\n world" finds "hello world".
|
|
2237
|
+
*
|
|
2238
|
+
* Fold version 1:
|
|
2239
|
+
* - whitespace runs collapse to a single space (any `\s`, including the
|
|
2240
|
+
* spaces some compatibility decompositions emit) — or are dropped
|
|
2241
|
+
* entirely with `dropWhitespace` (ignoreWhitespace),
|
|
2242
|
+
* - each code point is NFKD-decomposed (ligatures split: "fi" → "fi",
|
|
2243
|
+
* "²" → "2"),
|
|
2244
|
+
* - combining marks are stripped unless `keepMarks`,
|
|
2245
|
+
* - case is folded via upper→lower round-trip unless `keepCase` (this
|
|
2246
|
+
* poor-man's full fold catches "ß"→"ss" and "ς"→"σ", which a plain
|
|
2247
|
+
* `toLowerCase` misses).
|
|
2248
|
+
*
|
|
2249
|
+
* Every folded code unit remembers which original code point produced it
|
|
2250
|
+
* (`map`), so match ranges found in folded space translate back to exact
|
|
2251
|
+
* original-text ranges — the property the whole anchor stage rests on.
|
|
2252
|
+
*
|
|
2253
|
+
* Pre-folded corpus artifacts store this fold's output; bump
|
|
2254
|
+
* `SEARCH_FOLD_VERSION` on any semantic change so stored corpora are
|
|
2255
|
+
* rebuilt instead of silently mismatching fresh needles.
|
|
2256
|
+
*/
|
|
2257
|
+
/** Version stamp for persisted pre-folded corpus artifacts. */
|
|
2258
|
+
const SEARCH_FOLD_VERSION = 1;
|
|
2259
|
+
const WHITESPACE = /\s/;
|
|
2260
|
+
const MARKS = /\p{M}/gu;
|
|
2261
|
+
function foldText(original, options = {}) {
|
|
2262
|
+
const units = [];
|
|
2263
|
+
const map = [];
|
|
2264
|
+
let lastWasSpace = false;
|
|
2265
|
+
let index = 0;
|
|
2266
|
+
for (const cp of original) {
|
|
2267
|
+
let piece;
|
|
2268
|
+
if (WHITESPACE.test(cp)) piece = " ";
|
|
2269
|
+
else {
|
|
2270
|
+
piece = cp.normalize("NFKD");
|
|
2271
|
+
if (!options.keepMarks) piece = piece.replace(MARKS, "");
|
|
2272
|
+
if (!options.keepCase) piece = piece.toUpperCase().toLowerCase();
|
|
2273
|
+
}
|
|
2274
|
+
for (let u = 0; u < piece.length; u++) {
|
|
2275
|
+
const unit = piece[u];
|
|
2276
|
+
if (WHITESPACE.test(unit)) {
|
|
2277
|
+
if (options.dropWhitespace || lastWasSpace) continue;
|
|
2278
|
+
units.push(" ");
|
|
2279
|
+
map.push(index);
|
|
2280
|
+
lastWasSpace = true;
|
|
2281
|
+
} else {
|
|
2282
|
+
units.push(unit);
|
|
2283
|
+
map.push(index);
|
|
2284
|
+
lastWasSpace = false;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
index += cp.length;
|
|
2288
|
+
}
|
|
2289
|
+
return {
|
|
2290
|
+
folded: units.join(""),
|
|
2291
|
+
map: Uint32Array.from(map),
|
|
2292
|
+
original
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
/** Code-unit length of the code point starting at `index` (1 or 2). */
|
|
2296
|
+
function codePointLengthAt(text, index) {
|
|
2297
|
+
const unit = text.charCodeAt(index);
|
|
2298
|
+
if (unit >= 55296 && unit <= 56319 && index + 1 < text.length) {
|
|
2299
|
+
const next = text.charCodeAt(index + 1);
|
|
2300
|
+
if (next >= 56320 && next <= 57343) return 2;
|
|
2301
|
+
}
|
|
2302
|
+
return 1;
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Translate a match found in folded space back to a range of the original
|
|
2306
|
+
* text (code units).
|
|
2307
|
+
* The range covers whole original code points: a hit on either folded
|
|
2308
|
+
* half of "fi" spans the full ligature, and a hit ending on a collapsed
|
|
2309
|
+
* space extends only through the first whitespace char of the run (the
|
|
2310
|
+
* remainder is presentation, not match).
|
|
2311
|
+
*/
|
|
2312
|
+
function toOriginalRange(folded, foldedStart, foldedLength) {
|
|
2313
|
+
const start = folded.map[foldedStart];
|
|
2314
|
+
const lastOriginal = folded.map[foldedStart + foldedLength - 1];
|
|
2315
|
+
return {
|
|
2316
|
+
start,
|
|
2317
|
+
count: lastOriginal + codePointLengthAt(folded.original, lastOriginal) - start
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
//#endregion
|
|
2321
|
+
//#region src/measure/units.ts
|
|
2322
|
+
const METRES = {
|
|
2323
|
+
pt: .0254 / 72,
|
|
2324
|
+
mm: .001,
|
|
2325
|
+
cm: .01,
|
|
2326
|
+
m: 1,
|
|
2327
|
+
km: 1e3,
|
|
2328
|
+
in: .0254,
|
|
2329
|
+
ft: .3048,
|
|
2330
|
+
yd: .9144,
|
|
2331
|
+
mi: 1609.344
|
|
2332
|
+
};
|
|
2333
|
+
const squareOf = (unit) => `${unit}2`;
|
|
2334
|
+
function squareMetres(unit) {
|
|
2335
|
+
return unit === "ha" ? 1e4 : unit === "acre" ? 4046.8564224 : METRES[unit.slice(0, -1)] ** 2;
|
|
2336
|
+
}
|
|
2337
|
+
const areaUnitLabel = (unit) => unit.endsWith("2") ? unit.slice(0, -1) + "²" : unit;
|
|
2338
|
+
//#endregion
|
|
2339
|
+
//#region src/measure/calibrate.ts
|
|
2340
|
+
function invalid(message) {
|
|
2341
|
+
return new EngineError(EngineErrorCode.InvalidArg, message);
|
|
2342
|
+
}
|
|
2343
|
+
function positive(value) {
|
|
2344
|
+
if (!Number.isFinite(value) || value <= 0) throw invalid("Calibration requires finite positive lengths and scales");
|
|
2345
|
+
}
|
|
2346
|
+
function build(perPoint, unit, ratio, format) {
|
|
2347
|
+
positive(Math.fround(perPoint));
|
|
2348
|
+
const precision = format.precision ?? 100;
|
|
2349
|
+
if (!Number.isInteger(Math.log10(precision)) || precision < 1 || precision > 1e9) throw invalid("Invalid decimal precision");
|
|
2350
|
+
const areaUnit = format.areaUnit ?? squareOf(unit);
|
|
2351
|
+
const common = {
|
|
2352
|
+
precision,
|
|
2353
|
+
...format.fixed === void 0 ? {} : { fixed: format.fixed }
|
|
2354
|
+
};
|
|
2355
|
+
const distance = format.imperialFeetInches ? [{
|
|
2356
|
+
unit: "ft",
|
|
2357
|
+
conversion: Math.fround(METRES[unit] / METRES.ft)
|
|
2358
|
+
}, {
|
|
2359
|
+
unit: "in",
|
|
2360
|
+
conversion: 12,
|
|
2361
|
+
fraction: "fraction",
|
|
2362
|
+
precision: format.fractionalPrecision ?? 16,
|
|
2363
|
+
..."fixed" in common ? { fixed: common.fixed } : {}
|
|
2364
|
+
}] : [{
|
|
2365
|
+
unit,
|
|
2366
|
+
conversion: 1,
|
|
2367
|
+
...common
|
|
2368
|
+
}];
|
|
2369
|
+
if (format.imperialFeetInches && (!Number.isInteger(distance[1].precision) || distance[1].precision < 1 || distance[1].precision > 2147483647)) throw invalid("Invalid fraction denominator");
|
|
2370
|
+
const measure = {
|
|
2371
|
+
subtype: "rectilinear",
|
|
2372
|
+
ratio,
|
|
2373
|
+
x: [{
|
|
2374
|
+
unit,
|
|
2375
|
+
conversion: Math.fround(perPoint),
|
|
2376
|
+
...common
|
|
2377
|
+
}],
|
|
2378
|
+
distance,
|
|
2379
|
+
area: [{
|
|
2380
|
+
unit: areaUnitLabel(areaUnit),
|
|
2381
|
+
conversion: Math.fround(METRES[unit] ** 2 / squareMetres(areaUnit)),
|
|
2382
|
+
...common
|
|
2383
|
+
}]
|
|
2384
|
+
};
|
|
2385
|
+
assertWritableMeasure(measure);
|
|
2386
|
+
return measure;
|
|
2387
|
+
}
|
|
2388
|
+
function measureFromKnownLength(userSpaceLength, real, format = {}) {
|
|
2389
|
+
positive(userSpaceLength);
|
|
2390
|
+
positive(real.value);
|
|
2391
|
+
const perPoint = real.value / userSpaceLength;
|
|
2392
|
+
return build(perPoint, real.unit, `1 in = ${Number((perPoint * 72).toPrecision(8))} ${real.unit}`, format);
|
|
2393
|
+
}
|
|
2394
|
+
function measureFromRatio(paper, real, unit, options = {}) {
|
|
2395
|
+
const { userUnit = 1, ...format } = options;
|
|
2396
|
+
positive(paper);
|
|
2397
|
+
positive(real);
|
|
2398
|
+
positive(userUnit);
|
|
2399
|
+
return build(userUnit / 72 * .0254 * (real / paper) / METRES[unit], unit, `${paper}:${real}`, format);
|
|
2400
|
+
}
|
|
2401
|
+
//#endregion
|
|
2402
|
+
export { pdfAppearanceTurnOf as $, createPdfTextLayout as A, renderTargetArea as At, mapActionNode as B, resolveAnnotationPatch as C, pageQuadOf as Ct, pageGeometryOf as D, pageTransform as Dt, createTextLayout as E, pdfRectOf as Et, pdfAnnotationOf as F, mapLinkTarget as G, mapAnnotationActions as H, pdfAnnotationPatchOf as I, pageDestinationOf as J, mapPageActions as K, pageMeasureOf as L, pageAnnotationOf as M, renderSize as Mt, pageAnnotationPatchOf as N, pageSearchSliceOf as O, renderAreaTransform as Ot, pdfAnnotationDraftOf as P, applyResolvedPatch as Q, pageViewportsOf as R, resolveAnnotationDraft as S, pagePointOf as St, shapeForRect as T, pdfQuadOf as Tt, mapDocumentActions as U, mapActionTree as V, mapFieldActions as W, ANNOTATION_DEFAULTS as X, pdfDestinationOf as Y, annotationDefaultsOf as Z, pagePointUnturned as _, isAbortError as _t, squareMetres as a, DEFAULT_BUNDLE_LIMITS as at, pageTurnOfDrawn as b, isRotatedGeometryRun as bt, foldText as c, ANNOTATION_RESOURCE_ROLE_NAMES as ct, appearanceTurnOf as d, hasAnnotationResources as dt, toAttachmentRef as et, applyAnnotationPatch as f, resolveAnnotationResources as ft, pagePointTurned as g, AbortError as gt, pageGlyphLooseQuad as h, resolveBinarySource as ht, areaUnitLabel as i, bundleCodec as it, pageAnnotationDraftOf as j, renderTransform as jt, pageTextSegmentOf as k, renderMatrix as kt, toOriginalRange as l, annotationResourceBuffers as lt, pageGlyphLooseBounds as m, sniffBinaryMetadata as mt, measureFromRatio as n, assertAnnotationBundle as nt, squareOf as o, assertWithinLimit as ot, drawnPointsOf as p, withFileFromResource as pt, mapTriggerActions as q, METRES as r, assertAnnotationBundleManifest as rt, SEARCH_FOLD_VERSION as s, manifestBytesOf as st, measureFromKnownLength as t, AnnotationTransfer as tt, annotationOfDraft as u, assertAnnotationResources as ut, pagePointsBounds as v, glyphLooseBounds as vt, resolveRectCommand as w, pdfPointOf as wt, pageTurnOfUpright as x, pageBoxOf as xt, pageQuadBounds as y, glyphLooseQuad as yt, pdfMeasureOf as z };
|
|
2403
|
+
|
|
2404
|
+
//# sourceMappingURL=calibrate-7T3fnP8-.js.map
|