@cj-tech-master/excelts 9.1.0 → 9.2.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 (147) hide show
  1. package/README.md +16 -1
  2. package/dist/browser/modules/archive/compression/crc32.js +1 -1
  3. package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
  4. package/dist/browser/modules/archive/crypto/aes.js +1 -20
  5. package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
  6. package/dist/browser/modules/archive/crypto/index.js +3 -1
  7. package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
  8. package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
  9. package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
  10. package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
  11. package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
  12. package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
  13. package/dist/browser/modules/excel/utils/encryptor.js +4 -7
  14. package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
  15. package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
  16. package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
  17. package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
  18. package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
  19. package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
  20. package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
  21. package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
  22. package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
  23. package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
  24. package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
  25. package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
  26. package/dist/browser/modules/pdf/core/encryption.js +8 -7
  27. package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
  28. package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
  29. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
  30. package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
  31. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
  32. package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
  33. package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
  34. package/dist/browser/modules/pdf/core/pdfa.js +261 -0
  35. package/dist/browser/modules/pdf/index.d.ts +11 -0
  36. package/dist/browser/modules/pdf/index.js +9 -0
  37. package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  38. package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
  39. package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
  40. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
  41. package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
  42. package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
  43. package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
  44. package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
  45. package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
  46. package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
  47. package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
  48. package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
  49. package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
  50. package/dist/browser/modules/pdf/render/style-converter.js +1 -1
  51. package/dist/browser/modules/pdf/types.d.ts +14 -1
  52. package/dist/browser/modules/stream/browser/readable.js +8 -2
  53. package/dist/browser/utils/crypto.browser.d.ts +64 -0
  54. package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  55. package/dist/browser/utils/crypto.d.ts +97 -0
  56. package/dist/browser/utils/crypto.js +209 -0
  57. package/dist/cjs/modules/archive/compression/crc32.js +1 -1
  58. package/dist/cjs/modules/archive/crypto/aes.js +2 -23
  59. package/dist/cjs/modules/archive/crypto/index.js +3 -1
  60. package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
  61. package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
  62. package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
  63. package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
  64. package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
  65. package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
  66. package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
  67. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
  68. package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
  69. package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
  70. package/dist/cjs/modules/pdf/core/encryption.js +8 -7
  71. package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
  72. package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
  73. package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
  74. package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
  75. package/dist/cjs/modules/pdf/index.js +19 -1
  76. package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
  77. package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
  78. package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
  79. package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
  80. package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
  81. package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
  82. package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
  83. package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
  84. package/dist/cjs/modules/stream/browser/readable.js +8 -2
  85. package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
  86. package/dist/cjs/utils/crypto.js +228 -0
  87. package/dist/esm/modules/archive/compression/crc32.js +1 -1
  88. package/dist/esm/modules/archive/crypto/aes.js +1 -20
  89. package/dist/esm/modules/archive/crypto/index.js +3 -1
  90. package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
  91. package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
  92. package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
  93. package/dist/esm/modules/excel/utils/encryptor.js +4 -7
  94. package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
  95. package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
  96. package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
  97. package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
  98. package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
  99. package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
  100. package/dist/esm/modules/pdf/core/encryption.js +8 -7
  101. package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
  102. package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
  103. package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
  104. package/dist/esm/modules/pdf/core/pdfa.js +261 -0
  105. package/dist/esm/modules/pdf/index.js +9 -0
  106. package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
  107. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
  108. package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
  109. package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
  110. package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
  111. package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
  112. package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
  113. package/dist/esm/modules/pdf/render/style-converter.js +1 -1
  114. package/dist/esm/modules/stream/browser/readable.js +8 -2
  115. package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  116. package/dist/esm/utils/crypto.js +209 -0
  117. package/dist/iife/excelts.iife.js +1248 -1074
  118. package/dist/iife/excelts.iife.js.map +1 -1
  119. package/dist/iife/excelts.iife.min.js +53 -54
  120. package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
  121. package/dist/types/modules/archive/crypto/index.d.ts +2 -1
  122. package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
  123. package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
  124. package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
  125. package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
  126. package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
  127. package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
  128. package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
  129. package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
  130. package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
  131. package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
  132. package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
  133. package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
  134. package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
  135. package/dist/types/modules/pdf/index.d.ts +11 -0
  136. package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  137. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
  138. package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
  139. package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
  140. package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
  141. package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
  142. package/dist/types/modules/pdf/types.d.ts +14 -1
  143. package/dist/types/utils/crypto.browser.d.ts +64 -0
  144. package/dist/types/utils/crypto.d.ts +97 -0
  145. package/package.json +110 -111
  146. package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
  147. package/dist/types/modules/pdf/core/crypto.d.ts +0 -65
@@ -0,0 +1,1612 @@
1
+ "use strict";
2
+ /**
3
+ * PDF editor — modify existing PDF documents.
4
+ *
5
+ * Supports:
6
+ * - Adding new pages with free-form content
7
+ * - Adding text/shapes/images to existing pages (overlay)
8
+ * - Filling form fields (AcroForm)
9
+ * - Copying pages from other PDFs (merge)
10
+ * - Preserving page properties (Rotate, CropBox, etc.) and metadata
11
+ *
12
+ * Note: save() rebuilds the PDF from scratch rather than using incremental
13
+ * updates. This is simpler and more reliable but means object numbers change
14
+ * and existing digital signatures will be invalidated.
15
+ *
16
+ * @example Edit an existing PDF:
17
+ * ```typescript
18
+ * import { PdfEditor } from "@cj-tech-master/excelts/pdf";
19
+ *
20
+ * const editor = PdfEditor.load(existingPdfBytes);
21
+ * editor.getPage(0).drawText("APPROVED", { x: 200, y: 400, fontSize: 48, color: { r: 0, g: 0.5, b: 0 } });
22
+ * editor.setFormField("name", "John Doe");
23
+ * const result = await editor.save();
24
+ * ```
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.PdfEditor = exports.PdfEditorPage = void 0;
61
+ const pdf_document_1 = require("../reader/pdf-document");
62
+ const pdf_writer_1 = require("../core/pdf-writer");
63
+ const pdf_object_1 = require("../core/pdf-object");
64
+ const font_manager_1 = require("../font/font-manager");
65
+ const ttf_parser_1 = require("../font/ttf-parser");
66
+ const pdf_decrypt_1 = require("../reader/pdf-decrypt");
67
+ const form_extractor_1 = require("../reader/form-extractor");
68
+ const metadata_reader_1 = require("../reader/metadata-reader");
69
+ const pdf_parser_1 = require("../reader/pdf-parser");
70
+ const document_builder_1 = require("./document-builder");
71
+ const errors_1 = require("../errors");
72
+ const image_utils_1 = require("./image-utils");
73
+ const form_appearance_1 = require("./form-appearance");
74
+ const resource_merger_1 = require("./resource-merger");
75
+ // =============================================================================
76
+ // PdfEditorPage
77
+ // =============================================================================
78
+ /**
79
+ * Proxy for an existing page that allows overlaying new content.
80
+ * New content is drawn on top of existing content via a separate content stream.
81
+ */
82
+ class PdfEditorPage {
83
+ /** @internal */
84
+ constructor(pageIndex, width, height, fontManager) {
85
+ this._pageIndex = pageIndex;
86
+ this._width = width;
87
+ this._height = height;
88
+ this._overlay = new document_builder_1.PdfPageBuilder(width, height, fontManager);
89
+ }
90
+ /** Page width in points. */
91
+ get width() {
92
+ return this._width;
93
+ }
94
+ /** Page height in points. */
95
+ get height() {
96
+ return this._height;
97
+ }
98
+ /**
99
+ * Measure the width of a text string in points.
100
+ */
101
+ measureText(text, options) {
102
+ return this._overlay.measureText(text, options);
103
+ }
104
+ /**
105
+ * Draw text on this existing page (overlaid on top).
106
+ */
107
+ drawText(text, options) {
108
+ this._overlay.drawText(text, options);
109
+ return this;
110
+ }
111
+ /**
112
+ * Draw a rectangle on this existing page.
113
+ */
114
+ drawRect(options) {
115
+ this._overlay.drawRect(options);
116
+ return this;
117
+ }
118
+ /**
119
+ * Draw a circle on this existing page.
120
+ */
121
+ drawCircle(options) {
122
+ this._overlay.drawCircle(options);
123
+ return this;
124
+ }
125
+ /**
126
+ * Draw an ellipse on this existing page.
127
+ */
128
+ drawEllipse(options) {
129
+ this._overlay.drawEllipse(options);
130
+ return this;
131
+ }
132
+ /**
133
+ * Draw a line on this existing page.
134
+ */
135
+ drawLine(options) {
136
+ this._overlay.drawLine(options);
137
+ return this;
138
+ }
139
+ /**
140
+ * Draw an image on this existing page.
141
+ */
142
+ drawImage(options) {
143
+ this._overlay.drawImage(options);
144
+ return this;
145
+ }
146
+ /**
147
+ * Get the raw overlay content stream.
148
+ */
149
+ getContentStream() {
150
+ return this._overlay._stream;
151
+ }
152
+ /**
153
+ * Add an annotation to this existing page (Highlight, Text, FreeText, Stamp, etc.).
154
+ */
155
+ addAnnotation(options) {
156
+ this._overlay.addAnnotation(options);
157
+ return this;
158
+ }
159
+ /**
160
+ * Add a form field to this existing page.
161
+ */
162
+ addFormField(options) {
163
+ this._overlay.addFormField(options);
164
+ return this;
165
+ }
166
+ /**
167
+ * Draw an SVG path on this existing page.
168
+ */
169
+ drawSvgPath(d, options) {
170
+ this._overlay.drawSvgPath(d, options);
171
+ return this;
172
+ }
173
+ /**
174
+ * Draw a complex path from a list of path operations.
175
+ */
176
+ drawPath(ops, options) {
177
+ this._overlay.drawPath(ops, options);
178
+ return this;
179
+ }
180
+ /** @internal */
181
+ _hasOverlay() {
182
+ return (this._overlay._stream.toString().length > 0 ||
183
+ this._overlay._images.length > 0 ||
184
+ this._overlay._builderAnnotations.length > 0 ||
185
+ this._overlay._formFields.length > 0);
186
+ }
187
+ }
188
+ exports.PdfEditorPage = PdfEditorPage;
189
+ // =============================================================================
190
+ // PdfEditor
191
+ // =============================================================================
192
+ /**
193
+ * Editor for modifying existing PDF documents.
194
+ *
195
+ * Load an existing PDF, overlay content on existing pages, fill form fields,
196
+ * add new pages, copy pages from other documents, and save.
197
+ */
198
+ class PdfEditor {
199
+ constructor(data, password) {
200
+ this._pages = [];
201
+ this._newPages = [];
202
+ this._fontManager = new font_manager_1.FontManager();
203
+ this._formFieldUpdates = new Map();
204
+ this._copiedPages = [];
205
+ /** @internal - Indices of original pages to remove on save */
206
+ this._removedPageIndices = new Set();
207
+ /** @internal - True during saveIncremental() to preserve original refs */
208
+ this._isIncrementalSave = false;
209
+ /** @internal - Rotation overrides for original pages: index → degrees (0/90/180/270) */
210
+ this._rotationOverrides = new Map();
211
+ this._signaturePlaceholder = null;
212
+ /** @internal - Writer reference during save(), for deep-clone */
213
+ this._writerForSave = null;
214
+ /** @internal - Cache of cloned indirect refs: "objNum:gen" → new objNum in writer */
215
+ this._clonedRefs = new Map();
216
+ this._doc = new pdf_document_1.PdfDocument(data);
217
+ this._password = password;
218
+ // Handle encryption
219
+ if ((0, pdf_decrypt_1.isEncrypted)(this._doc)) {
220
+ const success = (0, pdf_decrypt_1.initDecryption)(this._doc, password);
221
+ if (!success) {
222
+ throw new errors_1.PdfStructureError("Failed to decrypt PDF: incorrect password");
223
+ }
224
+ }
225
+ // Initialize page proxies
226
+ const pagesInfo = this._doc.getPagesWithObjInfo();
227
+ for (let i = 0; i < pagesInfo.length; i++) {
228
+ const { dict } = pagesInfo[i];
229
+ const dims = this._doc.resolvePageBox(dict) ?? {
230
+ width: 612,
231
+ height: 792
232
+ };
233
+ this._pages.push(new PdfEditorPage(i, dims.width, dims.height, this._fontManager));
234
+ }
235
+ }
236
+ /**
237
+ * Load a PDF for editing.
238
+ *
239
+ * @param data - Raw PDF file bytes
240
+ * @param options - Load options (e.g., password)
241
+ * @returns A PdfEditor instance
242
+ */
243
+ static load(data, options) {
244
+ return new PdfEditor(data, options?.password ?? "");
245
+ }
246
+ /** Number of existing pages. */
247
+ get pageCount() {
248
+ return this._pages.length;
249
+ }
250
+ /**
251
+ * Get an existing page for editing (overlaying content).
252
+ *
253
+ * @param index - 0-based page index
254
+ */
255
+ getPage(index) {
256
+ if (index < 0 || index >= this._pages.length) {
257
+ throw new errors_1.PdfStructureError(`Page index ${index} out of range (0-${this._pages.length - 1})`);
258
+ }
259
+ return this._pages[index];
260
+ }
261
+ /**
262
+ * Add a new blank page to the end of the document.
263
+ */
264
+ addPage(options) {
265
+ const width = options?.width ?? 595.28;
266
+ const height = options?.height ?? 841.89;
267
+ const page = new document_builder_1.PdfPageBuilder(width, height, this._fontManager);
268
+ this._newPages.push(page);
269
+ return page;
270
+ }
271
+ /**
272
+ * Remove a page from the document.
273
+ *
274
+ * @param index - 0-based page index (of original pages only)
275
+ */
276
+ removePage(index) {
277
+ if (index < 0 || index >= this._pages.length) {
278
+ throw new errors_1.PdfStructureError(`Page index ${index} out of range (0-${this._pages.length - 1})`);
279
+ }
280
+ this._removedPageIndices.add(index);
281
+ return this;
282
+ }
283
+ /**
284
+ * Set the rotation of an existing page.
285
+ *
286
+ * @param index - 0-based page index (of original pages only)
287
+ * @param degrees - Rotation in degrees (must be 0, 90, 180, or 270)
288
+ */
289
+ rotatePage(index, degrees) {
290
+ if (index < 0 || index >= this._pages.length) {
291
+ throw new errors_1.PdfStructureError(`Page index ${index} out of range (0-${this._pages.length - 1})`);
292
+ }
293
+ if (degrees !== 0 && degrees !== 90 && degrees !== 180 && degrees !== 270) {
294
+ throw new errors_1.PdfStructureError(`Invalid rotation ${degrees}: must be 0, 90, 180, or 270`);
295
+ }
296
+ this._rotationOverrides.set(index, degrees);
297
+ return this;
298
+ }
299
+ /**
300
+ * Split the document: save each page (or a subset) as a separate PDF.
301
+ *
302
+ * @param pageIndices - 0-based page indices to extract. Omit to split all pages.
303
+ * @returns Array of Uint8Array, one per requested page.
304
+ */
305
+ async splitPages(pageIndices) {
306
+ const pagesInfo = this._doc.getPagesWithObjInfo();
307
+ const indices = pageIndices ?? Array.from({ length: pagesInfo.length }, (_, i) => i);
308
+ const results = [];
309
+ for (const idx of indices) {
310
+ if (idx < 0 || idx >= pagesInfo.length) {
311
+ continue;
312
+ }
313
+ // Create a single-page editor from original bytes and save it
314
+ const singlePageEditor = PdfEditor.load(this._doc.data, {
315
+ password: this._password
316
+ });
317
+ // Remove all pages except the one we want
318
+ for (let i = 0; i < pagesInfo.length; i++) {
319
+ if (i !== idx) {
320
+ singlePageEditor.removePage(i);
321
+ }
322
+ }
323
+ results.push(await singlePageEditor.save());
324
+ }
325
+ return results;
326
+ }
327
+ /**
328
+ * Embed a TrueType font for Unicode/CJK support.
329
+ */
330
+ embedFont(fontBytes) {
331
+ const ttfFont = (0, ttf_parser_1.parseTtf)(fontBytes);
332
+ this._fontManager.registerEmbeddedFont(ttfFont);
333
+ return this;
334
+ }
335
+ // ===========================================================================
336
+ // Form Fields
337
+ // ===========================================================================
338
+ /**
339
+ * Set the value of a form field.
340
+ * The field is identified by its fully qualified name (e.g., "form.address.city").
341
+ *
342
+ * @param fieldName - Fully qualified field name
343
+ * @param value - New value to set
344
+ */
345
+ setFormField(fieldName, value) {
346
+ this._formFieldUpdates.set(fieldName, value);
347
+ return this;
348
+ }
349
+ /**
350
+ * Set multiple form field values at once.
351
+ *
352
+ * @param fields - Object mapping field names to values
353
+ */
354
+ setFormFields(fields) {
355
+ for (const [name, value] of Object.entries(fields)) {
356
+ this._formFieldUpdates.set(name, value);
357
+ }
358
+ return this;
359
+ }
360
+ /**
361
+ * Get current form fields (before any modifications).
362
+ */
363
+ getFormFields() {
364
+ return (0, form_extractor_1.extractFormFields)(this._doc);
365
+ }
366
+ // ===========================================================================
367
+ // Page Copy / Merge
368
+ // ===========================================================================
369
+ /**
370
+ * Copy pages from another PDF document into this document.
371
+ *
372
+ * @param sourcePdf - Raw bytes of the source PDF
373
+ * @param pageIndices - 0-based page indices to copy. Omit to copy all pages.
374
+ * @param options - Load options for the source PDF (e.g., password)
375
+ */
376
+ copyPagesFrom(sourcePdf, pageIndices, options) {
377
+ const sourceDoc = new pdf_document_1.PdfDocument(sourcePdf);
378
+ if ((0, pdf_decrypt_1.isEncrypted)(sourceDoc)) {
379
+ const success = (0, pdf_decrypt_1.initDecryption)(sourceDoc, options?.password ?? "");
380
+ if (!success) {
381
+ throw new errors_1.PdfStructureError("Failed to decrypt source PDF for page copy");
382
+ }
383
+ }
384
+ const sourcePagesInfo = sourceDoc.getPagesWithObjInfo();
385
+ const indices = pageIndices ?? Array.from({ length: sourcePagesInfo.length }, (_, i) => i);
386
+ for (const idx of indices) {
387
+ if (idx < 0 || idx >= sourcePagesInfo.length) {
388
+ continue;
389
+ }
390
+ const { dict: pageDict } = sourcePagesInfo[idx];
391
+ const dims = sourceDoc.resolvePageBox(pageDict) ?? {
392
+ width: 612,
393
+ height: 792
394
+ };
395
+ // Collect all content streams from the source page
396
+ const contentStreams = this._collectContentStreams(sourceDoc, pageDict);
397
+ this._copiedPages.push({
398
+ width: dims.width,
399
+ height: dims.height,
400
+ contentStreams,
401
+ sourceDoc,
402
+ sourcePageDict: pageDict
403
+ });
404
+ }
405
+ return this;
406
+ }
407
+ // ===========================================================================
408
+ // Save
409
+ // ===========================================================================
410
+ /**
411
+ * Save the modified PDF.
412
+ *
413
+ * Rebuilds the PDF from scratch — content streams, resources, and page
414
+ * properties are deep-cloned into a new document. Original metadata and
415
+ * XMP streams are preserved. Digital signatures will be invalidated.
416
+ *
417
+ * @returns The modified PDF as Uint8Array
418
+ */
419
+ async save() {
420
+ // Rebuild the PDF (not incremental update — simpler and more reliable)
421
+ const writer = new pdf_writer_1.PdfWriter();
422
+ this._writerForSave = writer;
423
+ this._clonedRefs = new Map();
424
+ try {
425
+ return await this._buildFullSave(writer);
426
+ }
427
+ finally {
428
+ this._writerForSave = null;
429
+ this._clonedRefs.clear();
430
+ }
431
+ }
432
+ /** @internal Full rebuild implementation, extracted for try/finally cleanup. */
433
+ async _buildFullSave(writer) {
434
+ // Write font resources for any overlay content
435
+ const fontObjectMap = this._fontManager.writeFontResources(writer);
436
+ const fontDictStr = this._fontManager.buildFontDictString(fontObjectMap);
437
+ const pagesTreeObjNum = writer.allocObject();
438
+ const pageObjNums = [];
439
+ // Re-emit existing pages
440
+ const pagesInfo = this._doc.getPagesWithObjInfo();
441
+ for (let i = 0; i < pagesInfo.length; i++) {
442
+ // Skip removed pages
443
+ if (this._removedPageIndices.has(i)) {
444
+ continue;
445
+ }
446
+ const { dict: pageDict } = pagesInfo[i];
447
+ const dims = this._doc.resolvePageBox(pageDict) ?? {
448
+ width: 612,
449
+ height: 792
450
+ };
451
+ const editorPage = this._pages[i];
452
+ // Get original content streams
453
+ const originalStreams = this._collectContentStreams(this._doc, pageDict);
454
+ const originalResources = this._serializeResources(this._doc, pageDict);
455
+ // Combine original + overlay content
456
+ const allContentRefs = [];
457
+ // Write original content streams
458
+ for (const streamData of originalStreams) {
459
+ const objNum = writer.allocObject();
460
+ writer.addStreamObject(objNum, new pdf_object_1.PdfDict(), streamData);
461
+ allContentRefs.push(objNum);
462
+ }
463
+ // Write overlay content stream if present
464
+ let overlayResourcesStr = "";
465
+ if (editorPage._hasOverlay()) {
466
+ const overlayObjNum = writer.allocObject();
467
+ writer.addStreamObject(overlayObjNum, new pdf_object_1.PdfDict(), editorPage._overlay._stream);
468
+ allContentRefs.push(overlayObjNum);
469
+ // Build overlay-specific resources (fonts, images) as structured dict
470
+ const imageXObjectMap = this._writeOverlayImages(writer, editorPage._overlay);
471
+ const overlayDict = this._buildOverlayResourceDict(fontDictStr, imageXObjectMap);
472
+ overlayResourcesStr = (0, resource_merger_1.serializeResourceDict)(overlayDict);
473
+ }
474
+ // Write resources (merge original + overlay)
475
+ const resourcesObjNum = writer.allocObject();
476
+ const mergedResources = this._mergeResourceStrings(originalResources, overlayResourcesStr);
477
+ writer.addObject(resourcesObjNum, mergedResources || "<< >>");
478
+ // Write page dict
479
+ const contentsStr = allContentRefs.length === 1
480
+ ? (0, pdf_object_1.pdfRef)(allContentRefs[0])
481
+ : `[${allContentRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ")}]`;
482
+ // Apply form field updates to annotations
483
+ const annotRefs = this._writeFormFieldUpdates(writer, pageDict, i);
484
+ // Write overlay builder annotations (Highlight, Text, FreeText, Stamp, etc.)
485
+ for (const annot of editorPage._overlay._builderAnnotations) {
486
+ const annotObjNum = writer.allocObject();
487
+ const rect = `[${(0, pdf_object_1.pdfNumber)(annot.rect[0])} ${(0, pdf_object_1.pdfNumber)(annot.rect[1])} ${(0, pdf_object_1.pdfNumber)(annot.rect[2])} ${(0, pdf_object_1.pdfNumber)(annot.rect[3])}]`;
488
+ const annotDict = new pdf_object_1.PdfDict()
489
+ .set("Type", "/Annot")
490
+ .set("Subtype", `/${annot.subtype}`)
491
+ .set("Rect", rect)
492
+ .set("F", "4");
493
+ for (const [key, value] of annot.entries) {
494
+ annotDict.set(key, value);
495
+ }
496
+ writer.addObject(annotObjNum, annotDict);
497
+ annotRefs.push(annotObjNum);
498
+ }
499
+ // Write overlay form fields
500
+ for (const field of editorPage._overlay._formFields) {
501
+ if (field.options.type === "radio") {
502
+ // Radio group: parent field + child widgets
503
+ const parentObjNum = writer.allocObject();
504
+ const childRefs = [];
505
+ let ff = 1 << 15; // Radio
506
+ ff |= 1 << 14; // NoToggleToOff
507
+ if (field.options.readOnly) {
508
+ ff |= 1;
509
+ }
510
+ if (field.options.required) {
511
+ ff |= 1 << 1;
512
+ }
513
+ for (const btn of field.options.buttons) {
514
+ const childObjNum = writer.allocObject();
515
+ const rect = `[${btn.rect.map(v => (0, pdf_object_1.pdfNumber)(v)).join(" ")}]`;
516
+ const isSelected = field.options.selected === btn.value;
517
+ const apState = isSelected ? `/${btn.value}` : "/Off";
518
+ const childDict = new pdf_object_1.PdfDict()
519
+ .set("Type", "/Annot")
520
+ .set("Subtype", "/Widget")
521
+ .set("Rect", rect)
522
+ .set("Parent", (0, pdf_object_1.pdfRef)(parentObjNum))
523
+ .set("AS", apState)
524
+ .set("AP", `<< /N << /${btn.value} null /Off null >> >>`);
525
+ writer.addObject(childObjNum, childDict);
526
+ childRefs.push(childObjNum);
527
+ annotRefs.push(childObjNum); // Children go into page /Annots
528
+ }
529
+ const parentDict = new pdf_object_1.PdfDict()
530
+ .set("FT", "/Btn")
531
+ .set("T", (0, pdf_object_1.pdfString)(field.options.name))
532
+ .set("Ff", String(ff))
533
+ .set("Kids", `[${childRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ")}]`);
534
+ if (field.options.selected) {
535
+ parentDict.set("V", `/${field.options.selected}`);
536
+ }
537
+ writer.addObject(parentObjNum, parentDict);
538
+ // Parent does NOT go into annotRefs (not a visual annotation)
539
+ }
540
+ else {
541
+ // Single-widget fields: text, checkbox, dropdown
542
+ const fieldObjNum = writer.allocObject();
543
+ const r = field.options.rect;
544
+ const rect = `[${(0, pdf_object_1.pdfNumber)(r[0])} ${(0, pdf_object_1.pdfNumber)(r[1])} ${(0, pdf_object_1.pdfNumber)(r[2])} ${(0, pdf_object_1.pdfNumber)(r[3])}]`;
545
+ const fieldDict = new pdf_object_1.PdfDict()
546
+ .set("Type", "/Annot")
547
+ .set("Subtype", "/Widget")
548
+ .set("Rect", rect)
549
+ .set("T", (0, pdf_object_1.pdfString)(field.options.name))
550
+ .set("FT", field.options.type === "text"
551
+ ? "/Tx"
552
+ : field.options.type === "checkbox"
553
+ ? "/Btn"
554
+ : "/Ch");
555
+ if (field.options.value) {
556
+ fieldDict.set("V", (0, pdf_object_1.pdfString)(field.options.value));
557
+ }
558
+ fieldDict.set("DA", (0, pdf_object_1.pdfString)("/Helv 12 Tf 0 g"));
559
+ writer.addObject(fieldObjNum, fieldDict);
560
+ annotRefs.push(fieldObjNum);
561
+ }
562
+ }
563
+ const pageDict2 = new pdf_object_1.PdfDict()
564
+ .set("Type", "/Page")
565
+ .set("Parent", (0, pdf_object_1.pdfRef)(pagesTreeObjNum))
566
+ .set("MediaBox", `[0 0 ${(0, pdf_object_1.pdfNumber)(dims.width)} ${(0, pdf_object_1.pdfNumber)(dims.height)}]`)
567
+ .set("Contents", contentsStr)
568
+ .set("Resources", (0, pdf_object_1.pdfRef)(resourcesObjNum));
569
+ // Preserve page-level properties from the original page dict
570
+ this._copyPageProperties(pageDict, pageDict2);
571
+ // Apply rotation override if set
572
+ const rotationOverride = this._rotationOverrides.get(i);
573
+ if (rotationOverride !== undefined) {
574
+ if (rotationOverride === 0) {
575
+ // Remove rotation (unset the key)
576
+ pageDict2.delete("Rotate");
577
+ }
578
+ else {
579
+ pageDict2.set("Rotate", String(rotationOverride));
580
+ }
581
+ }
582
+ if (annotRefs.length > 0) {
583
+ pageDict2.set("Annots", `[${annotRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ")}]`);
584
+ }
585
+ const pageObjNum = writer.allocObject();
586
+ writer.addObject(pageObjNum, pageDict2);
587
+ pageObjNums.push(pageObjNum);
588
+ }
589
+ // Write copied pages
590
+ for (const copied of this._copiedPages) {
591
+ const contentRefs = [];
592
+ for (const streamData of copied.contentStreams) {
593
+ const objNum = writer.allocObject();
594
+ writer.addStreamObject(objNum, new pdf_object_1.PdfDict(), streamData);
595
+ contentRefs.push(objNum);
596
+ }
597
+ // Deep-clone resources from the source document
598
+ const resourcesStr = this._serializeResources(copied.sourceDoc, copied.sourcePageDict);
599
+ const resourcesObjNum = writer.allocObject();
600
+ writer.addObject(resourcesObjNum, resourcesStr || "<< >>");
601
+ const contentsStr = contentRefs.length === 1
602
+ ? (0, pdf_object_1.pdfRef)(contentRefs[0])
603
+ : `[${contentRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ")}]`;
604
+ const pageDict = new pdf_object_1.PdfDict()
605
+ .set("Type", "/Page")
606
+ .set("Parent", (0, pdf_object_1.pdfRef)(pagesTreeObjNum))
607
+ .set("MediaBox", `[0 0 ${(0, pdf_object_1.pdfNumber)(copied.width)} ${(0, pdf_object_1.pdfNumber)(copied.height)}]`)
608
+ .set("Contents", contentsStr)
609
+ .set("Resources", (0, pdf_object_1.pdfRef)(resourcesObjNum));
610
+ // Preserve page properties from source page
611
+ this._copyPageProperties(copied.sourcePageDict, pageDict, copied.sourceDoc);
612
+ const pageObjNum = writer.allocObject();
613
+ writer.addObject(pageObjNum, pageDict);
614
+ pageObjNums.push(pageObjNum);
615
+ }
616
+ // Write new pages
617
+ for (const page of this._newPages) {
618
+ const imageXObjectMap = this._writeOverlayImages(writer, page);
619
+ let xobjDictStr = "";
620
+ if (imageXObjectMap.size > 0) {
621
+ const entries = [...imageXObjectMap.entries()]
622
+ .map(([name, objNum]) => `/${name} ${(0, pdf_object_1.pdfRef)(objNum)}`)
623
+ .join(" ");
624
+ xobjDictStr = `<< ${entries} >>`;
625
+ }
626
+ const contentObjNum = writer.allocObject();
627
+ writer.addStreamObject(contentObjNum, new pdf_object_1.PdfDict(), page._stream);
628
+ const resourcesObjNum = writer.allocObject();
629
+ let resStr = "<< ";
630
+ if (fontDictStr) {
631
+ resStr += `/Font ${fontDictStr} `;
632
+ }
633
+ if (xobjDictStr) {
634
+ resStr += `/XObject ${xobjDictStr} `;
635
+ }
636
+ resStr += ">>";
637
+ writer.addObject(resourcesObjNum, resStr);
638
+ const pageObjNum = writer.addPage({
639
+ parentRef: pagesTreeObjNum,
640
+ width: page._width,
641
+ height: page._height,
642
+ contentsRef: contentObjNum,
643
+ resourcesRef: resourcesObjNum
644
+ });
645
+ pageObjNums.push(pageObjNum);
646
+ }
647
+ // Pages tree
648
+ const kidsStr = pageObjNums.map(n => (0, pdf_object_1.pdfRef)(n)).join(" ");
649
+ writer.addObject(pagesTreeObjNum, new pdf_object_1.PdfDict()
650
+ .set("Type", "/Pages")
651
+ .set("Kids", `[${kidsStr}]`)
652
+ .set("Count", String(pageObjNums.length)));
653
+ // Catalog — with optional AcroForm
654
+ const catalogObjNum = writer.allocObject();
655
+ const catalogDict = new pdf_object_1.PdfDict().set("Type", "/Catalog").set("Pages", (0, pdf_object_1.pdfRef)(pagesTreeObjNum));
656
+ // If we have form field updates, write an AcroForm dict
657
+ if (this._formFieldUpdates.size > 0) {
658
+ try {
659
+ const catalog = this._doc.getCatalog();
660
+ const acroFormRef = catalog.get("AcroForm");
661
+ if (acroFormRef) {
662
+ const acroFormStr = this._deepSerialize(this._doc, acroFormRef);
663
+ // Insert /NeedAppearances into the cloned dict
664
+ if (acroFormStr && acroFormStr.startsWith("<<")) {
665
+ catalogDict.set("AcroForm", acroFormStr.replace("<<", "<< /NeedAppearances true"));
666
+ }
667
+ else {
668
+ catalogDict.set("AcroForm", "<< /NeedAppearances true >>");
669
+ }
670
+ }
671
+ else {
672
+ catalogDict.set("AcroForm", "<< /NeedAppearances true >>");
673
+ }
674
+ }
675
+ catch {
676
+ catalogDict.set("AcroForm", "<< /NeedAppearances true >>");
677
+ }
678
+ }
679
+ // Preserve XMP metadata stream from original catalog
680
+ try {
681
+ const originalCatalog = this._doc.getCatalog();
682
+ const metadataRef = originalCatalog.get("Metadata");
683
+ if (metadataRef) {
684
+ const clonedRef = this._deepSerialize(this._doc, metadataRef);
685
+ if (clonedRef) {
686
+ catalogDict.set("Metadata", clonedRef);
687
+ }
688
+ }
689
+ }
690
+ catch {
691
+ // If catalog is inaccessible, skip XMP preservation
692
+ }
693
+ writer.addObject(catalogObjNum, catalogDict);
694
+ writer.setCatalog(catalogObjNum);
695
+ // Inject signature placeholder if signing is in progress
696
+ if (this._signaturePlaceholder) {
697
+ const sigDictObjNum = writer.allocObject();
698
+ writer.addObject(sigDictObjNum, this._signaturePlaceholder);
699
+ const sigWidgetObjNum = writer.allocObject();
700
+ const sigWidgetDict = new pdf_object_1.PdfDict()
701
+ .set("Type", "/Annot")
702
+ .set("Subtype", "/Widget")
703
+ .set("FT", "/Sig")
704
+ .set("Rect", "[0 0 0 0]")
705
+ .set("T", (0, pdf_object_1.pdfString)("Signature1"))
706
+ .set("V", (0, pdf_object_1.pdfRef)(sigDictObjNum))
707
+ .set("F", "4");
708
+ writer.addObject(sigWidgetObjNum, sigWidgetDict);
709
+ // Patch catalog to include signature widget in AcroForm with SigFlags
710
+ // Grab existing fields from the catalog dict string representation
711
+ const catalogStr = catalogDict.toString();
712
+ const existingFieldsMatch = catalogStr.match(/\/Fields\s*\[([^\]]*)\]/);
713
+ const existingFields = existingFieldsMatch ? existingFieldsMatch[1].trim() : "";
714
+ const sigFieldRef = (0, pdf_object_1.pdfRef)(sigWidgetObjNum);
715
+ const allFieldRefs = existingFields ? `${existingFields} ${sigFieldRef}` : sigFieldRef;
716
+ catalogDict.set("AcroForm", `<< /Fields [${allFieldRefs}] /SigFlags 3 >>`);
717
+ writer.addObject(catalogObjNum, catalogDict);
718
+ }
719
+ // Info dict — preserve original metadata
720
+ const originalMeta = (0, metadata_reader_1.extractMetadata)(this._doc);
721
+ writer.addInfoDict({
722
+ title: originalMeta.title || undefined,
723
+ author: originalMeta.author || undefined,
724
+ subject: originalMeta.subject || undefined,
725
+ creator: originalMeta.creator || "excelts"
726
+ });
727
+ return writer.build();
728
+ }
729
+ /**
730
+ * Save the modified PDF using incremental update.
731
+ *
732
+ * Appends new/modified objects after the original PDF bytes, preserving the
733
+ * original data byte-for-byte. This is ideal for overlays and form field
734
+ * updates on existing pages — it preserves digital signatures on unmodified
735
+ * content and produces smaller output.
736
+ *
737
+ * Falls back to {@link save} (full rebuild) if structural changes are
738
+ * present (new pages, copied pages, or removed pages).
739
+ *
740
+ * @returns The modified PDF as Uint8Array
741
+ */
742
+ async saveIncremental() {
743
+ // Fall back to full rebuild if structural changes are present
744
+ if (this._newPages.length > 0 ||
745
+ this._copiedPages.length > 0 ||
746
+ this._removedPageIndices.size > 0) {
747
+ return this.save();
748
+ }
749
+ // Fall back to full rebuild if rotation overrides are present
750
+ if (this._rotationOverrides.size > 0) {
751
+ return this.save();
752
+ }
753
+ // Fall back to full rebuild for xref-stream PDFs (no "trailer" keyword)
754
+ const tailBytes = this._doc.data.subarray(Math.max(0, this._doc.data.length - 1024));
755
+ const tailStr = new TextDecoder().decode(tailBytes);
756
+ if (!tailStr.includes("trailer")) {
757
+ return this.save();
758
+ }
759
+ // Check if there are any modifications at all
760
+ const hasOverlays = this._pages.some(p => p._hasOverlay());
761
+ const hasFormUpdates = this._formFieldUpdates.size > 0;
762
+ if (!hasOverlays && !hasFormUpdates) {
763
+ // No modifications — return the original bytes
764
+ return this._doc.data;
765
+ }
766
+ this._isIncrementalSave = true;
767
+ try {
768
+ return await this._buildIncrementalUpdate();
769
+ }
770
+ finally {
771
+ this._isIncrementalSave = false;
772
+ this._writerForSave = null;
773
+ this._clonedRefs.clear();
774
+ }
775
+ }
776
+ /** @internal — Core incremental update logic, separated for try/finally cleanup. */
777
+ async _buildIncrementalUpdate() {
778
+ // Determine the next available object number from the original PDF's /Size
779
+ const originalSize = (0, pdf_parser_1.dictGetNumber)(this._doc.trailer, "Size") ?? 1;
780
+ let nextObjNum = originalSize;
781
+ // Collect modified objects: objNum → serialized content
782
+ const modifiedObjects = new Map();
783
+ // Check what kinds of modifications exist
784
+ const hasOverlays = this._pages.some(p => p._hasOverlay());
785
+ // We need a temporary PdfWriter for font resources used in overlays
786
+ const writer = new pdf_writer_1.PdfWriter();
787
+ this._writerForSave = writer;
788
+ this._clonedRefs = new Map();
789
+ let fontDictStr = "";
790
+ // Map of writer objNum → actual new objNum we'll use in the incremental update
791
+ const writerFontObjRemap = new Map();
792
+ if (hasOverlays) {
793
+ // Write font resources via the writer (to serialize font objects)
794
+ const fontObjectMap = this._fontManager.writeFontResources(writer);
795
+ // Remap all writer-allocated objects (fonts + their dependencies like
796
+ // CID font descriptors, ToUnicode CMaps, etc.) into the incremental
797
+ // update's object number space.
798
+ const writerObjects = writer.getObjects();
799
+ for (const obj of writerObjects) {
800
+ if (!writerFontObjRemap.has(obj.objectNumber)) {
801
+ writerFontObjRemap.set(obj.objectNumber, nextObjNum++);
802
+ }
803
+ }
804
+ // Write all font objects into modifiedObjects with remapped refs
805
+ for (const obj of writerObjects) {
806
+ const newObjNum = writerFontObjRemap.get(obj.objectNumber);
807
+ const remappedContent = this._remapRefsInString(obj.content, writerFontObjRemap);
808
+ if (obj.streamData) {
809
+ // Parse the remapped content back into a PdfDict for stream objects
810
+ modifiedObjects.set(newObjNum, {
811
+ dict: pdf_object_1.PdfDict.fromRawString(remappedContent),
812
+ data: obj.streamData
813
+ });
814
+ }
815
+ else {
816
+ modifiedObjects.set(newObjNum, remappedContent);
817
+ }
818
+ }
819
+ // Build a remapped font dict string
820
+ const rawFontDict = this._fontManager.buildFontDictString(fontObjectMap);
821
+ fontDictStr = this._remapRefsInString(rawFontDict, writerFontObjRemap);
822
+ }
823
+ const pagesInfo = this._doc.getPagesWithObjInfo();
824
+ for (let i = 0; i < pagesInfo.length; i++) {
825
+ const editorPage = this._pages[i];
826
+ const { dict: pageDict, objNum: pageObjNum } = pagesInfo[i];
827
+ if (pageObjNum === 0) {
828
+ // Can't do incremental update without knowing the page object number.
829
+ // Fall back to full rebuild (finally block handles cleanup).
830
+ return this.save();
831
+ }
832
+ const pageHasOverlay = editorPage._hasOverlay();
833
+ const pageHasFormUpdates = this._hasFormUpdatesForPage(pageDict);
834
+ if (!pageHasOverlay && !pageHasFormUpdates) {
835
+ continue; // Page is unchanged
836
+ }
837
+ // Build the updated page dictionary. We start from the original page
838
+ // dict's entries and modify only what's necessary.
839
+ const updatedPageDict = new pdf_object_1.PdfDict();
840
+ // Copy all existing entries from the original page dict
841
+ for (const [key, val] of pageDict.entries()) {
842
+ if (key === "Contents" && pageHasOverlay) {
843
+ continue; // We'll rewrite /Contents below
844
+ }
845
+ if (key === "Resources" && pageHasOverlay) {
846
+ continue; // We'll rewrite /Resources below
847
+ }
848
+ if (key === "Annots" && pageHasFormUpdates) {
849
+ continue; // We'll rewrite /Annots below
850
+ }
851
+ updatedPageDict.set(key, this._serializeOriginalValue(val));
852
+ }
853
+ if (pageHasOverlay) {
854
+ // Create a new content stream object for the overlay
855
+ const overlayStreamData = editorPage._overlay._stream.toUint8Array();
856
+ const overlayObjNum = nextObjNum++;
857
+ modifiedObjects.set(overlayObjNum, {
858
+ dict: new pdf_object_1.PdfDict(),
859
+ data: overlayStreamData
860
+ });
861
+ // Build the new /Contents array: original refs + overlay ref
862
+ const originalContentsObj = pageDict.get("Contents");
863
+ const contentRefs = this._collectOriginalContentRefs(originalContentsObj);
864
+ contentRefs.push((0, pdf_object_1.pdfRef)(overlayObjNum));
865
+ const contentsStr = contentRefs.length === 1 ? contentRefs[0] : `[${contentRefs.join(" ")}]`;
866
+ updatedPageDict.set("Contents", contentsStr);
867
+ // Build merged resources: original + overlay fonts/images
868
+ const originalResources = this._serializeOriginalResources(this._doc, pageDict);
869
+ // For overlay images, write them as new objects
870
+ const imageObjMap = new Map();
871
+ for (let imgIdx = 0; imgIdx < editorPage._overlay._images.length; imgIdx++) {
872
+ const img = editorPage._overlay._images[imgIdx];
873
+ const imgName = `Im${imgIdx + 1}`;
874
+ const imgObjNum = nextObjNum++;
875
+ imageObjMap.set(imgName, imgObjNum);
876
+ modifiedObjects.set(imgObjNum, this._buildImageXObjectForIncremental(img.data, img.format));
877
+ }
878
+ // Build overlay resource string
879
+ let overlayStr = "<< ";
880
+ if (fontDictStr) {
881
+ overlayStr += `/Font ${fontDictStr} `;
882
+ }
883
+ if (imageObjMap.size > 0) {
884
+ const entries = [...imageObjMap.entries()]
885
+ .map(([name, objNum]) => `/${name} ${(0, pdf_object_1.pdfRef)(objNum)}`)
886
+ .join(" ");
887
+ overlayStr += `/XObject << ${entries} >> `;
888
+ }
889
+ overlayStr += ">>";
890
+ const mergedResources = this._mergeResourceStrings(originalResources, overlayStr);
891
+ // Write merged resources as a new object
892
+ const resourcesObjNum = nextObjNum++;
893
+ modifiedObjects.set(resourcesObjNum, mergedResources || "<< >>");
894
+ updatedPageDict.set("Resources", (0, pdf_object_1.pdfRef)(resourcesObjNum));
895
+ }
896
+ if (pageHasFormUpdates) {
897
+ // Build updated annotations list
898
+ const annotsResult = this._buildIncrementalAnnots(pageDict, modifiedObjects, nextObjNum);
899
+ nextObjNum = annotsResult.nextObjNum;
900
+ if (annotsResult.annotRefs.length > 0) {
901
+ updatedPageDict.set("Annots", `[${annotsResult.annotRefs.map(r => (0, pdf_object_1.pdfRef)(r)).join(" ")}]`);
902
+ }
903
+ }
904
+ // Write the updated page dict as a modified object (same object number as original)
905
+ modifiedObjects.set(pageObjNum, updatedPageDict.toString());
906
+ }
907
+ if (modifiedObjects.size === 0) {
908
+ return this._doc.data;
909
+ }
910
+ return (0, pdf_writer_1.buildIncremental)(this._doc.data, modifiedObjects, new Map());
911
+ }
912
+ /**
913
+ * Check if a page has form field updates in its annotations.
914
+ * @internal
915
+ */
916
+ _hasFormUpdatesForPage(pageDict) {
917
+ if (this._formFieldUpdates.size === 0) {
918
+ return false;
919
+ }
920
+ const annotsObj = pageDict.get("Annots");
921
+ if (!annotsObj) {
922
+ return false;
923
+ }
924
+ const annotsResolved = this._doc.deref(annotsObj);
925
+ if (!(0, pdf_parser_1.isPdfArray)(annotsResolved)) {
926
+ return false;
927
+ }
928
+ for (const annotRef of annotsResolved) {
929
+ const annotDict = this._doc.derefDict(annotRef);
930
+ if (!annotDict) {
931
+ continue;
932
+ }
933
+ const subtype = (0, pdf_parser_1.dictGetName)(annotDict, "Subtype");
934
+ if (subtype === "Widget") {
935
+ const fieldName = this._resolveFieldName(annotDict);
936
+ if (fieldName && this._formFieldUpdates.has(fieldName)) {
937
+ return true;
938
+ }
939
+ }
940
+ }
941
+ return false;
942
+ }
943
+ /**
944
+ * Collect original /Contents refs as serialized ref strings.
945
+ * For incremental update — preserves original object references.
946
+ * @internal
947
+ */
948
+ _collectOriginalContentRefs(contentsObj) {
949
+ if (!contentsObj) {
950
+ return [];
951
+ }
952
+ if ((0, pdf_parser_1.isPdfRef)(contentsObj)) {
953
+ // Could be a single ref to a stream, or a ref to an array
954
+ const resolved = this._doc.deref(contentsObj);
955
+ if ((0, pdf_parser_1.isPdfArray)(resolved)) {
956
+ return resolved
957
+ .filter((item) => (0, pdf_parser_1.isPdfRef)(item))
958
+ .map(item => (0, pdf_object_1.pdfRef)(item.objNum, item.gen));
959
+ }
960
+ return [(0, pdf_object_1.pdfRef)(contentsObj.objNum, contentsObj.gen)];
961
+ }
962
+ if ((0, pdf_parser_1.isPdfArray)(contentsObj)) {
963
+ return contentsObj
964
+ .filter((item) => (0, pdf_parser_1.isPdfRef)(item))
965
+ .map(item => (0, pdf_object_1.pdfRef)(item.objNum, item.gen));
966
+ }
967
+ return [];
968
+ }
969
+ /**
970
+ * Serialize a page's Resources dict preserving original object references.
971
+ * Unlike _serializeResources which deep-clones into the writer, this keeps
972
+ * the original object numbers intact for incremental updates.
973
+ * @internal
974
+ */
975
+ _serializeOriginalResources(doc, pageDict) {
976
+ const resourcesDict = doc.resolvePageResources(pageDict);
977
+ if (!resourcesDict || resourcesDict.size === 0) {
978
+ return "<< >>";
979
+ }
980
+ const parts = ["<<"];
981
+ for (const [key, val] of resourcesDict.entries()) {
982
+ const serialized = this._serializeOriginalValue(val);
983
+ if (serialized) {
984
+ parts.push(`/${key} ${serialized}`);
985
+ }
986
+ }
987
+ parts.push(">>");
988
+ return parts.join(" ");
989
+ }
990
+ /**
991
+ * Serialize a PDF value from the original document, preserving original refs.
992
+ * Unlike _deepSerialize which clones refs into a new writer, this keeps
993
+ * the original object numbers intact.
994
+ * @internal
995
+ */
996
+ _serializeOriginalValue(val) {
997
+ if (val === null || val === undefined) {
998
+ return "null";
999
+ }
1000
+ if (typeof val === "string") {
1001
+ // In parsed PDF objects, string type = PDF name (without leading /)
1002
+ // Uint8Array = PDF string literal
1003
+ return "/" + val;
1004
+ }
1005
+ if (typeof val === "number") {
1006
+ return (0, pdf_object_1.pdfNumber)(val);
1007
+ }
1008
+ if (typeof val === "boolean") {
1009
+ return val ? "true" : "false";
1010
+ }
1011
+ if (val instanceof Uint8Array) {
1012
+ return (0, pdf_object_1.pdfHexString)(val);
1013
+ }
1014
+ if ((0, pdf_parser_1.isPdfRef)(val)) {
1015
+ return (0, pdf_object_1.pdfRef)(val.objNum, val.gen);
1016
+ }
1017
+ if ((0, pdf_parser_1.isPdfArray)(val)) {
1018
+ const items = val.map(item => this._serializeOriginalValue(item));
1019
+ return `[${items.join(" ")}]`;
1020
+ }
1021
+ if (val instanceof Map) {
1022
+ const parts = ["<<"];
1023
+ for (const [k, v] of val.entries()) {
1024
+ parts.push(`/${k} ${this._serializeOriginalValue(v)}`);
1025
+ }
1026
+ parts.push(">>");
1027
+ return parts.join(" ");
1028
+ }
1029
+ return "";
1030
+ }
1031
+ /**
1032
+ * Replace object number references in a serialized string.
1033
+ * Maps old obj numbers (from temporary writer) to new obj numbers.
1034
+ * @internal
1035
+ */
1036
+ _remapRefsInString(str, remap) {
1037
+ // Replace patterns like "N 0 R" where N is in the remap map
1038
+ return str.replace(/(\d+) (\d+) R/g, (match, objNumStr, genStr) => {
1039
+ const objNum = parseInt(objNumStr, 10);
1040
+ const remapped = remap.get(objNum);
1041
+ if (remapped !== undefined) {
1042
+ return `${remapped} ${genStr} R`;
1043
+ }
1044
+ return match;
1045
+ });
1046
+ }
1047
+ /**
1048
+ * Build image XObject content for incremental update.
1049
+ * @internal
1050
+ */
1051
+ _buildImageXObjectForIncremental(data, format) {
1052
+ const dims = (0, image_utils_1.parseImageDimensions)(data, format);
1053
+ const dict = new pdf_object_1.PdfDict()
1054
+ .set("Type", "/XObject")
1055
+ .set("Subtype", "/Image")
1056
+ .set("Width", (0, pdf_object_1.pdfNumber)(dims.width))
1057
+ .set("Height", (0, pdf_object_1.pdfNumber)(dims.height))
1058
+ .set("BitsPerComponent", "8")
1059
+ .set("ColorSpace", "/DeviceRGB");
1060
+ if (format === "jpeg") {
1061
+ dict.set("Filter", "/DCTDecode");
1062
+ }
1063
+ return { dict, data };
1064
+ }
1065
+ /**
1066
+ * Build updated annotations for incremental save.
1067
+ * Modified widgets get rewritten at their original object number;
1068
+ * unmodified annots keep original refs.
1069
+ * @internal
1070
+ */
1071
+ _buildIncrementalAnnots(pageDict, modifiedObjects, nextObjNum) {
1072
+ const annotsObj = pageDict.get("Annots");
1073
+ if (!annotsObj) {
1074
+ return { annotRefs: [], nextObjNum };
1075
+ }
1076
+ const annotsResolved = this._doc.deref(annotsObj);
1077
+ if (!(0, pdf_parser_1.isPdfArray)(annotsResolved)) {
1078
+ return { annotRefs: [], nextObjNum };
1079
+ }
1080
+ const annotRefs = [];
1081
+ for (const annotRef of annotsResolved) {
1082
+ const annotDict = this._doc.derefDict(annotRef);
1083
+ if (!annotDict) {
1084
+ // Keep original ref if we can't resolve
1085
+ if ((0, pdf_parser_1.isPdfRef)(annotRef)) {
1086
+ annotRefs.push(annotRef.objNum);
1087
+ }
1088
+ continue;
1089
+ }
1090
+ const subtype = (0, pdf_parser_1.dictGetName)(annotDict, "Subtype");
1091
+ if (subtype === "Widget" && this._formFieldUpdates.size > 0) {
1092
+ const fieldName = this._resolveFieldName(annotDict);
1093
+ const newValue = fieldName ? this._formFieldUpdates.get(fieldName) : undefined;
1094
+ if (newValue !== undefined) {
1095
+ // Rewrite the annotation at its original object number
1096
+ const annotObjNum = (0, pdf_parser_1.isPdfRef)(annotRef) ? annotRef.objNum : nextObjNum++;
1097
+ const newDict = this._buildModifiedWidgetDict(annotDict, newValue);
1098
+ modifiedObjects.set(annotObjNum, newDict);
1099
+ annotRefs.push(annotObjNum);
1100
+ continue;
1101
+ }
1102
+ }
1103
+ // Keep original annotation reference
1104
+ if ((0, pdf_parser_1.isPdfRef)(annotRef)) {
1105
+ annotRefs.push(annotRef.objNum);
1106
+ }
1107
+ else {
1108
+ // Inline annotation — write as new object
1109
+ const annotObjNum = nextObjNum++;
1110
+ const serialized = this._serializeAnnotDict(annotDict);
1111
+ modifiedObjects.set(annotObjNum, serialized);
1112
+ annotRefs.push(annotObjNum);
1113
+ }
1114
+ }
1115
+ return { annotRefs, nextObjNum };
1116
+ }
1117
+ /** @internal - Copy preserved page properties from source to target dict. */
1118
+ _copyPageProperties(source, target, doc) {
1119
+ const resolveDoc = doc ?? this._doc;
1120
+ for (const key of PdfEditor._PAGE_PRESERVE_KEYS) {
1121
+ const val = source.get(key);
1122
+ if (val !== undefined && val !== null) {
1123
+ target.set(key, this._deepSerialize(resolveDoc, val));
1124
+ }
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Sign this PDF with a digital signature.
1129
+ *
1130
+ * Performs a full save with an embedded PKCS#7 signature placeholder,
1131
+ * then fills in the real CMS SignedData.
1132
+ *
1133
+ * @param options - Certificate, private key, and optional signer metadata
1134
+ * @returns The signed PDF as Uint8Array
1135
+ *
1136
+ * @example
1137
+ * ```typescript
1138
+ * const editor = PdfEditor.load(pdfBytes);
1139
+ * const signed = await editor.sign({
1140
+ * certificate: certDerBytes,
1141
+ * privateKey: pkcs8DerBytes,
1142
+ * name: "Jane Doe",
1143
+ * reason: "Approval"
1144
+ * });
1145
+ * ```
1146
+ */
1147
+ async sign(options) {
1148
+ const { buildSignatureDictPlaceholder, signPdf } = await Promise.resolve().then(() => __importStar(require("../core/digital-signature")));
1149
+ const { dictString } = buildSignatureDictPlaceholder({
1150
+ name: options.name,
1151
+ reason: options.reason,
1152
+ location: options.location,
1153
+ contactInfo: options.contactInfo
1154
+ });
1155
+ // Inject the signature placeholder into the form field updates
1156
+ // so that save() includes it in the output
1157
+ this._signaturePlaceholder = dictString;
1158
+ let pdfWithPlaceholder;
1159
+ try {
1160
+ pdfWithPlaceholder = await this.save();
1161
+ }
1162
+ finally {
1163
+ this._signaturePlaceholder = null;
1164
+ }
1165
+ return signPdf(pdfWithPlaceholder, options.certificate, options.privateKey);
1166
+ }
1167
+ /** @internal - Collect decoded content stream bytes from a page dict. */
1168
+ _collectContentStreams(doc, pageDict) {
1169
+ const contentsObj = pageDict.get("Contents");
1170
+ if (!contentsObj) {
1171
+ return [];
1172
+ }
1173
+ if ((0, pdf_parser_1.isPdfRef)(contentsObj)) {
1174
+ const result = doc.derefStreamWithObjNum(contentsObj);
1175
+ if (result) {
1176
+ return [doc.getStreamData(result.stream, result.objNum, result.gen)];
1177
+ }
1178
+ // Could be a ref to an array
1179
+ const resolved = doc.deref(contentsObj);
1180
+ if ((0, pdf_parser_1.isPdfArray)(resolved)) {
1181
+ return this._resolveStreamArray(doc, resolved);
1182
+ }
1183
+ return [];
1184
+ }
1185
+ if ((0, pdf_parser_1.isPdfArray)(contentsObj)) {
1186
+ return this._resolveStreamArray(doc, contentsObj);
1187
+ }
1188
+ return [];
1189
+ }
1190
+ /** @internal */
1191
+ _resolveStreamArray(doc, arr) {
1192
+ const result = [];
1193
+ for (const item of arr) {
1194
+ const r = doc.derefStreamWithObjNum(item);
1195
+ if (r) {
1196
+ result.push(doc.getStreamData(r.stream, r.objNum, r.gen));
1197
+ }
1198
+ }
1199
+ return result;
1200
+ }
1201
+ /** @internal - Serialize a page's Resources dict by deep-cloning objects into the writer. */
1202
+ _serializeResources(doc, pageDict) {
1203
+ const resourcesDict = doc.resolvePageResources(pageDict);
1204
+ if (!resourcesDict || resourcesDict.size === 0) {
1205
+ return "<< >>";
1206
+ }
1207
+ // Deep-clone the resources dict into the writer.
1208
+ // We need to re-emit Font, XObject, ExtGState sub-dicts with their
1209
+ // referenced objects written as new indirect objects in the writer.
1210
+ return this._serializeDictToWriter(doc, resourcesDict);
1211
+ }
1212
+ /**
1213
+ * @internal - Recursively serialize a PdfDictValue, writing stream objects
1214
+ * as new indirect objects and converting refs to new writer refs.
1215
+ */
1216
+ _serializeDictToWriter(doc, dict) {
1217
+ const parts = ["<<"];
1218
+ for (const [key, val] of dict.entries()) {
1219
+ const serialized = this._deepSerialize(doc, val);
1220
+ if (serialized) {
1221
+ parts.push(`/${key} ${serialized}`);
1222
+ }
1223
+ }
1224
+ parts.push(">>");
1225
+ return parts.join(" ");
1226
+ }
1227
+ /**
1228
+ * @internal - Deep-serialize a PDF value.
1229
+ * For indirect refs: resolve the target, write it as a new object in the writer,
1230
+ * and return a ref to the new object.
1231
+ * For dicts/arrays: recurse.
1232
+ */
1233
+ _deepSerialize(doc, val) {
1234
+ if (val === null || val === undefined) {
1235
+ return "null";
1236
+ }
1237
+ if (typeof val === "string") {
1238
+ // In parsed PDF objects, string type = PDF name (without leading /)
1239
+ return "/" + val;
1240
+ }
1241
+ if (typeof val === "number") {
1242
+ return (0, pdf_object_1.pdfNumber)(val);
1243
+ }
1244
+ if (typeof val === "boolean") {
1245
+ return val ? "true" : "false";
1246
+ }
1247
+ if (val instanceof Uint8Array) {
1248
+ return (0, pdf_object_1.pdfHexString)(val);
1249
+ }
1250
+ if ((0, pdf_parser_1.isPdfRef)(val)) {
1251
+ // Check if this ref has already been cloned
1252
+ const docId = doc === this._doc ? "main" : `src${doc.data.length}`;
1253
+ const cacheKey = `${docId}:${val.objNum}:${val.gen}`;
1254
+ const cached = this._clonedRefs.get(cacheKey);
1255
+ if (cached !== undefined) {
1256
+ return (0, pdf_object_1.pdfRef)(cached);
1257
+ }
1258
+ // Try as stream first
1259
+ const streamResult = doc.derefStreamWithObjNum(val);
1260
+ if (streamResult) {
1261
+ const newObjNum = this._writerForSave.allocObject();
1262
+ this._clonedRefs.set(cacheKey, newObjNum);
1263
+ // Get the decoded stream data
1264
+ const streamData = doc.getStreamData(streamResult.stream, streamResult.objNum, streamResult.gen);
1265
+ // Serialize the stream's dict (excluding /Length which will be set automatically)
1266
+ const streamDict = new pdf_object_1.PdfDict();
1267
+ for (const [k, v] of streamResult.stream.dict.entries()) {
1268
+ if (k === "Length" || k === "Filter" || k === "DecodeParms") {
1269
+ // Skip — the writer will re-compress and set these
1270
+ continue;
1271
+ }
1272
+ const sv = this._deepSerialize(doc, v);
1273
+ if (sv) {
1274
+ streamDict.set(k, sv);
1275
+ }
1276
+ }
1277
+ this._writerForSave.addStreamObject(newObjNum, streamDict, streamData);
1278
+ return (0, pdf_object_1.pdfRef)(newObjNum);
1279
+ }
1280
+ // Try as regular dict/value
1281
+ const resolved = doc.deref(val);
1282
+ if (resolved instanceof Map) {
1283
+ const newObjNum = this._writerForSave.allocObject();
1284
+ this._clonedRefs.set(cacheKey, newObjNum);
1285
+ const dictStr = this._serializeDictToWriter(doc, resolved);
1286
+ this._writerForSave.addObject(newObjNum, dictStr);
1287
+ return (0, pdf_object_1.pdfRef)(newObjNum);
1288
+ }
1289
+ // Primitive value behind a ref — just inline it
1290
+ return this._deepSerialize(doc, resolved);
1291
+ }
1292
+ if ((0, pdf_parser_1.isPdfArray)(val)) {
1293
+ const items = val.map(item => this._deepSerialize(doc, item));
1294
+ return `[${items.join(" ")}]`;
1295
+ }
1296
+ if (val instanceof Map) {
1297
+ return this._serializeDictToWriter(doc, val);
1298
+ }
1299
+ return "";
1300
+ }
1301
+ /** @internal */
1302
+ _writeOverlayImages(writer, page) {
1303
+ const map = new Map();
1304
+ for (let i = 0; i < page._images.length; i++) {
1305
+ const img = page._images[i];
1306
+ const imgName = `Im${i + 1}`;
1307
+ const objNum = (0, image_utils_1.writeImageXObject)(writer, img.data, img.format);
1308
+ map.set(imgName, objNum);
1309
+ }
1310
+ return map;
1311
+ }
1312
+ /** @internal - Build overlay resources as a structured PdfResourceDict. */
1313
+ _buildOverlayResourceDict(fontDictStr, imageXObjectMap) {
1314
+ const dict = new Map();
1315
+ if (fontDictStr) {
1316
+ // Parse the font dict string into structured entries
1317
+ // fontDictStr is already a `<< /F1 3 0 R ... >>` string
1318
+ const fontInner = fontDictStr.trim();
1319
+ if (fontInner.startsWith("<<") && fontInner.endsWith(">>")) {
1320
+ const parsed = (0, resource_merger_1.parseResourceDict)(`<< /Font ${fontInner} >>`);
1321
+ const fontMap = parsed.get("Font");
1322
+ if (fontMap) {
1323
+ dict.set("Font", fontMap);
1324
+ }
1325
+ }
1326
+ }
1327
+ if (imageXObjectMap.size > 0) {
1328
+ const xobjMap = new Map();
1329
+ for (const [name, objNum] of imageXObjectMap) {
1330
+ xobjMap.set(name, (0, pdf_object_1.pdfRef)(objNum));
1331
+ }
1332
+ dict.set("XObject", xobjMap);
1333
+ }
1334
+ return dict;
1335
+ }
1336
+ /** @internal - Merge original and overlay resource strings via parsed object graph. */
1337
+ _mergeResourceStrings(original, overlay) {
1338
+ // If no overlay, return original
1339
+ if (!overlay || overlay === "<< >>") {
1340
+ return original;
1341
+ }
1342
+ // If no original, return overlay
1343
+ if (!original || original === "<< >>") {
1344
+ return overlay;
1345
+ }
1346
+ const origDict = (0, resource_merger_1.parseResourceDict)(original);
1347
+ const overlayDict = (0, resource_merger_1.parseResourceDict)(overlay);
1348
+ const merged = (0, resource_merger_1.mergeResourceDicts)(origDict, overlayDict);
1349
+ return (0, resource_merger_1.serializeResourceDict)(merged);
1350
+ }
1351
+ /** @internal - Write form field value updates as annotation objects. */
1352
+ _writeFormFieldUpdates(writer, pageDict, _pageIndex) {
1353
+ if (this._formFieldUpdates.size === 0) {
1354
+ return this._copyExistingAnnots(writer, pageDict);
1355
+ }
1356
+ // Get existing annotations
1357
+ const annotRefs = this._copyExistingAnnots(writer, pageDict);
1358
+ // We handle form field updates by modifying Widget annotations.
1359
+ // This is done during the annotation copy above — if we find a Widget
1360
+ // annotation whose field name matches an update, we modify its /V value.
1361
+ return annotRefs;
1362
+ }
1363
+ /** @internal */
1364
+ _copyExistingAnnots(writer, pageDict) {
1365
+ const annotsObj = pageDict.get("Annots");
1366
+ if (!annotsObj) {
1367
+ return [];
1368
+ }
1369
+ const annotsResolved = this._doc.deref(annotsObj);
1370
+ if (!(0, pdf_parser_1.isPdfArray)(annotsResolved)) {
1371
+ return [];
1372
+ }
1373
+ const annotRefs = [];
1374
+ for (const annotRef of annotsResolved) {
1375
+ const annotDict = this._doc.derefDict(annotRef);
1376
+ if (!annotDict) {
1377
+ continue;
1378
+ }
1379
+ const subtype = (0, pdf_parser_1.dictGetName)(annotDict, "Subtype");
1380
+ const objNum = writer.allocObject();
1381
+ if (subtype === "Widget" && this._formFieldUpdates.size > 0) {
1382
+ // Check if this widget has a field name that matches an update
1383
+ const fieldName = this._resolveFieldName(annotDict);
1384
+ const newValue = fieldName ? this._formFieldUpdates.get(fieldName) : undefined;
1385
+ if (newValue !== undefined) {
1386
+ // Write a modified widget annotation with the new value
1387
+ const newDict = this._buildModifiedWidgetDict(annotDict, newValue);
1388
+ writer.addObject(objNum, newDict);
1389
+ annotRefs.push(objNum);
1390
+ continue;
1391
+ }
1392
+ }
1393
+ // Copy annotation as-is (serialize what we can)
1394
+ const serialized = this._serializeAnnotDict(annotDict);
1395
+ writer.addObject(objNum, serialized);
1396
+ annotRefs.push(objNum);
1397
+ }
1398
+ return annotRefs;
1399
+ }
1400
+ /** @internal */
1401
+ _resolveFieldName(dict) {
1402
+ const parts = [];
1403
+ let current = dict;
1404
+ const visited = new Set();
1405
+ while (current) {
1406
+ const tVal = current.get("T");
1407
+ if (tVal) {
1408
+ const resolved = this._doc.deref(tVal);
1409
+ let name = "";
1410
+ if (typeof resolved === "string") {
1411
+ name = resolved;
1412
+ }
1413
+ else if (resolved instanceof Uint8Array) {
1414
+ name = (0, pdf_parser_1.decodePdfStringBytes)(resolved);
1415
+ }
1416
+ if (name) {
1417
+ parts.unshift(name);
1418
+ }
1419
+ }
1420
+ const parentVal = current.get("Parent");
1421
+ if (!parentVal) {
1422
+ break;
1423
+ }
1424
+ // Cycle guard
1425
+ const key = String(parentVal);
1426
+ if (visited.has(key)) {
1427
+ break;
1428
+ }
1429
+ visited.add(key);
1430
+ current = this._doc.derefDict(parentVal);
1431
+ }
1432
+ return parts.join(".");
1433
+ }
1434
+ /** @internal */
1435
+ _buildModifiedWidgetDict(originalDict, newValue) {
1436
+ // Determine the field type (/FT) — may be directly on the widget or inherited from parent
1437
+ const fieldType = this._resolveFieldType(originalDict);
1438
+ // For text fields, generate an appearance stream instead of stripping /AP
1439
+ if (fieldType === "Tx" && this._writerForSave) {
1440
+ return this._buildTextFieldWidgetDict(originalDict, newValue);
1441
+ }
1442
+ // For non-text fields, fall back to stripping /AP (force viewer to regenerate)
1443
+ const parts = ["<<"];
1444
+ for (const [key, val] of originalDict.entries()) {
1445
+ if (key === "V" || key === "AP") {
1446
+ continue;
1447
+ }
1448
+ const serialized = this._serializePdfValue(val);
1449
+ if (serialized) {
1450
+ parts.push(`/${key} ${serialized}`);
1451
+ }
1452
+ }
1453
+ parts.push(`/V ${(0, pdf_object_1.pdfString)(newValue)}`);
1454
+ parts.push(">>");
1455
+ return parts.join(" ");
1456
+ }
1457
+ /**
1458
+ * @internal - Build a modified widget dict for a text field with an inline
1459
+ * appearance stream. The stream renders the field value so it is visible
1460
+ * in all viewers, even those that ignore /NeedAppearances.
1461
+ */
1462
+ _buildTextFieldWidgetDict(originalDict, newValue) {
1463
+ const writer = this._writerForSave;
1464
+ // Extract the widget Rect for sizing the appearance
1465
+ const rect = this._resolveWidgetRect(originalDict);
1466
+ // Determine alignment from /Q entry (0=left, 1=center, 2=right)
1467
+ const qVal = originalDict.get("Q");
1468
+ let alignment = "left";
1469
+ if (qVal === 1) {
1470
+ alignment = "center";
1471
+ }
1472
+ else if (qVal === 2) {
1473
+ alignment = "right";
1474
+ }
1475
+ // Generate the appearance stream
1476
+ const { stream, resources } = (0, form_appearance_1.generateTextFieldAppearance)({
1477
+ value: newValue,
1478
+ rect,
1479
+ alignment
1480
+ });
1481
+ // Write the appearance stream as a Form XObject indirect object
1482
+ const apObjNum = writer.allocObject();
1483
+ const apDict = new pdf_object_1.PdfDict()
1484
+ .set("Type", "/XObject")
1485
+ .set("Subtype", "/Form")
1486
+ .set("BBox", (0, form_appearance_1.buildAppearanceBBox)(rect))
1487
+ .set("Resources", resources);
1488
+ writer.addStreamObject(apObjNum, apDict, stream, { compress: false });
1489
+ // Build the widget dict
1490
+ const parts = ["<<"];
1491
+ for (const [key, val] of originalDict.entries()) {
1492
+ if (key === "V" || key === "AP") {
1493
+ continue;
1494
+ }
1495
+ const serialized = this._serializePdfValue(val);
1496
+ if (serialized) {
1497
+ parts.push(`/${key} ${serialized}`);
1498
+ }
1499
+ }
1500
+ // Set the new value and appearance
1501
+ parts.push(`/V ${(0, pdf_object_1.pdfString)(newValue)}`);
1502
+ parts.push(`/AP << /N ${(0, pdf_object_1.pdfRef)(apObjNum)} >>`);
1503
+ parts.push(">>");
1504
+ return parts.join(" ");
1505
+ }
1506
+ /**
1507
+ * @internal - Resolve the field type (/FT) which may be inherited from a parent dict.
1508
+ */
1509
+ _resolveFieldType(dict) {
1510
+ let current = dict;
1511
+ const visited = new Set();
1512
+ while (current) {
1513
+ const ft = (0, pdf_parser_1.dictGetName)(current, "FT");
1514
+ if (ft) {
1515
+ return ft;
1516
+ }
1517
+ const parentVal = current.get("Parent");
1518
+ if (!parentVal) {
1519
+ break;
1520
+ }
1521
+ const key = String(parentVal);
1522
+ if (visited.has(key)) {
1523
+ break;
1524
+ }
1525
+ visited.add(key);
1526
+ current = this._doc.derefDict(parentVal);
1527
+ }
1528
+ return undefined;
1529
+ }
1530
+ /**
1531
+ * @internal - Extract the /Rect array from a widget annotation dict as [x1, y1, x2, y2].
1532
+ */
1533
+ _resolveWidgetRect(dict) {
1534
+ let rectVal = dict.get("Rect");
1535
+ if (rectVal) {
1536
+ rectVal = this._doc.deref(rectVal);
1537
+ }
1538
+ if (rectVal && (0, pdf_parser_1.isPdfArray)(rectVal)) {
1539
+ return rectVal.map(v => (typeof v === "number" ? v : 0));
1540
+ }
1541
+ // Fallback: 0-size rect
1542
+ return [0, 0, 100, 20];
1543
+ }
1544
+ /** @internal */
1545
+ _serializeAnnotDict(dict) {
1546
+ const parts = ["<<"];
1547
+ for (const [key, val] of dict.entries()) {
1548
+ const serialized = this._serializePdfValue(val);
1549
+ if (serialized) {
1550
+ parts.push(`/${key} ${serialized}`);
1551
+ }
1552
+ }
1553
+ parts.push(">>");
1554
+ return parts.join(" ");
1555
+ }
1556
+ /** @internal */
1557
+ _serializePdfValue(val) {
1558
+ if (val === null || val === undefined) {
1559
+ return "null";
1560
+ }
1561
+ if (typeof val === "string") {
1562
+ // In parsed PDF objects, string type = PDF name (without leading /)
1563
+ return "/" + val;
1564
+ }
1565
+ if (typeof val === "number") {
1566
+ return (0, pdf_object_1.pdfNumber)(val);
1567
+ }
1568
+ if (typeof val === "boolean") {
1569
+ return val ? "true" : "false";
1570
+ }
1571
+ if (val instanceof Uint8Array) {
1572
+ return (0, pdf_object_1.pdfHexString)(val);
1573
+ }
1574
+ if ((0, pdf_parser_1.isPdfRef)(val)) {
1575
+ // During incremental save, original refs remain valid — preserve them as-is.
1576
+ // During full save, deep-clone into the new writer.
1577
+ if (this._writerForSave && !this._isIncrementalSave) {
1578
+ return this._deepSerialize(this._doc, val);
1579
+ }
1580
+ return `${val.objNum} ${val.gen} R`;
1581
+ }
1582
+ if ((0, pdf_parser_1.isPdfArray)(val)) {
1583
+ const items = val.map(item => this._serializePdfValue(item));
1584
+ return `[${items.join(" ")}]`;
1585
+ }
1586
+ if (val instanceof Map) {
1587
+ const parts = ["<<"];
1588
+ for (const [k, v] of val.entries()) {
1589
+ const serialized = this._serializePdfValue(v);
1590
+ parts.push(`/${k} ${serialized}`);
1591
+ }
1592
+ parts.push(">>");
1593
+ return parts.join(" ");
1594
+ }
1595
+ return "";
1596
+ }
1597
+ }
1598
+ exports.PdfEditor = PdfEditor;
1599
+ // ===========================================================================
1600
+ // Internal Helpers
1601
+ // ===========================================================================
1602
+ /** Page-level keys to preserve when rebuilding page dicts. */
1603
+ PdfEditor._PAGE_PRESERVE_KEYS = [
1604
+ "Rotate",
1605
+ "CropBox",
1606
+ "BleedBox",
1607
+ "TrimBox",
1608
+ "ArtBox",
1609
+ "Group",
1610
+ "UserUnit",
1611
+ "Tabs"
1612
+ ];