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