web-doc 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/THIRD_PARTY_NOTICES.md +26 -1
  2. package/dist/adapters/docx-images.d.ts +13 -4
  3. package/dist/adapters/docx-images.js +3 -0
  4. package/dist/adapters/docx-paragraphs.d.ts +65 -0
  5. package/dist/adapters/docx-paragraphs.js +165 -0
  6. package/dist/adapters/docx-prepass.d.ts +17 -0
  7. package/dist/adapters/docx-prepass.js +230 -0
  8. package/dist/adapters/office.d.ts +31 -5
  9. package/dist/adapters/office.js +60 -29
  10. package/dist/adapters/pdf.d.ts +9 -0
  11. package/dist/adapters/pdf.js +10 -0
  12. package/dist/assets/pdfium/pdfium.wasm +0 -0
  13. package/dist/contracts.d.ts +47 -2
  14. package/dist/edit/assets.d.ts +23 -0
  15. package/dist/edit/assets.js +75 -0
  16. package/dist/edit/docx/elements.d.ts +6 -0
  17. package/dist/edit/docx/elements.js +90 -0
  18. package/dist/edit/docx/engine.d.ts +51 -0
  19. package/dist/edit/docx/engine.js +473 -0
  20. package/dist/edit/docx/handlers.d.ts +3 -0
  21. package/dist/edit/docx/handlers.js +15 -0
  22. package/dist/edit/docx/ids.d.ts +45 -0
  23. package/dist/edit/docx/ids.js +101 -0
  24. package/dist/edit/docx/model.d.ts +92 -0
  25. package/dist/edit/docx/model.js +339 -0
  26. package/dist/edit/docx/operations.d.ts +37 -0
  27. package/dist/edit/docx/operations.js +3 -0
  28. package/dist/edit/docx/provider.d.ts +13 -0
  29. package/dist/edit/docx/provider.js +34 -0
  30. package/dist/edit/docx/schemas.d.ts +6 -0
  31. package/dist/edit/docx/schemas.js +192 -0
  32. package/dist/edit/docx/session.d.ts +37 -0
  33. package/dist/edit/docx/session.js +426 -0
  34. package/dist/edit/docx/structure-ops.d.ts +6 -0
  35. package/dist/edit/docx/structure-ops.js +494 -0
  36. package/dist/edit/docx/style.d.ts +45 -0
  37. package/dist/edit/docx/style.js +375 -0
  38. package/dist/edit/docx/table-ops.d.ts +4 -0
  39. package/dist/edit/docx/table-ops.js +219 -0
  40. package/dist/edit/docx/text-ops.d.ts +18 -0
  41. package/dist/edit/docx/text-ops.js +458 -0
  42. package/dist/edit/docx/text.d.ts +34 -0
  43. package/dist/edit/docx/text.js +263 -0
  44. package/dist/edit/docx/types.d.ts +191 -0
  45. package/dist/edit/docx/types.js +1 -0
  46. package/dist/edit/docx/write.d.ts +50 -0
  47. package/dist/edit/docx/write.js +352 -0
  48. package/dist/edit/engine.d.ts +117 -0
  49. package/dist/edit/engine.js +1 -0
  50. package/dist/edit/history.d.ts +53 -0
  51. package/dist/edit/history.js +101 -0
  52. package/dist/edit/ooxml/names.d.ts +20 -0
  53. package/dist/edit/ooxml/names.js +61 -0
  54. package/dist/edit/ooxml/opc.d.ts +50 -0
  55. package/dist/edit/ooxml/opc.js +150 -0
  56. package/dist/edit/ooxml/package.d.ts +82 -0
  57. package/dist/edit/ooxml/package.js +233 -0
  58. package/dist/edit/ooxml/patch.d.ts +51 -0
  59. package/dist/edit/ooxml/patch.js +250 -0
  60. package/dist/edit/ooxml/transaction.d.ts +39 -0
  61. package/dist/edit/ooxml/transaction.js +316 -0
  62. package/dist/edit/ooxml/worker.d.ts +8 -0
  63. package/dist/edit/ooxml/worker.js +12 -0
  64. package/dist/edit/ooxml/writer.d.ts +21 -0
  65. package/dist/edit/ooxml/writer.js +187 -0
  66. package/dist/edit/ooxml/xml.d.ts +74 -0
  67. package/dist/edit/ooxml/xml.js +451 -0
  68. package/dist/edit/ooxml/zip.d.ts +54 -0
  69. package/dist/edit/ooxml/zip.js +280 -0
  70. package/dist/edit/operations.d.ts +19 -0
  71. package/dist/edit/operations.js +137 -0
  72. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  73. package/dist/edit/pdf/engine/compact.js +442 -0
  74. package/dist/edit/pdf/engine/document.d.ts +95 -0
  75. package/dist/edit/pdf/engine/document.js +868 -0
  76. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  77. package/dist/edit/pdf/engine/elements.js +313 -0
  78. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  79. package/dist/edit/pdf/engine/existing-text.js +424 -0
  80. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  81. package/dist/edit/pdf/engine/fonts.js +466 -0
  82. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  83. package/dist/edit/pdf/engine/geometry.js +97 -0
  84. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  85. package/dist/edit/pdf/engine/handler.js +108 -0
  86. package/dist/edit/pdf/engine/images.d.ts +33 -0
  87. package/dist/edit/pdf/engine/images.js +188 -0
  88. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  89. package/dist/edit/pdf/engine/layout.js +177 -0
  90. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  91. package/dist/edit/pdf/engine/operations.js +3 -0
  92. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  93. package/dist/edit/pdf/engine/pages.js +98 -0
  94. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  95. package/dist/edit/pdf/engine/pdfium.js +249 -0
  96. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  97. package/dist/edit/pdf/engine/shapes.js +183 -0
  98. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  99. package/dist/edit/pdf/engine/tables.js +315 -0
  100. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  101. package/dist/edit/pdf/engine/text-box.js +317 -0
  102. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  103. package/dist/edit/pdf/engine/text-layout.js +67 -0
  104. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  105. package/dist/edit/pdf/engine/transform.js +137 -0
  106. package/dist/edit/pdf/provider.d.ts +35 -0
  107. package/dist/edit/pdf/provider.js +92 -0
  108. package/dist/edit/pdf/range-map.d.ts +14 -0
  109. package/dist/edit/pdf/range-map.js +107 -0
  110. package/dist/edit/pdf/schemas.d.ts +11 -0
  111. package/dist/edit/pdf/schemas.js +291 -0
  112. package/dist/edit/pdf/selection.d.ts +5 -0
  113. package/dist/edit/pdf/selection.js +152 -0
  114. package/dist/edit/pdf/session.d.ts +53 -0
  115. package/dist/edit/pdf/session.js +273 -0
  116. package/dist/edit/pdf/types.d.ts +336 -0
  117. package/dist/edit/pdf/types.js +1 -0
  118. package/dist/edit/pptx/elements.d.ts +74 -0
  119. package/dist/edit/pptx/elements.js +301 -0
  120. package/dist/edit/pptx/engine.d.ts +37 -0
  121. package/dist/edit/pptx/engine.js +466 -0
  122. package/dist/edit/pptx/geometry.d.ts +72 -0
  123. package/dist/edit/pptx/geometry.js +193 -0
  124. package/dist/edit/pptx/handler.d.ts +7 -0
  125. package/dist/edit/pptx/handler.js +91 -0
  126. package/dist/edit/pptx/handlers.d.ts +3 -0
  127. package/dist/edit/pptx/handlers.js +21 -0
  128. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  129. package/dist/edit/pptx/image-table-ops.js +233 -0
  130. package/dist/edit/pptx/model.d.ts +69 -0
  131. package/dist/edit/pptx/model.js +170 -0
  132. package/dist/edit/pptx/operations.d.ts +50 -0
  133. package/dist/edit/pptx/operations.js +11 -0
  134. package/dist/edit/pptx/provider.d.ts +17 -0
  135. package/dist/edit/pptx/provider.js +40 -0
  136. package/dist/edit/pptx/schemas.d.ts +6 -0
  137. package/dist/edit/pptx/schemas.js +220 -0
  138. package/dist/edit/pptx/session.d.ts +44 -0
  139. package/dist/edit/pptx/session.js +114 -0
  140. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  141. package/dist/edit/pptx/shape-ops.js +486 -0
  142. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  143. package/dist/edit/pptx/slide-ops.js +353 -0
  144. package/dist/edit/pptx/style.d.ts +31 -0
  145. package/dist/edit/pptx/style.js +181 -0
  146. package/dist/edit/pptx/text-ops.d.ts +13 -0
  147. package/dist/edit/pptx/text-ops.js +267 -0
  148. package/dist/edit/pptx/text-write.d.ts +67 -0
  149. package/dist/edit/pptx/text-write.js +293 -0
  150. package/dist/edit/pptx/text.d.ts +41 -0
  151. package/dist/edit/pptx/text.js +99 -0
  152. package/dist/edit/pptx/types.d.ts +232 -0
  153. package/dist/edit/pptx/types.js +1 -0
  154. package/dist/edit/schema.d.ts +11 -0
  155. package/dist/edit/schema.js +275 -0
  156. package/dist/edit/session.d.ts +66 -0
  157. package/dist/edit/session.js +701 -0
  158. package/dist/edit/sessions.d.ts +8 -0
  159. package/dist/edit/sessions.js +1 -0
  160. package/dist/edit/types.d.ts +276 -0
  161. package/dist/edit/types.js +1 -0
  162. package/dist/edit/worker-engine.d.ts +31 -0
  163. package/dist/edit/worker-engine.js +91 -0
  164. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  165. package/dist/fonts/manifest.json +6 -1
  166. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  167. package/dist/fonts.d.ts +2 -0
  168. package/dist/fonts.js +4 -0
  169. package/dist/headless.d.ts +4 -0
  170. package/dist/headless.js +4 -0
  171. package/dist/index.d.ts +5 -0
  172. package/dist/index.js +5 -0
  173. package/dist/limits.js +3 -0
  174. package/dist/ooxml-edit-worker.d.ts +1 -0
  175. package/dist/ooxml-edit-worker.js +8 -0
  176. package/dist/pdf-edit-worker.d.ts +1 -0
  177. package/dist/pdf-edit-worker.js +35 -0
  178. package/dist/spreadsheet-viewport.d.ts +5 -0
  179. package/dist/spreadsheet-viewport.js +11 -0
  180. package/dist/ui.js +7 -0
  181. package/dist/viewer.d.ts +6 -0
  182. package/dist/viewer.js +293 -19
  183. package/dist/viewport.d.ts +13 -0
  184. package/dist/viewport.js +126 -1
  185. package/dist/worker-protocol.d.ts +39 -2
  186. package/dist/workers/ooxml-edit-worker.js +8671 -0
  187. package/dist/workers/pdf-edit-worker.js +10844 -0
  188. package/package.json +3 -3
@@ -0,0 +1,317 @@
1
+ import { MARK_NAME, MARK_PARAM, OBJECT_TEXT, } from "./elements.js";
2
+ import { displayedSize, pageToUser } from "./geometry.js";
3
+ import { layoutText } from "./text-layout.js";
4
+ import { tableSpecOf, tableText } from "./tables.js";
5
+ export const insertTextBox = {
6
+ validate(operation, context, issue) {
7
+ if (operation.pageIndex >= context.pageCount) {
8
+ issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
9
+ return;
10
+ }
11
+ validateRect(operation.rect, context.geometry(operation.pageIndex), issue);
12
+ validateFont(fontRequest(resolveStyle(operation.style ?? {}), operation.text), context, issue);
13
+ },
14
+ apply(operation, context) {
15
+ const id = context.newId(operation.pageIndex);
16
+ const spec = {
17
+ kind: "textBox",
18
+ id,
19
+ rect: operation.rect,
20
+ text: operation.text,
21
+ style: resolveStyle(operation.style ?? {}),
22
+ };
23
+ const drawn = drawTextBox(context, operation.pageIndex, spec);
24
+ return {
25
+ createdIds: [id],
26
+ changedPages: [operation.pageIndex],
27
+ warnings: warningsFor(drawn, id),
28
+ };
29
+ },
30
+ };
31
+ /** `replaceText` for a text box: a rebuild from its inputs with new text. */
32
+ export const textBoxReplaceText = {
33
+ validate(operation, context, issue) {
34
+ const target = textBoxTarget(operation.target, context, issue);
35
+ if (!target)
36
+ return;
37
+ validateFont(fontRequest(target.spec.style, operation.text), context, issue);
38
+ },
39
+ apply(operation, context) {
40
+ const { location, spec } = textBoxTarget(operation.target, context);
41
+ return changed(location, rebuildTextBox(context, location, { ...spec, text: operation.text }));
42
+ },
43
+ };
44
+ /** `setTextStyle` for a text box: a rebuild with the merged style. */
45
+ export const textBoxSetTextStyle = {
46
+ validate(operation, context, issue) {
47
+ const target = textBoxTarget(operation.target, context, issue);
48
+ if (!target)
49
+ return;
50
+ const style = { ...target.spec.style, ...definedFields(operation.style) };
51
+ validateFont(fontRequest(style, target.spec.text), context, issue);
52
+ },
53
+ apply(operation, context) {
54
+ const { location, spec } = textBoxTarget(operation.target, context);
55
+ return changed(location, rebuildTextBox(context, location, {
56
+ ...spec,
57
+ style: { ...spec.style, ...definedFields(operation.style) },
58
+ }));
59
+ },
60
+ };
61
+ /**
62
+ * Lays the box out and inserts one text object per line, appended to the
63
+ * page or at `insertAt` in drawing order.
64
+ */
65
+ export function drawTextBox(context, pageIndex, spec, insertAt) {
66
+ const { pdfium, measurer } = context;
67
+ const { lib } = pdfium;
68
+ const font = context.fonts.resolve(pdfium, context.document, fontRequest(spec.style, spec.text));
69
+ const { ascent } = measurer.metrics(font.handle, spec.style.fontSize);
70
+ const layout = layoutText({
71
+ text: spec.text,
72
+ width: spec.rect.width,
73
+ height: spec.rect.height,
74
+ fontSize: spec.style.fontSize,
75
+ lineHeight: spec.style.lineHeight,
76
+ align: spec.style.align,
77
+ ascent,
78
+ advance: (text) => measurer.advance(font.handle, spec.style.fontSize, text),
79
+ });
80
+ const geometry = context.geometry(pageIndex);
81
+ const [r, g, b] = parseColor(spec.style.color);
82
+ const params = JSON.stringify(spec);
83
+ context.withPage(pageIndex, (page) => {
84
+ const records = [];
85
+ let index = insertAt ?? -1;
86
+ for (const line of layout.lines) {
87
+ const object = lib.FPDFPageObj_CreateTextObj(context.document, font.handle, spec.style.fontSize);
88
+ setText(pdfium, object, line.text);
89
+ lib.FPDFPageObj_SetFillColor(object, r, g, b, 255);
90
+ placeUpright(pdfium, object, geometry, spec.rect.x + line.x, spec.rect.y + line.baseline);
91
+ const mark = lib.FPDFPageObj_AddMark(object, MARK_NAME);
92
+ lib.FPDFPageObjMark_SetStringParam(context.document, object, mark, MARK_PARAM, params);
93
+ if (insertAt === undefined)
94
+ lib.FPDFPage_InsertObject(page, object);
95
+ else
96
+ lib.FPDFPage_InsertObjectAtIndex(page, object, index++);
97
+ records.push({ id: spec.id, type: OBJECT_TEXT, mark: spec });
98
+ }
99
+ if (insertAt === undefined)
100
+ context.appendObjects(pageIndex, records);
101
+ else
102
+ context.spliceObjects(pageIndex, insertAt, 0, records);
103
+ });
104
+ return {
105
+ overflow: layout.overflow,
106
+ ...(font.substitution ? { substitution: font.substitution } : {}),
107
+ };
108
+ }
109
+ /**
110
+ * Replaces a text box's objects with ones drawn from `spec`, at the same
111
+ * place in the drawing order so nothing else changes its stacking.
112
+ */
113
+ export function rebuildTextBox(context, location, spec) {
114
+ const first = removeObjects(context, location);
115
+ return drawTextBox(context, location.pageIndex, spec, first);
116
+ }
117
+ /** Removes an element's objects from the page; returns where they started. */
118
+ export function removeObjects(context, location) {
119
+ const { lib } = context.pdfium;
120
+ const first = location.indexes[0];
121
+ context.withPage(location.pageIndex, (page) => {
122
+ // Highest first, so earlier indexes stay valid while removing.
123
+ for (const index of [...location.indexes].reverse()) {
124
+ const object = lib.FPDFPage_GetObject(page, index);
125
+ lib.FPDFPage_RemoveObject(page, object);
126
+ lib.FPDFPageObj_Destroy(object);
127
+ }
128
+ });
129
+ context.spliceObjects(location.pageIndex, first, location.indexes.length, []);
130
+ return first;
131
+ }
132
+ /** The location and stored inputs of a text box, or the issue that stops the edit. */
133
+ export function textBoxTarget(target, context, issue = () => { }) {
134
+ const location = context.locate(target);
135
+ if (!location) {
136
+ issue("/target", "unknown-target", `No element ${target}`);
137
+ return undefined;
138
+ }
139
+ if (location.record.mark?.kind !== "textBox") {
140
+ issue("/target", "unsupported-target", "Only text boxes created by web-doc can be edited yet");
141
+ return undefined;
142
+ }
143
+ return { location, spec: location.record.mark };
144
+ }
145
+ export function changed(location, drawn) {
146
+ return {
147
+ createdIds: [],
148
+ changedPages: [location.pageIndex],
149
+ warnings: warningsFor(drawn, location.record.id),
150
+ };
151
+ }
152
+ function warningsFor(drawn, elementId) {
153
+ return [
154
+ ...(drawn.overflow
155
+ ? [
156
+ {
157
+ code: "fidelity-degraded",
158
+ message: "The text does not fit the box's height and runs past it",
159
+ details: { elementId },
160
+ },
161
+ ]
162
+ : []),
163
+ ...(drawn.substitution
164
+ ? [
165
+ {
166
+ code: "font-substitution",
167
+ message: drawn.substitution,
168
+ details: { elementId },
169
+ },
170
+ ]
171
+ : []),
172
+ ];
173
+ }
174
+ /**
175
+ * The fonts a batch will need: what each operation draws, read from the
176
+ * operation itself, from the text box it rebuilds, or from the text object
177
+ * it may have to replace.
178
+ */
179
+ export function fontRequestsOf(operations, markOf, elementOf) {
180
+ const requests = [];
181
+ const spec = (id) => {
182
+ const mark = markOf(id);
183
+ return mark?.kind === "textBox"
184
+ ? mark
185
+ : undefined;
186
+ };
187
+ const table = (id) => tableSpecOf(markOf(id));
188
+ for (const raw of operations) {
189
+ const operation = raw;
190
+ switch (operation.op) {
191
+ case "insertTextBox":
192
+ requests.push({
193
+ family: operation.style?.fontFamily ?? "Helvetica",
194
+ text: operation.text,
195
+ });
196
+ break;
197
+ case "replaceText": {
198
+ const box = spec(operation.target);
199
+ if (box) {
200
+ requests.push({ family: box.style.fontFamily, text: operation.text });
201
+ break;
202
+ }
203
+ const element = elementOf(operation.target);
204
+ if (element?.kind === "text")
205
+ requests.push({
206
+ family: element.textStyle?.fontFamily ?? "Helvetica",
207
+ text: operation.text,
208
+ });
209
+ break;
210
+ }
211
+ case "setTextStyle": {
212
+ const box = spec(operation.target);
213
+ if (box)
214
+ requests.push({
215
+ family: operation.style.fontFamily ?? box.style.fontFamily,
216
+ text: box.text,
217
+ });
218
+ break;
219
+ }
220
+ case "resizeElement":
221
+ case "moveElement": {
222
+ const box = spec(operation.target);
223
+ if (box)
224
+ requests.push({ family: box.style.fontFamily, text: box.text });
225
+ const grid = table(operation.target);
226
+ if (grid)
227
+ requests.push({
228
+ family: grid.style.fontFamily,
229
+ text: tableText(grid.rows),
230
+ });
231
+ break;
232
+ }
233
+ case "insertTable":
234
+ requests.push({
235
+ family: operation.style?.fontFamily ?? "Helvetica",
236
+ text: tableText(operation.rows),
237
+ });
238
+ break;
239
+ case "setTableCell": {
240
+ const grid = table(operation.target);
241
+ if (grid)
242
+ requests.push({
243
+ family: grid.style.fontFamily,
244
+ text: `${tableText(grid.rows)}\n${operation.text}`,
245
+ });
246
+ break;
247
+ }
248
+ default:
249
+ break;
250
+ }
251
+ }
252
+ return requests;
253
+ }
254
+ export function fontRequest(style, text) {
255
+ return {
256
+ family: style.fontFamily,
257
+ bold: style.bold,
258
+ italic: style.italic,
259
+ text,
260
+ };
261
+ }
262
+ export function validateFont(request, context, issue) {
263
+ const problem = context.fonts.problem(request);
264
+ if (problem)
265
+ issue(problem.path, problem.code, problem.message);
266
+ }
267
+ /**
268
+ * Positions an object so it reads upright on the displayed page: the object's
269
+ * origin goes to the page-space point and it turns against the page's /Rotate.
270
+ */
271
+ export function placeUpright(pdfium, object, geometry, pageX, pageY) {
272
+ const [a, b, c, d] = ROTATIONS[geometry.rotation % 4];
273
+ const origin = pageToUser(geometry, pageX, pageY);
274
+ pdfium.lib.FPDFPageObj_Transform(object, a, b, c, d, origin.x, origin.y);
275
+ }
276
+ /** Counter-clockwise quarter turns in user space, which cancel the page's clockwise /Rotate. */
277
+ const ROTATIONS = [
278
+ [1, 0, 0, 1],
279
+ [0, 1, -1, 0],
280
+ [-1, 0, 0, -1],
281
+ [0, -1, 1, 0],
282
+ ];
283
+ export function setText(pdfium, object, text) {
284
+ const wide = pdfium.writeWideString(text);
285
+ try {
286
+ pdfium.lib.FPDFText_SetText(object, wide);
287
+ }
288
+ finally {
289
+ pdfium.free(wide);
290
+ }
291
+ }
292
+ export function parseColor(color) {
293
+ return [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16));
294
+ }
295
+ export function resolveStyle(style) {
296
+ return {
297
+ fontFamily: style.fontFamily ?? "Helvetica",
298
+ fontSize: style.fontSize ?? 12,
299
+ bold: style.bold ?? false,
300
+ italic: style.italic ?? false,
301
+ color: style.color ?? "#000000",
302
+ align: style.align ?? "left",
303
+ lineHeight: style.lineHeight ?? 1.2,
304
+ };
305
+ }
306
+ function definedFields(style) {
307
+ return Object.fromEntries(Object.entries(style).filter(([, value]) => value !== undefined));
308
+ }
309
+ export function validateRect(rect, geometry, issue, path = "/rect") {
310
+ const size = displayedSize(geometry);
311
+ const tolerance = 0.01;
312
+ if (rect.x < -tolerance ||
313
+ rect.y < -tolerance ||
314
+ rect.x + rect.width > size.width + tolerance ||
315
+ rect.y + rect.height > size.height + tolerance)
316
+ issue(path, "range", `The rectangle must lie within the ${size.width}×${size.height} pt page`);
317
+ }
@@ -0,0 +1,28 @@
1
+ import type { PdfTextAlign } from "../types.js";
2
+ export interface LayoutInput {
3
+ readonly text: string;
4
+ readonly width: number;
5
+ readonly height: number;
6
+ readonly fontSize: number;
7
+ /** Multiple of the font size. */
8
+ readonly lineHeight: number;
9
+ readonly align: PdfTextAlign;
10
+ readonly ascent: number;
11
+ /** Width of a string in points at the font size. */
12
+ readonly advance: (text: string) => number;
13
+ }
14
+ export interface LayoutLine {
15
+ readonly text: string;
16
+ /** Offset of the line's left edge from the box's left edge. */
17
+ readonly x: number;
18
+ /** Offset of the baseline from the box's top edge. */
19
+ readonly baseline: number;
20
+ readonly width: number;
21
+ }
22
+ export interface Layout {
23
+ readonly lines: readonly LayoutLine[];
24
+ /** Height the lines need; may exceed the box. */
25
+ readonly height: number;
26
+ readonly overflow: boolean;
27
+ }
28
+ export declare function layoutText(input: LayoutInput): Layout;
@@ -0,0 +1,67 @@
1
+ export function layoutText(input) {
2
+ const step = input.fontSize * input.lineHeight;
3
+ const texts = [];
4
+ for (const paragraph of input.text.replaceAll("\r\n", "\n").split("\n"))
5
+ texts.push(...wrapParagraph(paragraph.replaceAll("\t", " "), input));
6
+ const lines = texts.map((text, index) => {
7
+ const width = input.advance(text);
8
+ return {
9
+ text,
10
+ x: offset(input.align, input.width, width),
11
+ baseline: index * step + input.ascent,
12
+ width,
13
+ };
14
+ });
15
+ const height = lines.length * step;
16
+ return { lines, height, overflow: height > input.height + 0.001 };
17
+ }
18
+ function wrapParagraph(paragraph, input) {
19
+ const lines = [];
20
+ let current = "";
21
+ const push = () => {
22
+ lines.push(current.trimEnd());
23
+ current = "";
24
+ };
25
+ // Runs of spaces travel with the line they are in; a word that does not
26
+ // fit moves to the next line, and one wider than the box is cut.
27
+ for (const token of paragraph.match(/ +|[^ ]+/g) ?? []) {
28
+ if (token.startsWith(" ")) {
29
+ current += token;
30
+ continue;
31
+ }
32
+ const candidate = current + token;
33
+ if (input.advance(candidate) <= input.width) {
34
+ current = candidate;
35
+ continue;
36
+ }
37
+ if (current.trim())
38
+ push();
39
+ else
40
+ current = "";
41
+ if (input.advance(token) <= input.width) {
42
+ current = token;
43
+ continue;
44
+ }
45
+ for (const character of token) {
46
+ const next = current + character;
47
+ if (current && input.advance(next) > input.width) {
48
+ push();
49
+ current = character;
50
+ }
51
+ else
52
+ current = next;
53
+ }
54
+ }
55
+ lines.push(current.trimEnd());
56
+ return lines;
57
+ }
58
+ function offset(align, boxWidth, lineWidth) {
59
+ switch (align) {
60
+ case "center":
61
+ return Math.max(0, (boxWidth - lineWidth) / 2);
62
+ case "right":
63
+ return Math.max(0, boxWidth - lineWidth);
64
+ default:
65
+ return 0;
66
+ }
67
+ }
@@ -0,0 +1,5 @@
1
+ import type { DeleteElementOperation, MoveElementOperation, ResizeElementOperation } from "../types.js";
2
+ import type { OperationHandler } from "./operations.js";
3
+ export declare const moveElement: OperationHandler<MoveElementOperation>;
4
+ export declare const resizeElement: OperationHandler<ResizeElementOperation>;
5
+ export declare const deleteElement: OperationHandler<DeleteElementOperation>;
@@ -0,0 +1,137 @@
1
+ import { pageToUser } from "./geometry.js";
2
+ import { changed, rebuildTextBox, removeObjects, validateRect, } from "./text-box.js";
3
+ import { changedTable, rebuildTable, tableSpecOf } from "./tables.js";
4
+ /*
5
+ * Geometry operations that apply to any element. Plain objects are moved and
6
+ * stretched through PDFium matrices in user space; web-doc's own composites
7
+ * are rebuilt from their stored inputs so the mark stays true.
8
+ */
9
+ export const moveElement = {
10
+ validate(operation, context, issue) {
11
+ const target = anyTarget(operation.target, context, issue);
12
+ if (!target)
13
+ return;
14
+ if ((operation.to === undefined) === (operation.by === undefined)) {
15
+ issue("", "one-of", "Give exactly one of `to` and `by`");
16
+ return;
17
+ }
18
+ const delta = moveDelta(operation, target.element.bounds);
19
+ validateRect(shifted(target.element.bounds, delta.dx, delta.dy), context.geometry(target.location.pageIndex), issue, operation.to ? "/to" : "/by");
20
+ },
21
+ apply(operation, context) {
22
+ const { location, element } = anyTarget(operation.target, context);
23
+ const { dx, dy } = moveDelta(operation, element.bounds);
24
+ const spec = textBoxSpec(location);
25
+ if (spec)
26
+ return changed(location, rebuildTextBox(context, location, {
27
+ ...spec,
28
+ rect: shifted(spec.rect, dx, dy),
29
+ }));
30
+ const table = tableSpecOf(location.record.mark);
31
+ if (table)
32
+ return changedTable(location, rebuildTable(context, location, {
33
+ ...table,
34
+ at: { x: table.at.x + dx, y: table.at.y + dy },
35
+ }));
36
+ const geometry = context.geometry(location.pageIndex);
37
+ const from = pageToUser(geometry, 0, 0);
38
+ const to = pageToUser(geometry, dx, dy);
39
+ transformObjects(context, location, [
40
+ 1,
41
+ 0,
42
+ 0,
43
+ 1,
44
+ to.x - from.x,
45
+ to.y - from.y,
46
+ ]);
47
+ return changed(location, { overflow: false });
48
+ },
49
+ };
50
+ export const resizeElement = {
51
+ validate(operation, context, issue) {
52
+ const target = anyTarget(operation.target, context, issue);
53
+ if (!target)
54
+ return;
55
+ if (target.element.kind === "table") {
56
+ issue("/target", "unsupported-target", "Tables cannot be resized");
57
+ return;
58
+ }
59
+ validateRect(operation.rect, context.geometry(target.location.pageIndex), issue);
60
+ },
61
+ apply(operation, context) {
62
+ const { location, element } = anyTarget(operation.target, context);
63
+ const spec = textBoxSpec(location);
64
+ if (spec)
65
+ return changed(location, rebuildTextBox(context, location, { ...spec, rect: operation.rect }));
66
+ const geometry = context.geometry(location.pageIndex);
67
+ const { bounds } = element;
68
+ // Scale about the top-left corner in page space, then move it into place.
69
+ const scaleX = operation.rect.width / bounds.width;
70
+ const scaleY = operation.rect.height / bounds.height;
71
+ const [userScaleX, userScaleY] = geometry.rotation % 2 === 0 ? [scaleX, scaleY] : [scaleY, scaleX];
72
+ const anchor = pageToUser(geometry, bounds.x, bounds.y);
73
+ transformObjects(context, location, [
74
+ userScaleX,
75
+ 0,
76
+ 0,
77
+ userScaleY,
78
+ anchor.x - userScaleX * anchor.x,
79
+ anchor.y - userScaleY * anchor.y,
80
+ ]);
81
+ const from = pageToUser(geometry, 0, 0);
82
+ const to = pageToUser(geometry, operation.rect.x - bounds.x, operation.rect.y - bounds.y);
83
+ transformObjects(context, location, [
84
+ 1,
85
+ 0,
86
+ 0,
87
+ 1,
88
+ to.x - from.x,
89
+ to.y - from.y,
90
+ ]);
91
+ return changed(location, { overflow: false });
92
+ },
93
+ };
94
+ export const deleteElement = {
95
+ validate(operation, context, issue) {
96
+ anyTarget(operation.target, context, issue);
97
+ },
98
+ apply(operation, context) {
99
+ const { location } = anyTarget(operation.target, context);
100
+ removeObjects(context, location);
101
+ return {
102
+ ...changed(location, { overflow: false }),
103
+ removedIds: [operation.target],
104
+ };
105
+ },
106
+ };
107
+ /** Any element, by id, with its location; reports a missing one. */
108
+ function anyTarget(target, context, issue = () => { }) {
109
+ const location = context.locate(target);
110
+ const element = location && context.element(target);
111
+ if (!location || !element) {
112
+ issue("/target", "unknown-target", `No element ${target}`);
113
+ return undefined;
114
+ }
115
+ return { location, element };
116
+ }
117
+ function textBoxSpec(location) {
118
+ return location.record.mark?.kind === "textBox"
119
+ ? location.record.mark
120
+ : undefined;
121
+ }
122
+ function moveDelta(operation, bounds) {
123
+ return operation.to
124
+ ? { dx: operation.to.x - bounds.x, dy: operation.to.y - bounds.y }
125
+ : { dx: operation.by.dx, dy: operation.by.dy };
126
+ }
127
+ function shifted(rect, dx, dy) {
128
+ return { ...rect, x: rect.x + dx, y: rect.y + dy };
129
+ }
130
+ /** Concatenates a user-space matrix onto every object of an element. */
131
+ function transformObjects(context, location, [a, b, c, d, e, f]) {
132
+ const { lib } = context.pdfium;
133
+ context.withPage(location.pageIndex, (page) => {
134
+ for (const index of location.indexes)
135
+ lib.FPDFPageObj_Transform(lib.FPDFPage_GetObject(page, index), a, b, c, d, e, f);
136
+ });
137
+ }
@@ -0,0 +1,35 @@
1
+ import { type WorkerLike } from "../../worker-client.js";
2
+ import type { EditEngineContext } from "../engine.js";
3
+ import type { PagePoint, PageRect, TextPosition, TextRange } from "../types.js";
4
+ import { WorkerEngineClient } from "../worker-engine.js";
5
+ import type { PageBitmap, PageLayout, TextLayout } from "./types.js";
6
+ /** The reads behind the overlay primitives, beyond the core engine interface. */
7
+ export interface PdfEngineReads {
8
+ textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
9
+ positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
10
+ rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
11
+ renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
12
+ pageLayout(pageIndex: number, signal: AbortSignal): Promise<PageLayout | undefined>;
13
+ }
14
+ export interface PdfEditProviderOptions {
15
+ readonly workerUrl?: string | URL;
16
+ readonly wasmUrl?: string | URL;
17
+ /** TrueType font for text the standard fonts cannot encode. */
18
+ readonly fallbackFontUrl?: string | URL;
19
+ /** Test hook: supplies the worker instead of the packaged script. */
20
+ readonly createWorker?: () => WorkerLike;
21
+ }
22
+ /**
23
+ * Starts the PDF edit worker for `original` and returns the engine the core
24
+ * drives. Loaded lazily by the PDF adapter's provider on the first `edit()`.
25
+ */
26
+ export declare function loadPdfEditEngine(original: Uint8Array, context: EditEngineContext, options?: PdfEditProviderOptions): Promise<PdfEditEngineClient>;
27
+ export declare class PdfEditEngineClient extends WorkerEngineClient implements PdfEngineReads {
28
+ readonly schemas: import("../types.js").OperationSchemaSet;
29
+ start(original: Uint8Array, options: PdfEditProviderOptions): Promise<void>;
30
+ textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
31
+ positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
32
+ rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
33
+ renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
34
+ pageLayout(pageIndex: number, signal: AbortSignal): Promise<PageLayout | undefined>;
35
+ }
@@ -0,0 +1,92 @@
1
+ import { ViewerError } from "../../errors.js";
2
+ import { WorkerRpcClient } from "../../worker-client.js";
3
+ import { packageRelativeUrl, resolveAssetUrl, WorkerEngineClient, } from "../worker-engine.js";
4
+ import { pdfOperationSchemas } from "./schemas.js";
5
+ /**
6
+ * Starts the PDF edit worker for `original` and returns the engine the core
7
+ * drives. Loaded lazily by the PDF adapter's provider on the first `edit()`.
8
+ */
9
+ export async function loadPdfEditEngine(original, context, options = {}) {
10
+ const worker = options.createWorker
11
+ ? options.createWorker()
12
+ : createPackagedWorker(options, context);
13
+ const rpc = new WorkerRpcClient(worker);
14
+ const engine = new PdfEditEngineClient(rpc, context);
15
+ try {
16
+ await engine.start(original, options);
17
+ return engine;
18
+ }
19
+ catch (error) {
20
+ rpc.destroy();
21
+ throw error;
22
+ }
23
+ }
24
+ export class PdfEditEngineClient extends WorkerEngineClient {
25
+ schemas = pdfOperationSchemas;
26
+ async start(original, options) {
27
+ const base = this.context.assetBaseUrl;
28
+ const init = {
29
+ wasmUrl: resolveAssetUrl(options.wasmUrl, base, base
30
+ ? new URL("assets/pdfium/pdfium.wasm", base)
31
+ : packageRelativeUrl("../../../assets/pdfium/pdfium.wasm", import.meta.url)).href,
32
+ fallbackFontUrl: resolveAssetUrl(options.fallbackFontUrl, base, base
33
+ ? new URL("fonts/noto-sans-latin-cyrillic.ttf", base)
34
+ : packageRelativeUrl("../../../fonts/noto-sans-latin-cyrillic.ttf", import.meta.url)).href,
35
+ };
36
+ await this.request("edit-init", init, this.context.signal);
37
+ const data = original.slice().buffer;
38
+ const fonts = (this.context.fonts ?? []).map(toWorkerFont);
39
+ const open = {
40
+ data,
41
+ limits: this.context.limits,
42
+ format: "pdf",
43
+ ...(this.context.fileName ? { fileName: this.context.fileName } : {}),
44
+ ...(fonts.length > 0 ? { fonts } : {}),
45
+ };
46
+ await this.request("edit-open", open, this.context.signal, [
47
+ data,
48
+ ...fonts
49
+ .map((font) => font.source)
50
+ .filter((source) => source instanceof ArrayBuffer),
51
+ ]);
52
+ }
53
+ textLayout(id, signal) {
54
+ return this.request("edit-text-layout", { id }, signal);
55
+ }
56
+ positionAt(pageIndex, point, signal) {
57
+ return this.request("edit-position-at", { pageIndex, point }, signal);
58
+ }
59
+ rangeRects(range, signal) {
60
+ return this.request("edit-range-rects", { range }, signal);
61
+ }
62
+ async renderWithout(pageIndex, elementIds, scale, signal) {
63
+ const bitmap = await this.request("edit-render-without", { pageIndex, elementIds, scale }, signal);
64
+ return { ...bitmap, data: new Uint8Array(bitmap.data) };
65
+ }
66
+ pageLayout(pageIndex, signal) {
67
+ return this.request("edit-page-layout", { pageIndex }, signal);
68
+ }
69
+ }
70
+ /** A host font in the worker's shape: bytes are copied, URLs made absolute. */
71
+ function toWorkerFont(font) {
72
+ const source = font.source instanceof ArrayBuffer
73
+ ? font.source.slice(0)
74
+ : font.source instanceof Uint8Array
75
+ ? font.source.slice().buffer
76
+ : new URL(font.source, globalThis.location?.href ?? "http://localhost/")
77
+ .href;
78
+ return {
79
+ family: font.family,
80
+ weight: font.weight ?? 400,
81
+ style: font.style ?? "normal",
82
+ source,
83
+ };
84
+ }
85
+ function createPackagedWorker(options, context) {
86
+ if (typeof Worker === "undefined")
87
+ throw new ViewerError("edit-unsupported", "PDF editing requires a browser module Worker", { details: { format: "pdf", reason: "no-worker" } });
88
+ const url = resolveAssetUrl(options.workerUrl, context.assetBaseUrl, context.assetBaseUrl
89
+ ? new URL("workers/pdf-edit-worker.js", context.assetBaseUrl)
90
+ : packageRelativeUrl("../../../workers/pdf-edit-worker.js", import.meta.url));
91
+ return new Worker(url, { type: "module", name: "web-doc-pdf-edit" });
92
+ }