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,183 @@
1
+ import { OBJECT_PATH } from "./elements.js";
2
+ import { pageToUser } from "./geometry.js";
3
+ import { parseColor, validateRect } from "./text-box.js";
4
+ /*
5
+ * Vector shapes as path objects. Geometry is given in page space and turned
6
+ * into user space point by point, so shapes land where the host expects on
7
+ * rotated and cropped pages too.
8
+ */
9
+ /** FPDFPath_SetDrawMode fill modes. */
10
+ const FILL_NONE = 0;
11
+ const FILL_ALTERNATE = 1;
12
+ /** Bézier control-point ratio that approximates a quarter circle. */
13
+ const KAPPA = 0.5522847498;
14
+ /** Round joins keep rectangle corners tidy at wide strokes. */
15
+ const LINE_JOIN_ROUND = 1;
16
+ export const insertShape = {
17
+ validate(operation, context, issue) {
18
+ if (operation.pageIndex >= context.pageCount) {
19
+ issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
20
+ return;
21
+ }
22
+ const geometry = context.geometry(operation.pageIndex);
23
+ if (operation.shape === "line") {
24
+ if (!operation.from || !operation.to) {
25
+ issue("", "required", "A line needs `from` and `to`");
26
+ return;
27
+ }
28
+ if ("fill" in operation && operation.fill !== undefined)
29
+ issue("/fill", "unsupported-style", "A line cannot be filled");
30
+ if (!operation.stroke)
31
+ issue("/stroke", "required", "A line needs a stroke");
32
+ for (const [name, point] of [
33
+ ["from", operation.from],
34
+ ["to", operation.to],
35
+ ])
36
+ validateRect({ ...point, width: 0.001, height: 0.001 }, geometry, issue, `/${name}`);
37
+ return;
38
+ }
39
+ if (!operation.rect) {
40
+ issue("/rect", "required", `A ${operation.shape} needs a rect`);
41
+ return;
42
+ }
43
+ validateRect(operation.rect, geometry, issue);
44
+ if (!operation.stroke && !operation.fill)
45
+ issue("", "required", "Give a stroke, a fill, or both");
46
+ },
47
+ apply(operation, context) {
48
+ const { lib } = context.pdfium;
49
+ const geometry = context.geometry(operation.pageIndex);
50
+ const id = context.newId(operation.pageIndex);
51
+ const path = operation.shape === "line"
52
+ ? linePath(context, geometry, operation.from, operation.to)
53
+ : operation.shape === "rectangle"
54
+ ? rectanglePath(context, geometry, operation.rect)
55
+ : ellipsePath(context, geometry, operation.rect);
56
+ applyStyle(context, path, operation.stroke, operation.shape === "line" ? undefined : operation.fill);
57
+ context.withPage(operation.pageIndex, (page) => {
58
+ lib.FPDFPage_InsertObject(page, path);
59
+ });
60
+ context.appendObjects(operation.pageIndex, [{ id, type: OBJECT_PATH }]);
61
+ return {
62
+ createdIds: [id],
63
+ changedPages: [operation.pageIndex],
64
+ warnings: [],
65
+ };
66
+ },
67
+ };
68
+ export const setShapeStyle = {
69
+ validate(operation, context, issue) {
70
+ const target = shapeTarget(operation.target, context, issue);
71
+ if (!target)
72
+ return;
73
+ const style = target.element.shapeStyle ?? {};
74
+ const stroke = operation.stroke === undefined ? style.stroke : operation.stroke;
75
+ const fill = operation.fill === undefined ? style.fill : operation.fill;
76
+ if (!stroke && !fill)
77
+ issue("", "required", "A shape keeps a stroke, a fill, or both");
78
+ },
79
+ apply(operation, context) {
80
+ const { lib } = context.pdfium;
81
+ const { location, element } = shapeTarget(operation.target, context);
82
+ const style = element.shapeStyle ?? {};
83
+ const stroke = operation.stroke === undefined ? style.stroke : operation.stroke;
84
+ const fill = operation.fill === undefined ? style.fill : operation.fill;
85
+ context.withPage(location.pageIndex, (page) => {
86
+ applyStyle(context, lib.FPDFPage_GetObject(page, location.indexes[0]), stroke ?? undefined, fill ?? undefined);
87
+ });
88
+ return result(location);
89
+ },
90
+ };
91
+ function shapeTarget(target, context, issue = () => { }) {
92
+ const location = context.locate(target);
93
+ const element = location && context.element(target);
94
+ if (!location || !element) {
95
+ issue("/target", "unknown-target", `No element ${target}`);
96
+ return undefined;
97
+ }
98
+ if (element.kind !== "shape") {
99
+ issue("/target", "unsupported-target", "Only shapes have a stroke and fill");
100
+ return undefined;
101
+ }
102
+ return { location, element };
103
+ }
104
+ function applyStyle(context, path, stroke, fill) {
105
+ const { lib } = context.pdfium;
106
+ if (stroke) {
107
+ const [r, g, b] = parseColor(stroke.color);
108
+ lib.FPDFPageObj_SetStrokeColor(path, r, g, b, 255);
109
+ lib.FPDFPageObj_SetStrokeWidth(path, stroke.width);
110
+ }
111
+ if (fill) {
112
+ const [r, g, b] = parseColor(fill.color);
113
+ lib.FPDFPageObj_SetFillColor(path, r, g, b, 255);
114
+ }
115
+ lib.FPDFPath_SetDrawMode(path, fill ? FILL_ALTERNATE : FILL_NONE, Boolean(stroke));
116
+ }
117
+ function linePath(context, geometry, from, to) {
118
+ const { lib } = context.pdfium;
119
+ const start = pageToUser(geometry, from.x, from.y);
120
+ const end = pageToUser(geometry, to.x, to.y);
121
+ const path = lib.FPDFPageObj_CreateNewPath(start.x, start.y);
122
+ lib.FPDFPath_LineTo(path, end.x, end.y);
123
+ return path;
124
+ }
125
+ function rectanglePath(context, geometry, rect) {
126
+ const { lib } = context.pdfium;
127
+ const corners = [
128
+ pageToUser(geometry, rect.x, rect.y),
129
+ pageToUser(geometry, rect.x + rect.width, rect.y),
130
+ pageToUser(geometry, rect.x + rect.width, rect.y + rect.height),
131
+ pageToUser(geometry, rect.x, rect.y + rect.height),
132
+ ];
133
+ const path = lib.FPDFPageObj_CreateNewPath(corners[0].x, corners[0].y);
134
+ for (const corner of corners.slice(1))
135
+ lib.FPDFPath_LineTo(path, corner.x, corner.y);
136
+ lib.FPDFPath_Close(path);
137
+ lib.FPDFPageObj_SetLineJoin(path, LINE_JOIN_ROUND);
138
+ return path;
139
+ }
140
+ function ellipsePath(context, geometry, rect) {
141
+ const { lib } = context.pdfium;
142
+ const cx = rect.x + rect.width / 2;
143
+ const cy = rect.y + rect.height / 2;
144
+ const rx = rect.width / 2;
145
+ const ry = rect.height / 2;
146
+ const at = (x, y) => pageToUser(geometry, x, y);
147
+ // Four quarter arcs, clockwise in page space from the right-most point.
148
+ const arcs = [
149
+ [
150
+ { x: cx + rx, y: cy + ry * KAPPA },
151
+ { x: cx + rx * KAPPA, y: cy + ry },
152
+ { x: cx, y: cy + ry },
153
+ ],
154
+ [
155
+ { x: cx - rx * KAPPA, y: cy + ry },
156
+ { x: cx - rx, y: cy + ry * KAPPA },
157
+ { x: cx - rx, y: cy },
158
+ ],
159
+ [
160
+ { x: cx - rx, y: cy - ry * KAPPA },
161
+ { x: cx - rx * KAPPA, y: cy - ry },
162
+ { x: cx, y: cy - ry },
163
+ ],
164
+ [
165
+ { x: cx + rx * KAPPA, y: cy - ry },
166
+ { x: cx + rx, y: cy - ry * KAPPA },
167
+ { x: cx + rx, y: cy },
168
+ ],
169
+ ];
170
+ const start = at(cx + rx, cy);
171
+ const path = lib.FPDFPageObj_CreateNewPath(start.x, start.y);
172
+ for (const [c1, c2, end] of arcs) {
173
+ const u1 = at(c1.x, c1.y);
174
+ const u2 = at(c2.x, c2.y);
175
+ const u3 = at(end.x, end.y);
176
+ lib.FPDFPath_BezierTo(path, u1.x, u1.y, u2.x, u2.y, u3.x, u3.y);
177
+ }
178
+ lib.FPDFPath_Close(path);
179
+ return path;
180
+ }
181
+ function result(location) {
182
+ return { createdIds: [], changedPages: [location.pageIndex], warnings: [] };
183
+ }
@@ -0,0 +1,44 @@
1
+ import type { PagePoint } from "../../types.js";
2
+ import type { InsertTableOperation, PdfTableStyle, SetTableCellOperation } from "../types.js";
3
+ import { type MarkParams } from "./elements.js";
4
+ import type { FontRequest } from "./fonts.js";
5
+ import type { ElementLocation, Issue, OperationContext, OperationHandler, OperationResult } from "./operations.js";
6
+ import { type DrawnTextBox } from "./text-box.js";
7
+ export declare const MAX_ROWS = 100;
8
+ export declare const MAX_COLUMNS = 20;
9
+ /** A table's inputs with every default filled in; stored in its head mark. */
10
+ export type TableSpec = {
11
+ readonly kind: "table";
12
+ readonly id: string;
13
+ readonly at: PagePoint;
14
+ readonly rows: readonly (readonly string[])[];
15
+ /** Absolute column widths in points. */
16
+ readonly columnWidths: readonly number[];
17
+ readonly style: {
18
+ readonly fontFamily: string;
19
+ readonly fontSize: number;
20
+ readonly color: string;
21
+ readonly borderColor: string;
22
+ readonly borderWidth: number;
23
+ readonly cellPadding: number;
24
+ readonly headerFill: string | null;
25
+ };
26
+ };
27
+ export declare const insertTable: OperationHandler<InsertTableOperation>;
28
+ export declare const setTableCell: OperationHandler<SetTableCellOperation>;
29
+ /** Lays the table out and inserts its objects, appended or at `insertAt`. */
30
+ export declare function drawTable(context: OperationContext, pageIndex: number, spec: TableSpec, insertAt?: number): DrawnTextBox;
31
+ /** Redraws a table from `spec` at the same place in the drawing order. */
32
+ export declare function rebuildTable(context: OperationContext, location: ElementLocation, spec: TableSpec): DrawnTextBox;
33
+ /** The location and stored inputs of a table, or the issue that stops the edit. */
34
+ export declare function tableTarget(target: string, context: OperationContext, issue?: Issue): {
35
+ readonly location: ElementLocation;
36
+ readonly spec: TableSpec;
37
+ } | undefined;
38
+ /** The spec a mark carries when it is a table's head mark. */
39
+ export declare function tableSpecOf(mark: MarkParams | undefined): TableSpec | undefined;
40
+ export declare function changedTable(location: ElementLocation, drawn: DrawnTextBox): OperationResult;
41
+ /** Every cell's text, for font coverage checks. */
42
+ export declare function tableText(rows: readonly (readonly string[])[]): string;
43
+ export declare function tableFontRequest(style: TableSpec["style"], text: string): FontRequest;
44
+ export declare function resolveTableStyle(style: PdfTableStyle): TableSpec["style"];
@@ -0,0 +1,315 @@
1
+ import { MARK_NAME, MARK_PARAM, OBJECT_PATH, OBJECT_TEXT, } from "./elements.js";
2
+ import { displayedSize, pageToUser } from "./geometry.js";
3
+ import { layoutText } from "./text-layout.js";
4
+ import { parseColor, placeUpright, removeObjects, setText, validateRect, } from "./text-box.js";
5
+ /*
6
+ * Tables are parametric like text boxes: the grid is one stroked path, an
7
+ * optional header fill one more, and every cell line a text object. The
8
+ * inputs live in the `WebDoc` mark of the path objects; the text objects
9
+ * carry only the table's id, so a large table does not repeat its contents.
10
+ */
11
+ export const MAX_ROWS = 100;
12
+ export const MAX_COLUMNS = 20;
13
+ /** FPDFPath_SetDrawMode fill modes. */
14
+ const FILL_NONE = 0;
15
+ const FILL_ALTERNATE = 1;
16
+ const LINE_HEIGHT = 1.2;
17
+ export const insertTable = {
18
+ validate(operation, context, issue) {
19
+ if (operation.pageIndex >= context.pageCount) {
20
+ issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
21
+ return;
22
+ }
23
+ if (!validateRows(operation.rows, issue))
24
+ return;
25
+ const columns = operation.rows[0].length;
26
+ if (operation.columnWidths !== undefined &&
27
+ operation.columnWidths.length !== columns) {
28
+ issue("/columnWidths", "range", `Give one width per column; the table has ${columns}`);
29
+ return;
30
+ }
31
+ const style = resolveTableStyle(operation.style ?? {});
32
+ const widths = resolveColumns(operation.width, columns, operation.columnWidths);
33
+ validateRect({
34
+ x: operation.at.x,
35
+ y: operation.at.y,
36
+ width: operation.width,
37
+ height: 0.001,
38
+ }, context.geometry(operation.pageIndex), issue, "/at");
39
+ if (Math.min(...widths) - 2 * style.cellPadding < 1)
40
+ issue("/width", "range", "Every column must be wider than twice the cell padding");
41
+ validateTableFont(style, tableText(operation.rows), context, issue, "/rows");
42
+ },
43
+ apply(operation, context) {
44
+ const id = context.newId(operation.pageIndex);
45
+ const columns = operation.rows[0].length;
46
+ const spec = {
47
+ kind: "table",
48
+ id,
49
+ at: operation.at,
50
+ rows: operation.rows,
51
+ columnWidths: resolveColumns(operation.width, columns, operation.columnWidths),
52
+ style: resolveTableStyle(operation.style ?? {}),
53
+ };
54
+ const drawn = drawTable(context, operation.pageIndex, spec);
55
+ return {
56
+ createdIds: [id],
57
+ changedPages: [operation.pageIndex],
58
+ warnings: tableWarnings(drawn, id),
59
+ };
60
+ },
61
+ };
62
+ export const setTableCell = {
63
+ validate(operation, context, issue) {
64
+ const target = tableTarget(operation.target, context, issue);
65
+ if (!target)
66
+ return;
67
+ const { spec } = target;
68
+ if (operation.row >= spec.rows.length) {
69
+ issue("/row", "range", `The table has ${spec.rows.length} rows`);
70
+ return;
71
+ }
72
+ const columns = spec.rows[0].length;
73
+ if (operation.column >= columns) {
74
+ issue("/column", "range", `The table has ${columns} columns`);
75
+ return;
76
+ }
77
+ validateTableFont(spec.style, operation.text, context, issue, "/text");
78
+ },
79
+ apply(operation, context) {
80
+ const { location, spec } = tableTarget(operation.target, context);
81
+ const rows = spec.rows.map((row, rowIndex) => rowIndex === operation.row
82
+ ? row.map((cell, columnIndex) => columnIndex === operation.column ? operation.text : cell)
83
+ : row);
84
+ return changedTable(location, rebuildTable(context, location, { ...spec, rows }));
85
+ },
86
+ };
87
+ /** Lays the table out and inserts its objects, appended or at `insertAt`. */
88
+ export function drawTable(context, pageIndex, spec, insertAt) {
89
+ const { pdfium, measurer } = context;
90
+ const { lib } = pdfium;
91
+ const { style } = spec;
92
+ const font = context.fonts.resolve(pdfium, context.document, tableFontRequest(style, tableText(spec.rows)));
93
+ const { ascent } = measurer.metrics(font.handle, style.fontSize);
94
+ const layout = layoutTable(spec, ascent, (text) => measurer.advance(font.handle, style.fontSize, text));
95
+ const geometry = context.geometry(pageIndex);
96
+ const overflow = spec.at.y + layout.height > displayedSize(geometry).height + 0.001;
97
+ const head = JSON.stringify(spec);
98
+ const memberMark = { kind: "table", id: spec.id };
99
+ const member = JSON.stringify(memberMark);
100
+ const at = (x, y) => pageToUser(geometry, x, y);
101
+ const width = spec.columnWidths.reduce((total, w) => total + w, 0);
102
+ const right = spec.at.x + width;
103
+ const bottom = spec.at.y + layout.height;
104
+ const [textR, textG, textB] = parseColor(style.color);
105
+ context.withPage(pageIndex, (page) => {
106
+ const records = [];
107
+ let index = insertAt ?? -1;
108
+ const place = (object, type, mark, params) => {
109
+ const handle = lib.FPDFPageObj_AddMark(object, MARK_NAME);
110
+ lib.FPDFPageObjMark_SetStringParam(context.document, object, handle, MARK_PARAM, params);
111
+ if (insertAt === undefined)
112
+ lib.FPDFPage_InsertObject(page, object);
113
+ else
114
+ lib.FPDFPage_InsertObjectAtIndex(page, object, index++);
115
+ records.push({ id: spec.id, type, mark });
116
+ };
117
+ if (style.headerFill) {
118
+ const headerBottom = spec.at.y + layout.rows[0].height;
119
+ const corners = [
120
+ at(spec.at.x, spec.at.y),
121
+ at(right, spec.at.y),
122
+ at(right, headerBottom),
123
+ at(spec.at.x, headerBottom),
124
+ ];
125
+ const fill = lib.FPDFPageObj_CreateNewPath(corners[0].x, corners[0].y);
126
+ for (const corner of corners.slice(1))
127
+ lib.FPDFPath_LineTo(fill, corner.x, corner.y);
128
+ lib.FPDFPath_Close(fill);
129
+ const [r, g, b] = parseColor(style.headerFill);
130
+ lib.FPDFPageObj_SetFillColor(fill, r, g, b, 255);
131
+ lib.FPDFPath_SetDrawMode(fill, FILL_ALTERNATE, false);
132
+ place(fill, OBJECT_PATH, spec, head);
133
+ }
134
+ // One path for the whole grid: the frame, then the inner lines.
135
+ const frame = [
136
+ at(spec.at.x, spec.at.y),
137
+ at(right, spec.at.y),
138
+ at(right, bottom),
139
+ at(spec.at.x, bottom),
140
+ ];
141
+ const grid = lib.FPDFPageObj_CreateNewPath(frame[0].x, frame[0].y);
142
+ for (const corner of frame.slice(1))
143
+ lib.FPDFPath_LineTo(grid, corner.x, corner.y);
144
+ lib.FPDFPath_Close(grid);
145
+ for (const column of layout.columns.slice(1)) {
146
+ const top = at(column.x, spec.at.y);
147
+ const end = at(column.x, bottom);
148
+ lib.FPDFPath_MoveTo(grid, top.x, top.y);
149
+ lib.FPDFPath_LineTo(grid, end.x, end.y);
150
+ }
151
+ for (const row of layout.rows.slice(1)) {
152
+ const start = at(spec.at.x, row.y);
153
+ const end = at(right, row.y);
154
+ lib.FPDFPath_MoveTo(grid, start.x, start.y);
155
+ lib.FPDFPath_LineTo(grid, end.x, end.y);
156
+ }
157
+ const [r, g, b] = parseColor(style.borderColor);
158
+ lib.FPDFPageObj_SetStrokeColor(grid, r, g, b, 255);
159
+ lib.FPDFPageObj_SetStrokeWidth(grid, style.borderWidth);
160
+ lib.FPDFPath_SetDrawMode(grid, FILL_NONE, true);
161
+ place(grid, OBJECT_PATH, spec, head);
162
+ layout.rows.forEach((row) => {
163
+ row.cells.forEach((lines, columnIndex) => {
164
+ const column = layout.columns[columnIndex];
165
+ for (const line of lines) {
166
+ if (!line.text)
167
+ continue;
168
+ const object = lib.FPDFPageObj_CreateTextObj(context.document, font.handle, style.fontSize);
169
+ setText(pdfium, object, line.text);
170
+ lib.FPDFPageObj_SetFillColor(object, textR, textG, textB, 255);
171
+ placeUpright(pdfium, object, geometry, column.x + style.cellPadding + line.x, row.y + style.cellPadding + line.baseline);
172
+ place(object, OBJECT_TEXT, memberMark, member);
173
+ }
174
+ });
175
+ });
176
+ if (insertAt === undefined)
177
+ context.appendObjects(pageIndex, records);
178
+ else
179
+ context.spliceObjects(pageIndex, insertAt, 0, records);
180
+ });
181
+ return {
182
+ overflow,
183
+ ...(font.substitution ? { substitution: font.substitution } : {}),
184
+ };
185
+ }
186
+ /** Redraws a table from `spec` at the same place in the drawing order. */
187
+ export function rebuildTable(context, location, spec) {
188
+ const first = removeObjects(context, location);
189
+ return drawTable(context, location.pageIndex, spec, first);
190
+ }
191
+ /** The location and stored inputs of a table, or the issue that stops the edit. */
192
+ export function tableTarget(target, context, issue = () => { }) {
193
+ const location = context.locate(target);
194
+ if (!location) {
195
+ issue("/target", "unknown-target", `No element ${target}`);
196
+ return undefined;
197
+ }
198
+ const spec = tableSpecOf(location.record.mark);
199
+ if (!spec) {
200
+ issue("/target", "unsupported-target", "Only tables created by web-doc have cells");
201
+ return undefined;
202
+ }
203
+ return { location, spec };
204
+ }
205
+ /** The spec a mark carries when it is a table's head mark. */
206
+ export function tableSpecOf(mark) {
207
+ return mark?.kind === "table" && Array.isArray(mark.rows)
208
+ ? mark
209
+ : undefined;
210
+ }
211
+ export function changedTable(location, drawn) {
212
+ return {
213
+ createdIds: [],
214
+ changedPages: [location.pageIndex],
215
+ warnings: tableWarnings(drawn, location.record.id),
216
+ };
217
+ }
218
+ /** Every cell's text, for font coverage checks. */
219
+ export function tableText(rows) {
220
+ return rows.flat().join("\n");
221
+ }
222
+ export function tableFontRequest(style, text) {
223
+ return { family: style.fontFamily, bold: false, italic: false, text };
224
+ }
225
+ export function resolveTableStyle(style) {
226
+ return {
227
+ fontFamily: style.fontFamily ?? "Helvetica",
228
+ fontSize: style.fontSize ?? 10,
229
+ color: style.color ?? "#000000",
230
+ borderColor: style.borderColor ?? "#000000",
231
+ borderWidth: style.borderWidth ?? 0.75,
232
+ cellPadding: style.cellPadding ?? 4,
233
+ headerFill: style.headerFill ?? null,
234
+ };
235
+ }
236
+ /** Column edges and row heights: each row is as tall as its tallest cell. */
237
+ function layoutTable(spec, ascent, advance) {
238
+ const { cellPadding, fontSize } = spec.style;
239
+ const columns = [];
240
+ let x = spec.at.x;
241
+ for (const width of spec.columnWidths) {
242
+ columns.push({ x, width });
243
+ x += width;
244
+ }
245
+ const rows = [];
246
+ let y = spec.at.y;
247
+ for (const cells of spec.rows) {
248
+ const layouts = cells.map((text, columnIndex) => layoutText({
249
+ text,
250
+ width: columns[columnIndex].width - 2 * cellPadding,
251
+ height: Number.POSITIVE_INFINITY,
252
+ fontSize,
253
+ lineHeight: LINE_HEIGHT,
254
+ align: "left",
255
+ ascent,
256
+ advance,
257
+ }));
258
+ const height = Math.max(...layouts.map((layout) => layout.height)) + 2 * cellPadding;
259
+ rows.push({ y, height, cells: layouts.map((layout) => layout.lines) });
260
+ y += height;
261
+ }
262
+ return { columns, rows, height: y - spec.at.y };
263
+ }
264
+ /** Column widths in points from optional relative weights. */
265
+ function resolveColumns(width, columns, weights) {
266
+ const relative = weights ?? Array.from({ length: columns }, () => 1);
267
+ const total = relative.reduce((sum, weight) => sum + weight, 0);
268
+ return relative.map((weight) => (weight / total) * width);
269
+ }
270
+ function validateRows(rows, issue) {
271
+ if (rows.length === 0 || rows.length > MAX_ROWS) {
272
+ issue("/rows", "range", `A table has 1 to ${MAX_ROWS} rows`);
273
+ return false;
274
+ }
275
+ const columns = rows[0].length;
276
+ if (columns === 0 || columns > MAX_COLUMNS) {
277
+ issue("/rows/0", "range", `A table has 1 to ${MAX_COLUMNS} columns`);
278
+ return false;
279
+ }
280
+ let ok = true;
281
+ rows.forEach((row, index) => {
282
+ if (row.length !== columns) {
283
+ issue(`/rows/${index}`, "range", `Row ${index} has ${row.length} cells; the first row has ${columns}`);
284
+ ok = false;
285
+ }
286
+ });
287
+ return ok;
288
+ }
289
+ function validateTableFont(style, text, context, issue, textPath) {
290
+ const problem = context.fonts.problem(tableFontRequest(style, text));
291
+ if (problem)
292
+ issue(problem.path === "/text" ? textPath : problem.path, problem.code, problem.message);
293
+ }
294
+ function tableWarnings(drawn, elementId) {
295
+ return [
296
+ ...(drawn.overflow
297
+ ? [
298
+ {
299
+ code: "fidelity-degraded",
300
+ message: "The table runs past the bottom of the page",
301
+ details: { elementId },
302
+ },
303
+ ]
304
+ : []),
305
+ ...(drawn.substitution
306
+ ? [
307
+ {
308
+ code: "font-substitution",
309
+ message: drawn.substitution,
310
+ details: { elementId },
311
+ },
312
+ ]
313
+ : []),
314
+ ];
315
+ }
@@ -0,0 +1,71 @@
1
+ import type { EditOperation, PageRect } from "../../types.js";
2
+ import type { InsertTextBoxOperation, PdfElement, PdfTextAlign, PdfTextBoxStyle, ReplaceTextOperation, SetTextStyleOperation } from "../types.js";
3
+ import { type MarkParams } from "./elements.js";
4
+ import type { FontRequest } from "./fonts.js";
5
+ import { type PageGeometry } from "./geometry.js";
6
+ import type { ElementLocation, Issue, OperationContext, OperationHandler, OperationResult } from "./operations.js";
7
+ import type { Pdfium } from "./pdfium.js";
8
+ /** A text box's inputs with every default filled in; stored in its mark. */
9
+ export type TextBoxSpec = {
10
+ readonly kind: "textBox";
11
+ readonly id: string;
12
+ readonly rect: PageRect;
13
+ readonly text: string;
14
+ readonly style: {
15
+ readonly fontFamily: string;
16
+ readonly fontSize: number;
17
+ readonly bold: boolean;
18
+ readonly italic: boolean;
19
+ readonly color: string;
20
+ readonly align: PdfTextAlign;
21
+ readonly lineHeight: number;
22
+ };
23
+ };
24
+ export declare const insertTextBox: OperationHandler<InsertTextBoxOperation>;
25
+ /** `replaceText` for a text box: a rebuild from its inputs with new text. */
26
+ export declare const textBoxReplaceText: OperationHandler<ReplaceTextOperation>;
27
+ /** `setTextStyle` for a text box: a rebuild with the merged style. */
28
+ export declare const textBoxSetTextStyle: OperationHandler<SetTextStyleOperation>;
29
+ export interface DrawnTextBox {
30
+ readonly overflow: boolean;
31
+ /** Set when the text was drawn with another font than asked for. */
32
+ readonly substitution?: string;
33
+ }
34
+ /**
35
+ * Lays the box out and inserts one text object per line, appended to the
36
+ * page or at `insertAt` in drawing order.
37
+ */
38
+ export declare function drawTextBox(context: OperationContext, pageIndex: number, spec: TextBoxSpec, insertAt?: number): DrawnTextBox;
39
+ /**
40
+ * Replaces a text box's objects with ones drawn from `spec`, at the same
41
+ * place in the drawing order so nothing else changes its stacking.
42
+ */
43
+ export declare function rebuildTextBox(context: OperationContext, location: ElementLocation, spec: TextBoxSpec): DrawnTextBox;
44
+ /** Removes an element's objects from the page; returns where they started. */
45
+ export declare function removeObjects(context: OperationContext, location: ElementLocation): number;
46
+ /** The location and stored inputs of a text box, or the issue that stops the edit. */
47
+ export declare function textBoxTarget(target: string, context: OperationContext, issue?: Issue): {
48
+ readonly location: ElementLocation;
49
+ readonly spec: TextBoxSpec;
50
+ } | undefined;
51
+ export declare function changed(location: ElementLocation, drawn: DrawnTextBox): OperationResult;
52
+ /**
53
+ * The fonts a batch will need: what each operation draws, read from the
54
+ * operation itself, from the text box it rebuilds, or from the text object
55
+ * it may have to replace.
56
+ */
57
+ export declare function fontRequestsOf(operations: readonly EditOperation[], markOf: (id: string) => MarkParams | undefined, elementOf: (id: string) => PdfElement | undefined): {
58
+ readonly family: string;
59
+ readonly text: string;
60
+ }[];
61
+ export declare function fontRequest(style: TextBoxSpec["style"], text: string): FontRequest;
62
+ export declare function validateFont(request: FontRequest, context: OperationContext, issue: Issue): void;
63
+ /**
64
+ * Positions an object so it reads upright on the displayed page: the object's
65
+ * origin goes to the page-space point and it turns against the page's /Rotate.
66
+ */
67
+ export declare function placeUpright(pdfium: Pdfium, object: number, geometry: PageGeometry, pageX: number, pageY: number): void;
68
+ export declare function setText(pdfium: Pdfium, object: number, text: string): void;
69
+ export declare function parseColor(color: string): [number, number, number];
70
+ export declare function resolveStyle(style: PdfTextBoxStyle): TextBoxSpec["style"];
71
+ export declare function validateRect(rect: PageRect, geometry: PageGeometry, issue: Issue, path?: string): void;