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,108 @@
1
+ import { ViewerError } from "../../../errors.js";
2
+ import { pdfOperationSchemas } from "../schemas.js";
3
+ import { PdfEditDocument } from "./document.js";
4
+ import { AssetStore } from "../../assets.js";
5
+ import { FontLibrary } from "./fonts.js";
6
+ /**
7
+ * Serves the edit worker protocol for one PDF document. It runs inside the
8
+ * edit worker in the browser and directly in Node tests; the host supplies
9
+ * how WebAssembly and fonts are obtained.
10
+ */
11
+ export function createPdfEditHandler(host) {
12
+ let pdfium;
13
+ let state;
14
+ const fonts = new FontLibrary((url) => host.fetchBytes(url));
15
+ const assets = new AssetStore();
16
+ const engine = () => {
17
+ if (!state)
18
+ throw new ViewerError("lifecycle-error", "No PDF is open for editing");
19
+ return state;
20
+ };
21
+ return async (operation, payload) => {
22
+ switch (operation) {
23
+ case "edit-init": {
24
+ const init = payload;
25
+ pdfium ??= await host.loadPdfium(init.wasmUrl);
26
+ fonts.setFallbackUrl(init.fallbackFontUrl);
27
+ return undefined;
28
+ }
29
+ case "edit-open": {
30
+ if (!pdfium)
31
+ throw new ViewerError("lifecycle-error", "PDFium is not initialised");
32
+ const open = payload;
33
+ state?.dispose();
34
+ fonts.register(open.fonts ?? []);
35
+ state = new PdfEditDocument(pdfium, new Uint8Array(open.data), fonts, open.limits, assets, host.compact);
36
+ const result = { pageCount: state.pageCount };
37
+ return result;
38
+ }
39
+ case "edit-validate": {
40
+ const { operations } = payload;
41
+ await fonts.prepare(engine().fontRequests(operations));
42
+ await engine().images.prepare(operations, host.decodeImage, assets);
43
+ return engine().validate(operations);
44
+ }
45
+ case "edit-apply": {
46
+ const { batch } = payload;
47
+ await fonts.prepare(engine().fontRequests(batch.operations));
48
+ await engine().images.prepare(batch.operations, host.decodeImage, assets);
49
+ return engine().apply(batch);
50
+ }
51
+ case "edit-put-asset": {
52
+ const { id, data } = payload;
53
+ assets.set(id, new Uint8Array(data));
54
+ return undefined;
55
+ }
56
+ case "edit-materialize": {
57
+ const { purpose, options } = (payload ?? {});
58
+ const mode = options?.mode;
59
+ const { bytes, warnings } = engine().materializeDocument(purpose ?? "show", mode === "full" || mode === "incremental" ? mode : undefined);
60
+ return { data: bytes.buffer, warnings };
61
+ }
62
+ case "edit-restore": {
63
+ const { batches, base } = payload;
64
+ const operations = batches.flatMap((batch) => batch.operations);
65
+ await fonts.prepare(engine().fontRequests(operations));
66
+ await engine().images.prepare(operations, host.decodeImage, assets);
67
+ engine().restore({
68
+ batches,
69
+ ...(base ? { base: new Uint8Array(base) } : {}),
70
+ });
71
+ return undefined;
72
+ }
73
+ case "edit-elements":
74
+ return engine().getElements(payload.query);
75
+ case "edit-element":
76
+ return engine().getElement(payload.id);
77
+ case "edit-elements-at": {
78
+ const { pageIndex, point } = payload;
79
+ return engine().elementsAt(pageIndex, point);
80
+ }
81
+ case "edit-find-text": {
82
+ const { query, options } = payload;
83
+ return engine().findText(query, options);
84
+ }
85
+ case "edit-text-layout":
86
+ return engine().textLayout(payload.id);
87
+ case "edit-position-at": {
88
+ const { pageIndex, point } = payload;
89
+ return engine().positionAt(pageIndex, point);
90
+ }
91
+ case "edit-range-rects":
92
+ return engine().rangeRects(payload.range);
93
+ case "edit-render-without": {
94
+ const { pageIndex, elementIds, scale } = payload;
95
+ return engine().renderPageWithout(pageIndex, elementIds, scale);
96
+ }
97
+ case "edit-page-layout":
98
+ return engine().pageLayout(payload.pageIndex);
99
+ case "edit-dispose":
100
+ state?.dispose();
101
+ state = undefined;
102
+ return undefined;
103
+ default:
104
+ throw new ViewerError("internal", `The PDF edit worker does not handle ${operation}`);
105
+ }
106
+ };
107
+ }
108
+ export { pdfOperationSchemas };
@@ -0,0 +1,33 @@
1
+ import { type AssetSource } from "../../assets.js";
2
+ import type { EditOperation } from "../../types.js";
3
+ import type { InsertImageOperation } from "../types.js";
4
+ import type { OperationHandler } from "./operations.js";
5
+ export interface DecodedImage {
6
+ readonly width: number;
7
+ readonly height: number;
8
+ /** Row-major RGBA, 4 bytes per pixel. */
9
+ readonly rgba: Uint8Array;
10
+ }
11
+ export type ImageDecoder = (bytes: Uint8Array, mimeType: "image/png" | "image/jpeg") => Promise<DecodedImage>;
12
+ /**
13
+ * Decoded pixels for the PNG operations of a batch, keyed by content, so the
14
+ * synchronous apply never decodes. Only the last few images are kept.
15
+ */
16
+ export declare class ImageCache {
17
+ #private;
18
+ /** Decodes every PNG the batch inserts; must run before validate or apply. */
19
+ prepare(operations: readonly EditOperation[], decode: ImageDecoder, assets: AssetSource): Promise<void>;
20
+ get(bytes: Uint8Array): DecodedImage | undefined;
21
+ }
22
+ export declare const insertImage: OperationHandler<InsertImageOperation>;
23
+ export declare function isPng(bytes: Uint8Array): boolean;
24
+ /** Width and height from the IHDR chunk. */
25
+ export declare function pngSize(bytes: Uint8Array): {
26
+ width: number;
27
+ height: number;
28
+ } | undefined;
29
+ /** Width and height from the first SOF marker of a baseline or progressive JPEG. */
30
+ export declare function jpegSize(bytes: Uint8Array): {
31
+ width: number;
32
+ height: number;
33
+ } | undefined;
@@ -0,0 +1,188 @@
1
+ import { ViewerError } from "../../../errors.js";
2
+ import { resolveBinary } from "../../assets.js";
3
+ import { OBJECT_IMAGE } from "./elements.js";
4
+ import { pageToUser } from "./geometry.js";
5
+ import { validateRect } from "./text-box.js";
6
+ /*
7
+ * Raster images. JPEG data is handed to PDFium as it is, so the file keeps
8
+ * the original stream; PNG data is decoded by the host (createImageBitmap in
9
+ * the worker) into pixels PDFium stores losslessly, alpha included.
10
+ */
11
+ /** FPDFBitmap_CreateEx pixel format with alpha. */
12
+ const BITMAP_BGRA = 4;
13
+ /**
14
+ * Decoded pixels for the PNG operations of a batch, keyed by content, so the
15
+ * synchronous apply never decodes. Only the last few images are kept.
16
+ */
17
+ export class ImageCache {
18
+ #decoded = new Map();
19
+ /** Decodes every PNG the batch inserts; must run before validate or apply. */
20
+ async prepare(operations, decode, assets) {
21
+ for (const raw of operations) {
22
+ const operation = raw;
23
+ if (operation.op !== "insertImage" || operation.mimeType !== "image/png")
24
+ continue;
25
+ const bytes = resolveBinary(operation.data, assets);
26
+ if (!isPng(bytes))
27
+ continue;
28
+ const key = contentKey(bytes);
29
+ if (this.#decoded.has(key))
30
+ continue;
31
+ this.#decoded.set(key, await decode(bytes, "image/png"));
32
+ while (this.#decoded.size > 8)
33
+ this.#decoded.delete(this.#decoded.keys().next().value);
34
+ }
35
+ }
36
+ get(bytes) {
37
+ return this.#decoded.get(contentKey(bytes));
38
+ }
39
+ }
40
+ export const insertImage = {
41
+ validate(operation, context, issue) {
42
+ if (operation.pageIndex >= context.pageCount) {
43
+ issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
44
+ return;
45
+ }
46
+ validateRect(operation.rect, context.geometry(operation.pageIndex), issue);
47
+ const bytes = resolveBinary(operation.data, context.assets);
48
+ if (bytes.byteLength > context.limits.maxInputBytes)
49
+ throw new ViewerError("resource-limit", "Image data exceeds maxInputBytes", {
50
+ details: {
51
+ bytes: bytes.byteLength,
52
+ limit: context.limits.maxInputBytes,
53
+ },
54
+ });
55
+ const size = operation.mimeType === "image/png" ? pngSize(bytes) : jpegSize(bytes);
56
+ if (!size) {
57
+ issue("/data", "invalid-data", `The data is not a readable ${operation.mimeType} image`);
58
+ return;
59
+ }
60
+ const pixels = size.width * size.height;
61
+ if (!Number.isSafeInteger(pixels) ||
62
+ pixels > context.limits.maxDecodedPixels)
63
+ throw new ViewerError("resource-limit", "Decoded image exceeds maxDecodedPixels", { details: { pixels, limit: context.limits.maxDecodedPixels } });
64
+ if (operation.mimeType === "image/png" && !context.images.get(bytes))
65
+ issue("/data", "invalid-data", "The PNG could not be decoded");
66
+ },
67
+ apply(operation, context) {
68
+ const { pdfium } = context;
69
+ const { lib } = pdfium;
70
+ const bytes = resolveBinary(operation.data, context.assets);
71
+ const id = context.newId(operation.pageIndex);
72
+ const geometry = context.geometry(operation.pageIndex);
73
+ const image = lib.FPDFPageObj_NewImageObj(context.document);
74
+ if (operation.mimeType === "image/jpeg") {
75
+ const loaded = pdfium.withFileAccess(bytes, (fileAccess) => lib.FPDFImageObj_LoadJpegFileInline(0, 0, image, fileAccess));
76
+ if (!loaded)
77
+ throw new ViewerError("edit-failed", "PDFium could not read the JPEG", {
78
+ details: { stage: "apply" },
79
+ });
80
+ }
81
+ else {
82
+ const decoded = context.images.get(bytes);
83
+ setBitmap(context.pdfium, image, decoded);
84
+ }
85
+ // The unit square maps onto the page-space rectangle, upright on screen:
86
+ // its bottom-left and the two edges leaving it, in user space.
87
+ const { rect } = operation;
88
+ const origin = pageToUser(geometry, rect.x, rect.y + rect.height);
89
+ const right = pageToUser(geometry, rect.x + rect.width, rect.y + rect.height);
90
+ const up = pageToUser(geometry, rect.x, rect.y);
91
+ lib.FPDFImageObj_SetMatrix(image, right.x - origin.x, right.y - origin.y, up.x - origin.x, up.y - origin.y, origin.x, origin.y);
92
+ context.withPage(operation.pageIndex, (page) => {
93
+ lib.FPDFPage_InsertObject(page, image);
94
+ });
95
+ context.appendObjects(operation.pageIndex, [{ id, type: OBJECT_IMAGE }]);
96
+ return {
97
+ createdIds: [id],
98
+ changedPages: [operation.pageIndex],
99
+ warnings: [],
100
+ };
101
+ },
102
+ };
103
+ function setBitmap(pdfium, image, decoded) {
104
+ const { lib } = pdfium;
105
+ const stride = decoded.width * 4;
106
+ const bgra = new Uint8Array(decoded.rgba.byteLength);
107
+ for (let offset = 0; offset < bgra.byteLength; offset += 4) {
108
+ bgra[offset] = decoded.rgba[offset + 2];
109
+ bgra[offset + 1] = decoded.rgba[offset + 1];
110
+ bgra[offset + 2] = decoded.rgba[offset];
111
+ bgra[offset + 3] = decoded.rgba[offset + 3];
112
+ }
113
+ const buffer = pdfium.writeBytes(bgra);
114
+ try {
115
+ const bitmap = lib.FPDFBitmap_CreateEx(decoded.width, decoded.height, BITMAP_BGRA, buffer, stride);
116
+ if (!bitmap)
117
+ throw new ViewerError("edit-failed", "PDFium could not create the bitmap", {
118
+ details: { stage: "apply" },
119
+ });
120
+ try {
121
+ if (!lib.FPDFImageObj_SetBitmap(0, 0, image, bitmap))
122
+ throw new ViewerError("edit-failed", "PDFium could not store the image", {
123
+ details: { stage: "apply" },
124
+ });
125
+ }
126
+ finally {
127
+ lib.FPDFBitmap_Destroy(bitmap);
128
+ }
129
+ }
130
+ finally {
131
+ pdfium.free(buffer);
132
+ }
133
+ }
134
+ export function isPng(bytes) {
135
+ return (bytes.length > 24 &&
136
+ bytes[0] === 0x89 &&
137
+ bytes[1] === 0x50 &&
138
+ bytes[2] === 0x4e &&
139
+ bytes[3] === 0x47);
140
+ }
141
+ /** Width and height from the IHDR chunk. */
142
+ export function pngSize(bytes) {
143
+ if (!isPng(bytes))
144
+ return undefined;
145
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
146
+ const width = view.getUint32(16);
147
+ const height = view.getUint32(20);
148
+ return width > 0 && height > 0 ? { width, height } : undefined;
149
+ }
150
+ /** Width and height from the first SOF marker of a baseline or progressive JPEG. */
151
+ export function jpegSize(bytes) {
152
+ if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8)
153
+ return undefined;
154
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
155
+ let offset = 2;
156
+ while (offset + 9 < bytes.length) {
157
+ if (bytes[offset] !== 0xff)
158
+ return undefined;
159
+ const marker = bytes[offset + 1];
160
+ if (marker === 0xd8 ||
161
+ (marker >= 0xd0 && marker <= 0xd7) ||
162
+ marker === 0x01) {
163
+ offset += 2;
164
+ continue;
165
+ }
166
+ const length = view.getUint16(offset + 2);
167
+ if (marker >= 0xc0 &&
168
+ marker <= 0xcf &&
169
+ marker !== 0xc4 &&
170
+ marker !== 0xc8 &&
171
+ marker !== 0xcc) {
172
+ const height = view.getUint16(offset + 5);
173
+ const width = view.getUint16(offset + 7);
174
+ return width > 0 && height > 0 ? { width, height } : undefined;
175
+ }
176
+ if (marker === 0xda)
177
+ return undefined;
178
+ offset += 2 + length;
179
+ }
180
+ return undefined;
181
+ }
182
+ /** Cheap content key: length plus an FNV-1a hash of the bytes. */
183
+ function contentKey(bytes) {
184
+ let hash = 0x811c9dc5;
185
+ for (const byte of bytes)
186
+ hash = Math.imul(hash ^ byte, 0x01000193) >>> 0;
187
+ return `${bytes.byteLength}:${hash.toString(16)}`;
188
+ }
@@ -0,0 +1,23 @@
1
+ import type { PagePoint, PageRect, TextPosition, TextRange } from "../../types.js";
2
+ import type { PdfElement, TextLayout } from "../types.js";
3
+ import { type PageGeometry } from "./geometry.js";
4
+ import type { Pdfium } from "./pdfium.js";
5
+ /** A loaded page with its text page and the character-to-element mapping. */
6
+ export interface TextPageScan {
7
+ readonly page: number;
8
+ readonly textPage: number;
9
+ readonly geometry: PageGeometry;
10
+ /** Object handle → element id, in drawing order. */
11
+ readonly byObject: ReadonlyMap<number, string>;
12
+ readonly elements: readonly PdfElement[];
13
+ /** Per character of the text page: its element and offset, when an object draws it. */
14
+ readonly offsets: readonly (TextPosition | undefined)[];
15
+ }
16
+ /** The layout of one element, or `undefined` when it draws no text. */
17
+ export declare function layoutOf(pdfium: Pdfium, scan: TextPageScan, element: PdfElement): TextLayout | undefined;
18
+ /** The layouts of every text element of the page, in reading order, from one pass. */
19
+ export declare function layoutsOf(pdfium: Pdfium, scan: TextPageScan): TextLayout[];
20
+ /** The caret position nearest to a page-space point, or none without text. */
21
+ export declare function positionIn(pdfium: Pdfium, scan: TextPageScan, point: PagePoint): TextPosition | undefined;
22
+ /** The rectangles a range covers: one per line fragment, in reading order. */
23
+ export declare function rectsOf(pdfium: Pdfium, scan: TextPageScan, range: TextRange): PageRect[];
@@ -0,0 +1,177 @@
1
+ import { textStyle } from "./elements.js";
2
+ import { pageToUser, rectContains, round, roundRect, unionRects, userRectToPage, userToPage, } from "./geometry.js";
3
+ /** How far a point may be from a glyph for `FPDFText_GetCharIndexAtPos`. */
4
+ const HIT_TOLERANCE = 2;
5
+ const TEXT_KINDS = new Set(["text", "textBox", "table"]);
6
+ /** The layout of one element, or `undefined` when it draws no text. */
7
+ export function layoutOf(pdfium, scan, element) {
8
+ if (!TEXT_KINDS.has(element.kind))
9
+ return undefined;
10
+ const glyphs = glyphsOf(pdfium, scan, (position) => position.elementId === element.id);
11
+ return {
12
+ elementId: element.id,
13
+ pageIndex: element.pageIndex,
14
+ lines: linesOf(pdfium, scan, glyphs),
15
+ };
16
+ }
17
+ /** The layouts of every text element of the page, in reading order, from one pass. */
18
+ export function layoutsOf(pdfium, scan) {
19
+ const glyphs = glyphsOf(pdfium, scan, () => true);
20
+ const byElement = new Map();
21
+ for (const glyph of glyphs) {
22
+ const own = byElement.get(glyph.position.elementId);
23
+ if (own)
24
+ own.push(glyph);
25
+ else
26
+ byElement.set(glyph.position.elementId, [glyph]);
27
+ }
28
+ const layouts = [];
29
+ for (const element of scan.elements) {
30
+ const own = byElement.get(element.id);
31
+ if (!own || !TEXT_KINDS.has(element.kind))
32
+ continue;
33
+ layouts.push({
34
+ elementId: element.id,
35
+ pageIndex: element.pageIndex,
36
+ lines: linesOf(pdfium, scan, own),
37
+ });
38
+ }
39
+ return layouts;
40
+ }
41
+ /** The caret position nearest to a page-space point, or none without text. */
42
+ export function positionIn(pdfium, scan, point) {
43
+ const glyphs = glyphsOf(pdfium, scan, () => true);
44
+ if (glyphs.length === 0)
45
+ return undefined;
46
+ const user = pageToUser(scan.geometry, point.x, point.y);
47
+ const hit = pdfium.lib.FPDFText_GetCharIndexAtPos(scan.textPage, user.x, user.y, HIT_TOLERANCE, HIT_TOLERANCE);
48
+ let glyph = glyphs.find((entry) => entry.index === hit);
49
+ if (!glyph) {
50
+ let best = Number.POSITIVE_INFINITY;
51
+ for (const entry of glyphs) {
52
+ const distance = distanceToRect(point, entry.box);
53
+ if (distance < best) {
54
+ best = distance;
55
+ glyph = entry;
56
+ }
57
+ }
58
+ }
59
+ // Past the glyph's middle along the reading direction, the caret goes after it.
60
+ const after = alongReading(point, glyph.box, scan.geometry.rotation) > 0.5;
61
+ return {
62
+ elementId: glyph.position.elementId,
63
+ offset: glyph.position.offset + (after ? 1 : 0),
64
+ };
65
+ }
66
+ /** The rectangles a range covers: one per line fragment, in reading order. */
67
+ export function rectsOf(pdfium, scan, range) {
68
+ const order = scan.elements.map((element) => element.id);
69
+ const first = order.indexOf(range.start.elementId);
70
+ const last = order.indexOf(range.end.elementId);
71
+ if (first < 0 || last < 0 || first > last)
72
+ return [];
73
+ const glyphs = glyphsOf(pdfium, scan, (position) => {
74
+ const at = order.indexOf(position.elementId);
75
+ if (at < first || at > last)
76
+ return false;
77
+ const from = at === first ? range.start.offset : 0;
78
+ const to = at === last ? range.end.offset : Number.POSITIVE_INFINITY;
79
+ return position.offset >= from && position.offset < to;
80
+ });
81
+ return linesOf(pdfium, scan, glyphs).map((line) => line.bounds);
82
+ }
83
+ function glyphsOf(pdfium, scan, keep) {
84
+ const { lib } = pdfium;
85
+ const { textPage, geometry, offsets } = scan;
86
+ const glyphs = [];
87
+ for (let index = 0; index < offsets.length; index += 1) {
88
+ const position = offsets[index];
89
+ if (!position || !keep(position))
90
+ continue;
91
+ const object = lib.FPDFText_GetTextObject(textPage, index);
92
+ const tight = pdfium.readNumbers(4, "double", ([l, r, b, t]) => lib.FPDFText_GetCharBox(textPage, index, l, r, b, t));
93
+ // FS_RECTF: left, top, right, bottom.
94
+ const loose = pdfium.readNumbers(4, "float", ([pointer]) => lib.FPDFText_GetLooseCharBox(textPage, index, pointer));
95
+ const origin = pdfium.readNumbers(2, "double", ([x, y]) => lib.FPDFText_GetCharOrigin(textPage, index, x, y));
96
+ if (!loose || !origin)
97
+ continue;
98
+ const [looseLeft, looseTop, looseRight, looseBottom] = loose;
99
+ const [left, right, bottom, top] = tight && tight[0] !== tight[1] && tight[2] !== tight[3]
100
+ ? tight
101
+ : [looseLeft, looseRight, looseBottom, looseTop];
102
+ glyphs.push({
103
+ index,
104
+ object,
105
+ position,
106
+ char: String.fromCodePoint(lib.FPDFText_GetUnicode(textPage, index)),
107
+ box: roundRect(userRectToPage(geometry, left, bottom, right, top)),
108
+ advance: round(Math.abs(looseRight - looseLeft)),
109
+ origin: roundPoint(userToPage(geometry, origin[0], origin[1])),
110
+ });
111
+ }
112
+ return glyphs;
113
+ }
114
+ /** Groups glyphs into lines, one per text object, in drawing order. */
115
+ function linesOf(pdfium, scan, glyphs) {
116
+ const byObject = new Map();
117
+ for (const glyph of glyphs) {
118
+ const line = byObject.get(glyph.object);
119
+ if (line)
120
+ line.push(glyph);
121
+ else
122
+ byObject.set(glyph.object, [glyph]);
123
+ }
124
+ const lines = [];
125
+ for (const object of scan.byObject.keys()) {
126
+ const members = byObject.get(object);
127
+ if (!members)
128
+ continue;
129
+ members.sort((a, b) => a.position.offset - b.position.offset);
130
+ const style = textStyle(pdfium, object);
131
+ const { elementId } = members[0].position;
132
+ lines.push({
133
+ range: {
134
+ start: { elementId, offset: members[0].position.offset },
135
+ end: { elementId, offset: members.at(-1).position.offset + 1 },
136
+ },
137
+ text: members.map((glyph) => glyph.char).join(""),
138
+ bounds: roundRect(unionRects(members.map((glyph) => glyph.box))),
139
+ baseline: members[0].origin,
140
+ glyphs: members.map(({ position, box, advance }) => ({
141
+ offset: position.offset,
142
+ box,
143
+ advance,
144
+ })),
145
+ fontFamily: style.fontFamily,
146
+ fontSize: style.fontSize,
147
+ color: style.color,
148
+ });
149
+ }
150
+ return lines;
151
+ }
152
+ /** Where a point falls along a glyph in reading direction, 0 before it to 1 after. */
153
+ function alongReading(point, box, rotation) {
154
+ const dx = box.width > 0 ? (point.x - box.x) / box.width : 0.5;
155
+ const dy = box.height > 0 ? (point.y - box.y) / box.height : 0.5;
156
+ // Page rotation turns the reading direction: 0 → right, 1 → down, 2 → left, 3 → up.
157
+ switch (rotation) {
158
+ case 1:
159
+ return dy;
160
+ case 2:
161
+ return 1 - dx;
162
+ case 3:
163
+ return 1 - dy;
164
+ default:
165
+ return dx;
166
+ }
167
+ }
168
+ function distanceToRect(point, rect) {
169
+ if (rectContains(rect, point))
170
+ return 0;
171
+ const dx = Math.max(rect.x - point.x, 0, point.x - (rect.x + rect.width));
172
+ const dy = Math.max(rect.y - point.y, 0, point.y - (rect.y + rect.height));
173
+ return Math.hypot(dx, dy);
174
+ }
175
+ function roundPoint(point) {
176
+ return { x: round(point.x), y: round(point.y) };
177
+ }
@@ -0,0 +1,67 @@
1
+ import type { ResourceLimits, ViewerWarning } from "../../../contracts.js";
2
+ import type { AssetSource } from "../../assets.js";
3
+ import type { ImageCache } from "./images.js";
4
+ import type { OperationIssue } from "../../types.js";
5
+ import type { PdfElement, PdfOperation } from "../types.js";
6
+ import type { PageGeometry } from "./geometry.js";
7
+ import type { ObjectRecord } from "./elements.js";
8
+ import type { Pdfium } from "./pdfium.js";
9
+ import type { FontLibrary, TextMeasurer } from "./fonts.js";
10
+ /** What an operation sees of the document while validating or applying. */
11
+ export interface OperationContext {
12
+ readonly pdfium: Pdfium;
13
+ readonly document: number;
14
+ readonly measurer: TextMeasurer;
15
+ readonly fonts: FontLibrary;
16
+ readonly images: ImageCache;
17
+ readonly limits: ResourceLimits;
18
+ /** Bytes behind `asset:` references. */
19
+ readonly assets: AssetSource;
20
+ readonly pageCount: number;
21
+ /** Geometry of a page; loads it if needed. */
22
+ geometry(pageIndex: number): PageGeometry;
23
+ /** Element id for something this operation creates. */
24
+ newId(pageIndex: number, suffix?: string): string;
25
+ /** Loads a page for writing; its content is regenerated afterwards. */
26
+ withPage<T>(pageIndex: number, use: (page: number) => T): T;
27
+ /** Records objects appended to a page by this operation. */
28
+ appendObjects(pageIndex: number, records: readonly ObjectRecord[]): void;
29
+ /** Where an element's objects sit: their page and their indexes in drawing order. */
30
+ locate(id: string): ElementLocation | undefined;
31
+ /** The element as a query would return it. */
32
+ element(id: string): PdfElement | undefined;
33
+ /** Ids of every element on a page, for what a page deletion removes. */
34
+ pageElementIds(pageIndex: number): readonly string[];
35
+ /** Displayed size of a page in points. */
36
+ pageSize(pageIndex: number): {
37
+ readonly width: number;
38
+ readonly height: number;
39
+ };
40
+ /** Records a page created at `index`; returns its stable key. */
41
+ insertPageRecord(index: number): string;
42
+ removePageRecord(index: number): void;
43
+ movePageRecord(from: number, to: number): void;
44
+ /** Drops what the model cached about a page after its geometry changed. */
45
+ invalidatePage(index: number): void;
46
+ /** Replaces `count` object records from `start` with `records`. */
47
+ spliceObjects(pageIndex: number, start: number, count: number, records: readonly ObjectRecord[]): void;
48
+ }
49
+ export interface ElementLocation {
50
+ readonly pageIndex: number;
51
+ /** Indexes of the element's objects in the page's drawing order, ascending. */
52
+ readonly indexes: readonly number[];
53
+ readonly record: ObjectRecord;
54
+ }
55
+ export interface OperationResult {
56
+ readonly createdIds: readonly string[];
57
+ /** Ids the operation made disappear. */
58
+ readonly removedIds?: readonly string[];
59
+ readonly changedPages: readonly number[];
60
+ readonly warnings: readonly ViewerWarning[];
61
+ }
62
+ export interface OperationHandler<T extends PdfOperation = PdfOperation> {
63
+ validate(operation: T, context: OperationContext, issue: (path: string, code: string, message: string) => void): void;
64
+ apply(operation: T, context: OperationContext): OperationResult;
65
+ }
66
+ export type Issue = (path: string, code: string, message: string) => void;
67
+ export declare function issueCollector(operationIndex: number, issues: OperationIssue[]): Issue;
@@ -0,0 +1,3 @@
1
+ export function issueCollector(operationIndex, issues) {
2
+ return (path, code, message) => issues.push({ operationIndex, path, code, message });
3
+ }
@@ -0,0 +1,6 @@
1
+ import type { DeletePageOperation, InsertPageOperation, MovePageOperation, RotatePageOperation } from "../types.js";
2
+ import type { OperationHandler } from "./operations.js";
3
+ export declare const insertPage: OperationHandler<InsertPageOperation>;
4
+ export declare const deletePage: OperationHandler<DeletePageOperation>;
5
+ export declare const movePage: OperationHandler<MovePageOperation>;
6
+ export declare const rotatePage: OperationHandler<RotatePageOperation>;