@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.
- package/README.md +16 -1
- package/dist/browser/modules/archive/compression/crc32.js +1 -1
- package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/browser/modules/archive/crypto/aes.js +1 -20
- package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
- package/dist/browser/modules/archive/crypto/index.js +3 -1
- package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/browser/modules/excel/utils/encryptor.js +4 -7
- package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
- package/dist/browser/modules/pdf/core/encryption.js +8 -7
- package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/browser/modules/pdf/core/pdfa.js +261 -0
- package/dist/browser/modules/pdf/index.d.ts +11 -0
- package/dist/browser/modules/pdf/index.js +9 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
- package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/browser/modules/pdf/render/style-converter.js +1 -1
- package/dist/browser/modules/pdf/types.d.ts +14 -1
- package/dist/browser/modules/stream/browser/readable.js +8 -2
- package/dist/browser/utils/crypto.browser.d.ts +64 -0
- package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/browser/utils/crypto.d.ts +97 -0
- package/dist/browser/utils/crypto.js +209 -0
- package/dist/cjs/modules/archive/compression/crc32.js +1 -1
- package/dist/cjs/modules/archive/crypto/aes.js +2 -23
- package/dist/cjs/modules/archive/crypto/index.js +3 -1
- package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
- package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
- package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
- package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
- package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
- package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
- package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
- package/dist/cjs/modules/pdf/core/encryption.js +8 -7
- package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
- package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
- package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
- package/dist/cjs/modules/pdf/index.js +19 -1
- package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
- package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
- package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
- package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
- package/dist/cjs/modules/stream/browser/readable.js +8 -2
- package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
- package/dist/cjs/utils/crypto.js +228 -0
- package/dist/esm/modules/archive/compression/crc32.js +1 -1
- package/dist/esm/modules/archive/crypto/aes.js +1 -20
- package/dist/esm/modules/archive/crypto/index.js +3 -1
- package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/esm/modules/excel/utils/encryptor.js +4 -7
- package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
- package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
- package/dist/esm/modules/pdf/core/encryption.js +8 -7
- package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
- package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/esm/modules/pdf/core/pdfa.js +261 -0
- package/dist/esm/modules/pdf/index.js +9 -0
- package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
- package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
- package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/esm/modules/pdf/render/style-converter.js +1 -1
- package/dist/esm/modules/stream/browser/readable.js +8 -2
- package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/esm/utils/crypto.js +209 -0
- package/dist/iife/excelts.iife.js +1248 -1074
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +53 -54
- package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/types/modules/archive/crypto/index.d.ts +2 -1
- package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/types/modules/pdf/index.d.ts +11 -0
- package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/types/modules/pdf/types.d.ts +14 -1
- package/dist/types/utils/crypto.browser.d.ts +64 -0
- package/dist/types/utils/crypto.d.ts +97 -0
- package/package.json +110 -111
- package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
- 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
|
+
];
|