modern-pdf-lib 0.9.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/LICENSE +21 -0
- package/README.md +455 -0
- package/dist/documentMerge-CNPWlWic.mjs +18351 -0
- package/dist/documentMerge-DnLzOg5P.cjs +18878 -0
- package/dist/fflateAdapter-D2mv_ttM.mjs +196 -0
- package/dist/fflateAdapter-cT4YeY_h.cjs +207 -0
- package/dist/fontSubset-BOGts8y9.mjs +203 -0
- package/dist/fontSubset-C0Rm9ih6.cjs +226 -0
- package/dist/index.cjs +4597 -0
- package/dist/index.d.cts +7898 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +7898 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4306 -0
- package/dist/libdeflateWasm-QVHmuzw-.mjs +220 -0
- package/dist/libdeflateWasm-to2bG6NG.cjs +237 -0
- package/dist/loader-D9LTYmrX.mjs +162 -0
- package/dist/loader-mwt5wRJz.cjs +164 -0
- package/dist/pdfCatalog-DTXk0tbK.cjs +627 -0
- package/dist/pdfCatalog-Dk4qUVvx.mjs +532 -0
- package/dist/pdfPage-C9vw_D1J.cjs +5203 -0
- package/dist/pdfPage-DZA6XJzR.mjs +4544 -0
- package/dist/pngEmbed-BN-gMJrb.cjs +536 -0
- package/dist/pngEmbed-DgeNWlbS.mjs +525 -0
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/rolldown-runtime-CKhH4XqG.cjs +24 -0
- package/package.json +94 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4306 @@
|
|
|
1
|
+
import { $ as setFillColor, $t as showTextArray, A as endMarkedContent, At as moveTo, B as saveState, Bt as endText, C as beginArtifact, Ct as ellipsePath, D as beginMarkedContentWithProperties, Dt as fillEvenOdd, E as beginMarkedContentSequence, Et as fillAndStroke, F as radians, Ft as setLineJoin, G as applyFillColor, Gt as setFont, H as setGraphicsState, Ht as moveTextSetLeading, I as radiansToDegrees, It as setLineWidth, J as colorToComponents, Jt as setTextMatrix, K as applyStrokeColor, Kt as setFontSize, L as restoreState, Lt as setMiterLimit, M as concatMatrix, Mt as setDashPattern, N as degrees, Nt as setFlatness, O as createMarkedContentScope, Ot as fillEvenOddAndStroke, P as degreesToRadians, Pt as setLineCap, Q as setColorSpace, Qt as showText, R as rotate, Rt as stroke, S as createAnnotation, St as curveToInitial, T as beginMarkedContent, Tt as fill, U as skew, Ut as nextLine, V as scale, Vt as moveText, W as translate, Wt as setCharacterSpacing, X as grayscale, Xt as setTextRise, Y as componentsToColor, Yt as setTextRenderingMode, Z as rgb, Zt as setWordSpacing, _ as parseSvgTransform, _t as closeFillAndStroke, a as applyRedactions, at as setStrokeColorCmyk, b as annotationFromDict, bt as curveTo, c as PdfLayer, ct as setStrokeColorSpace, d as endLayerContent, dt as drawImageXObject, en as showTextHex, et as setFillColorCmyk, f as drawSvgOnPage, ft as drawXObject, g as parseSvgPath, gt as closeAndStroke, h as parseSvgColor, ht as clipEvenOdd, i as wrapText, it as setStrokeColor, j as wrapInMarkedContent, jt as rectangle, k as endArtifact, kt as lineTo, l as PdfLayerManager, lt as setStrokingColor, m as parseSvg, mt as clip, n as PdfPage, nn as showTextWithSpacing, nt as setFillColorRgb, o as getRedactionMarks, ot as setStrokeColorGray, p as svgToPdfOperators, pt as circlePath, q as cmyk, qt as setLeading, rt as setFillingColor, s as markForRedaction, st as setStrokeColorRgb, t as PageSizes, tn as showTextNextLine, tt as setFillColorGray, u as beginLayerContent, ut as drawImageWithMatrix, v as AnnotationFlags, vt as closeFillEvenOddAndStroke, w as beginArtifactWithType, wt as endPath, x as buildAnnotationDict, xt as curveToFinal, y as PdfAnnotation, yt as closePath, z as rotationMatrix, zt as beginText } from "./pdfPage-DZA6XJzR.mjs";
|
|
2
|
+
import { a as formatPdfDate, c as PdfBool, d as PdfNull, f as PdfNumber, g as PdfString, h as PdfStream, i as buildPageTree, l as PdfDict, m as PdfRef, n as buildDocumentStructure, p as PdfObjectRegistry, r as buildInfoDict, s as PdfArray, t as buildCatalog, u as PdfName } from "./pdfCatalog-Dk4qUVvx.mjs";
|
|
3
|
+
import { $ as checkAccessibility, A as buildEmbeddedFilesNameTree, At as EmbeddedFont, B as encodeLength, C as generateSignatureAppearance, Ct as aesEncryptCBC, D as addWatermark, Dt as base64Encode, E as PdfField, Et as base64Decode, F as signPdf, G as encodeSet, H as encodeOctetString, I as buildPkcs7Signature, J as parseDerTlv, K as encodeUTCTime, L as decodeOidBytes, M as verifySignature, Mt as PdfStreamWriter, N as verifySignatures, Nt as PdfWriter, O as addWatermarkToPage, Ot as isOpenTypeCFF, P as getSignatures, Pt as serializePdf, Q as prepareForSigning, R as encodeContextTag, S as generateRadioAppearance, St as aesDecryptCBC, T as FieldFlags, Tt as md5, U as encodePrintableString, V as encodeOID, W as encodeSequence, X as embedSignature, Y as computeSignatureHash, Z as findSignatures, _ as PdfTextField, _t as sha256, a as PdfDocument, at as buildViewerPreferencesDict, b as generateDropdownAppearance, bt as decodePermissions, c as embedPageAsFormXObject, ct as createXmpStream, d as PdfSignatureField, dt as PdfOutlineTree, et as isAccessible, f as PdfButtonField, ft as loadPdf, g as PdfCheckboxField, gt as verifyUserPassword, h as PdfRadioGroup, ht as verifyOwnerPassword, i as splitPdf, it as PdfViewerPreferences, j as getAttachments, jt as extractMetrics, k as attachFile, kt as isTrueType, l as decodeStream, lt as parseXmpMetadata, m as PdfDropdownField, mt as computeFileEncryptionKey, nt as PdfStructureElement, o as StandardFonts, ot as parseViewerPreferences, p as PdfListboxField, pt as PdfEncryptionHandler, q as encodeUtf8String, r as mergePdfs, rt as PdfStructureTree, s as createPdf, st as buildXmpMetadata, t as copyPages, tt as summarizeIssues, u as PdfForm, ut as PdfOutlineItem, v as generateButtonAppearance, vt as sha384, w as generateTextAppearance, wt as rc4, x as generateListboxAppearance, xt as encodePermissions, y as generateCheckboxAppearance, yt as sha512, z as encodeInteger } from "./documentMerge-CNPWlWic.mjs";
|
|
4
|
+
import { deflateSync } from "fflate";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/pdfValueHelpers.ts
|
|
7
|
+
/**
|
|
8
|
+
* @module utils/pdfValueHelpers
|
|
9
|
+
*
|
|
10
|
+
* Convenience helpers for converting between JS values and PDF object types.
|
|
11
|
+
* These match the pdf-lib helper API surface.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Create a {@link PdfName} from a string.
|
|
15
|
+
*
|
|
16
|
+
* @param value The name value (with or without leading `/`).
|
|
17
|
+
*/
|
|
18
|
+
function asPDFName(value) {
|
|
19
|
+
return PdfName.of(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a {@link PdfNumber} from a numeric value.
|
|
23
|
+
*
|
|
24
|
+
* @param value The number.
|
|
25
|
+
*/
|
|
26
|
+
function asPDFNumber(value) {
|
|
27
|
+
return PdfNumber.of(value);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract a numeric value from a {@link PdfObject}.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` if the object is not a PdfNumber.
|
|
33
|
+
*
|
|
34
|
+
* @param obj The PDF object to inspect.
|
|
35
|
+
*/
|
|
36
|
+
function asNumber(obj) {
|
|
37
|
+
if (obj.kind === "number") return obj.value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/core/enums.ts
|
|
42
|
+
/**
|
|
43
|
+
* PDF blend modes (PDF 1.4+, Table 136).
|
|
44
|
+
* Applied via ExtGState /BM key.
|
|
45
|
+
*/
|
|
46
|
+
const BlendMode = {
|
|
47
|
+
Normal: "Normal",
|
|
48
|
+
Multiply: "Multiply",
|
|
49
|
+
Screen: "Screen",
|
|
50
|
+
Overlay: "Overlay",
|
|
51
|
+
Darken: "Darken",
|
|
52
|
+
Lighten: "Lighten",
|
|
53
|
+
ColorDodge: "ColorDodge",
|
|
54
|
+
ColorBurn: "ColorBurn",
|
|
55
|
+
HardLight: "HardLight",
|
|
56
|
+
SoftLight: "SoftLight",
|
|
57
|
+
Difference: "Difference",
|
|
58
|
+
Exclusion: "Exclusion"
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* PDF text rendering modes (Table 106).
|
|
62
|
+
* Applied via the Tr operator inside a text object.
|
|
63
|
+
*/
|
|
64
|
+
const TextRenderingMode = {
|
|
65
|
+
Fill: 0,
|
|
66
|
+
Outline: 1,
|
|
67
|
+
FillAndOutline: 2,
|
|
68
|
+
Invisible: 3,
|
|
69
|
+
FillAndClip: 4,
|
|
70
|
+
OutlineAndClip: 5,
|
|
71
|
+
FillAndOutlineAndClip: 6,
|
|
72
|
+
Clip: 7
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* PDF line cap styles (Table 54).
|
|
76
|
+
* Applied via the J operator.
|
|
77
|
+
*/
|
|
78
|
+
const LineCapStyle = {
|
|
79
|
+
Butt: 0,
|
|
80
|
+
Round: 1,
|
|
81
|
+
Projecting: 2
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* PDF line join styles (Table 55).
|
|
85
|
+
* Applied via the j operator.
|
|
86
|
+
*/
|
|
87
|
+
const LineJoinStyle = {
|
|
88
|
+
Miter: 0,
|
|
89
|
+
Round: 1,
|
|
90
|
+
Bevel: 2
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Text alignment for form fields and layout operations.
|
|
94
|
+
*/
|
|
95
|
+
const TextAlignment = {
|
|
96
|
+
Left: 0,
|
|
97
|
+
Center: 1,
|
|
98
|
+
Right: 2
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Image alignment for layout operations.
|
|
102
|
+
*/
|
|
103
|
+
const ImageAlignment = {
|
|
104
|
+
Left: 0,
|
|
105
|
+
Center: 1,
|
|
106
|
+
Right: 2
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Preset parsing speeds — maps to objectsPerTick values in LoadPdfOptions.
|
|
110
|
+
*
|
|
111
|
+
* Lower values keep the main thread more responsive but parse more slowly.
|
|
112
|
+
*/
|
|
113
|
+
const ParseSpeeds = {
|
|
114
|
+
Fastest: Infinity,
|
|
115
|
+
Fast: 500,
|
|
116
|
+
Medium: 100,
|
|
117
|
+
Slow: 10
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/core/pageManipulation.ts
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a PageSize input to a `[width, height]` tuple.
|
|
124
|
+
*
|
|
125
|
+
* @param size Page size as a tuple, object, or undefined (defaults to A4).
|
|
126
|
+
* @returns A `[width, height]` tuple in PDF points.
|
|
127
|
+
*/
|
|
128
|
+
function resolveSize(size) {
|
|
129
|
+
const resolved = size ?? PageSizes.A4;
|
|
130
|
+
if (Array.isArray(resolved)) return [resolved[0], resolved[1]];
|
|
131
|
+
return [resolved.width, resolved.height];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate that a page index is within bounds.
|
|
135
|
+
*
|
|
136
|
+
* @param index The index to validate.
|
|
137
|
+
* @param pageCount The total number of pages.
|
|
138
|
+
* @param operation A description of the operation for error messages.
|
|
139
|
+
* @throws RangeError if the index is out of bounds.
|
|
140
|
+
*/
|
|
141
|
+
function validateIndex(index, pageCount, operation) {
|
|
142
|
+
if (!Number.isInteger(index) || index < 0 || index >= pageCount) throw new RangeError(`${operation}: index ${index} out of range [0, ${pageCount - 1}]`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validate that a page index is valid for insertion (0 to pageCount inclusive).
|
|
146
|
+
*
|
|
147
|
+
* @param index The index to validate.
|
|
148
|
+
* @param pageCount The total number of pages.
|
|
149
|
+
* @param operation A description of the operation for error messages.
|
|
150
|
+
* @throws RangeError if the index is out of bounds.
|
|
151
|
+
*/
|
|
152
|
+
function validateInsertIndex(index, pageCount, operation) {
|
|
153
|
+
if (!Number.isInteger(index) || index < 0 || index > pageCount) throw new RangeError(`${operation}: index ${index} out of range [0, ${pageCount}]`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate that a rotation angle is a multiple of 90.
|
|
157
|
+
*
|
|
158
|
+
* @param angle The rotation angle in degrees.
|
|
159
|
+
* @throws Error if the angle is not a valid rotation.
|
|
160
|
+
*/
|
|
161
|
+
function validateRotation(angle) {
|
|
162
|
+
if (angle % 90 !== 0) throw new Error(`rotatePage: angle must be a multiple of 90, got ${angle}`);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Insert a new blank page into the document at the specified position.
|
|
166
|
+
*
|
|
167
|
+
* All existing pages at `index` and beyond are shifted to make room.
|
|
168
|
+
*
|
|
169
|
+
* @param doc The PdfDocument to modify.
|
|
170
|
+
* @param index Zero-based position at which to insert the page.
|
|
171
|
+
* Must be in the range `[0, pageCount]`.
|
|
172
|
+
* @param size Optional page size. Defaults to A4.
|
|
173
|
+
* @returns The newly created PdfPage.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* import { createPdf, insertPage, PageSizes } from 'modern-pdf';
|
|
178
|
+
*
|
|
179
|
+
* const doc = createPdf();
|
|
180
|
+
* doc.addPage();
|
|
181
|
+
* const newPage = insertPage(doc, 0, PageSizes.Letter); // insert at front
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function insertPage(doc, index, size) {
|
|
185
|
+
const pages = doc.getInternalPages();
|
|
186
|
+
const registry = doc.getRegistry();
|
|
187
|
+
validateInsertIndex(index, pages.length, "insertPage");
|
|
188
|
+
const [w, h] = resolveSize(size);
|
|
189
|
+
const page = new PdfPage(w, h, registry);
|
|
190
|
+
doc.registerFontsOnPage(page);
|
|
191
|
+
pages.splice(index, 0, page);
|
|
192
|
+
return page;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Remove a page from the document by its zero-based index.
|
|
196
|
+
*
|
|
197
|
+
* @param doc The PdfDocument to modify.
|
|
198
|
+
* @param index Zero-based index of the page to remove.
|
|
199
|
+
* @throws RangeError if the index is out of bounds.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* removePage(doc, 0); // Remove the first page
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
function removePage(doc, index) {
|
|
207
|
+
const pages = doc.getInternalPages();
|
|
208
|
+
validateIndex(index, pages.length, "removePage");
|
|
209
|
+
pages.splice(index, 1);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Move a page from one position to another within the document.
|
|
213
|
+
*
|
|
214
|
+
* The page at `fromIndex` is removed and then inserted at `toIndex`
|
|
215
|
+
* (computed after the removal).
|
|
216
|
+
*
|
|
217
|
+
* @param doc The PdfDocument to modify.
|
|
218
|
+
* @param fromIndex Current zero-based index of the page to move.
|
|
219
|
+
* @param toIndex Target zero-based index. Must be in range
|
|
220
|
+
* `[0, pageCount - 1]` after removal.
|
|
221
|
+
* @throws RangeError if either index is out of bounds.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* movePage(doc, 2, 0); // Move page 2 to the front
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
function movePage(doc, fromIndex, toIndex) {
|
|
229
|
+
const pages = doc.getInternalPages();
|
|
230
|
+
validateIndex(fromIndex, pages.length, "movePage (fromIndex)");
|
|
231
|
+
const [page] = pages.splice(fromIndex, 1);
|
|
232
|
+
if (toIndex < 0 || toIndex > pages.length) {
|
|
233
|
+
pages.splice(fromIndex, 0, page);
|
|
234
|
+
throw new RangeError(`movePage (toIndex): index ${toIndex} out of range [0, ${pages.length}]`);
|
|
235
|
+
}
|
|
236
|
+
pages.splice(toIndex, 0, page);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Rotate a page by the specified angle.
|
|
240
|
+
*
|
|
241
|
+
* The angle is cumulative with any existing rotation set on the page.
|
|
242
|
+
* The page's `/Rotate` entry in the page dictionary will be set when
|
|
243
|
+
* the document is saved.
|
|
244
|
+
*
|
|
245
|
+
* @param doc The PdfDocument to modify.
|
|
246
|
+
* @param index Zero-based index of the page to rotate.
|
|
247
|
+
* @param angle Rotation angle in degrees. Must be a multiple of 90.
|
|
248
|
+
* Common values: 90, 180, 270, -90.
|
|
249
|
+
* @throws RangeError if the index is out of bounds.
|
|
250
|
+
* @throws Error if the angle is not a multiple of 90.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* rotatePage(doc, 0, 90); // Rotate first page 90 degrees clockwise
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
function rotatePage(doc, index, angle) {
|
|
258
|
+
const pages = doc.getInternalPages();
|
|
259
|
+
validateIndex(index, pages.length, "rotatePage");
|
|
260
|
+
validateRotation(angle);
|
|
261
|
+
const page = pages[index];
|
|
262
|
+
const newRotation = (((page.getRotation?.() ?? 0) + angle) % 360 + 360) % 360;
|
|
263
|
+
page.setRotation(newRotation);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Set a crop box on a page.
|
|
267
|
+
*
|
|
268
|
+
* The crop box defines the visible region of the page when displayed
|
|
269
|
+
* or printed. It defaults to the media box if not set.
|
|
270
|
+
*
|
|
271
|
+
* @param doc The PdfDocument to modify.
|
|
272
|
+
* @param index Zero-based index of the page.
|
|
273
|
+
* @param cropBox The crop box rectangle.
|
|
274
|
+
* @throws RangeError if the index is out of bounds.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* cropPage(doc, 0, { x: 50, y: 50, width: 495, height: 742 });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
function cropPage(doc, index, cropBox) {
|
|
282
|
+
const pages = doc.getInternalPages();
|
|
283
|
+
validateIndex(index, pages.length, "cropPage");
|
|
284
|
+
pages[index].setCropBox(cropBox.x, cropBox.y, cropBox.width, cropBox.height);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get the size of a page.
|
|
288
|
+
*
|
|
289
|
+
* @param doc The PdfDocument.
|
|
290
|
+
* @param index Zero-based page index.
|
|
291
|
+
* @returns A `{ width, height }` object in PDF points.
|
|
292
|
+
*/
|
|
293
|
+
function getPageSize(doc, index) {
|
|
294
|
+
const pages = doc.getInternalPages();
|
|
295
|
+
validateIndex(index, pages.length, "getPageSize");
|
|
296
|
+
const page = pages[index];
|
|
297
|
+
return {
|
|
298
|
+
width: page.width,
|
|
299
|
+
height: page.height
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Resize a page by setting its media box.
|
|
304
|
+
*
|
|
305
|
+
* Note: This changes the page dimensions but does not scale existing
|
|
306
|
+
* content. Content that was drawn at coordinates beyond the new
|
|
307
|
+
* dimensions may be clipped.
|
|
308
|
+
*
|
|
309
|
+
* @param doc The PdfDocument to modify.
|
|
310
|
+
* @param index Zero-based page index.
|
|
311
|
+
* @param size New page size.
|
|
312
|
+
*/
|
|
313
|
+
function resizePage(doc, index, size) {
|
|
314
|
+
const pages = doc.getInternalPages();
|
|
315
|
+
validateIndex(index, pages.length, "resizePage");
|
|
316
|
+
const [w, h] = resolveSize(size);
|
|
317
|
+
pages[index].setMediaBox(0, 0, w, h);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Reverse the page order of the entire document.
|
|
321
|
+
*
|
|
322
|
+
* @param doc The PdfDocument to modify.
|
|
323
|
+
*/
|
|
324
|
+
function reversePages(doc) {
|
|
325
|
+
doc.getInternalPages().reverse();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Remove multiple pages at once, given their zero-based indices.
|
|
329
|
+
*
|
|
330
|
+
* Indices are processed in descending order to avoid shifting issues.
|
|
331
|
+
*
|
|
332
|
+
* @param doc The PdfDocument to modify.
|
|
333
|
+
* @param indices Array of zero-based page indices to remove.
|
|
334
|
+
* @throws RangeError if any index is out of bounds.
|
|
335
|
+
*/
|
|
336
|
+
function removePages(doc, indices) {
|
|
337
|
+
const pages = doc.getInternalPages();
|
|
338
|
+
for (const index of indices) validateIndex(index, pages.length, "removePages");
|
|
339
|
+
const sorted = new Set(indices).values().toArray().sort((a, b) => b - a);
|
|
340
|
+
for (const index of sorted) pages.splice(index, 1);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Rotate all pages in the document by the specified angle.
|
|
344
|
+
*
|
|
345
|
+
* @param doc The PdfDocument to modify.
|
|
346
|
+
* @param angle Rotation angle in degrees (must be a multiple of 90).
|
|
347
|
+
*/
|
|
348
|
+
function rotateAllPages(doc, angle) {
|
|
349
|
+
validateRotation(angle);
|
|
350
|
+
const pages = doc.getInternalPages();
|
|
351
|
+
for (const page of pages) {
|
|
352
|
+
const newRotation = (((page.getRotation?.() ?? 0) + angle) % 360 + 360) % 360;
|
|
353
|
+
page.setRotation(newRotation);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/core/layout.ts
|
|
359
|
+
/**
|
|
360
|
+
* Break text into lines that fit within `maxWidth`, measuring each line's
|
|
361
|
+
* width. Explicit newlines (`\n`) are always honoured.
|
|
362
|
+
*
|
|
363
|
+
* The returned `height` is the total vertical extent: one line's ascent
|
|
364
|
+
* plus `(n-1) * lineHeight`.
|
|
365
|
+
*/
|
|
366
|
+
function layoutMultilineText(text, options) {
|
|
367
|
+
const { font, fontSize, maxWidth, wordBreaks } = options;
|
|
368
|
+
const lineHeight = options.lineHeight ?? fontSize * 1.2;
|
|
369
|
+
const rawLines = text.split("\n");
|
|
370
|
+
const allLines = [];
|
|
371
|
+
for (const rawLine of rawLines) if (maxWidth > 0) {
|
|
372
|
+
const wrapped = wrapText(rawLine, maxWidth, font, fontSize, wordBreaks);
|
|
373
|
+
for (const line of wrapped) allLines.push({
|
|
374
|
+
text: line,
|
|
375
|
+
width: font.widthOfTextAtSize(line, fontSize)
|
|
376
|
+
});
|
|
377
|
+
} else allLines.push({
|
|
378
|
+
text: rawLine,
|
|
379
|
+
width: font.widthOfTextAtSize(rawLine, fontSize)
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
lines: allLines,
|
|
383
|
+
height: allLines.length > 0 ? font.heightAtSize(fontSize) + (allLines.length - 1) * lineHeight : 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Layout text into evenly-spaced cells for combed text fields.
|
|
388
|
+
*
|
|
389
|
+
* Each character is centred within its cell. Characters beyond
|
|
390
|
+
* `cellCount` are silently truncated.
|
|
391
|
+
*/
|
|
392
|
+
function layoutCombedText(text, options) {
|
|
393
|
+
const { font, fontSize, cellCount, cellWidth } = options;
|
|
394
|
+
return text.slice(0, cellCount).split("").map((char, i) => {
|
|
395
|
+
const charWidth = font.widthOfTextAtSize(char, fontSize);
|
|
396
|
+
return {
|
|
397
|
+
char,
|
|
398
|
+
x: i * cellWidth + cellWidth / 2 - charWidth / 2,
|
|
399
|
+
width: charWidth
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Compute the largest font size (in points) at which `text` fits within
|
|
405
|
+
* the given width (and optionally height) constraints.
|
|
406
|
+
*
|
|
407
|
+
* Uses binary search between `minSize` (default 4) and `maxSize`
|
|
408
|
+
* (default 500), converging to within 0.1 pt.
|
|
409
|
+
*/
|
|
410
|
+
function computeFontSize(text, options) {
|
|
411
|
+
const { font, maxWidth, wordBreaks } = options;
|
|
412
|
+
const maxHeight = options.maxHeight ?? Infinity;
|
|
413
|
+
const minSize = options.minSize ?? 4;
|
|
414
|
+
const maxSize = options.maxSize ?? 500;
|
|
415
|
+
let lo = minSize;
|
|
416
|
+
let hi = maxSize;
|
|
417
|
+
for (let i = 0; i < 40; i++) {
|
|
418
|
+
const mid = (lo + hi) / 2;
|
|
419
|
+
const layout = layoutMultilineText(text, {
|
|
420
|
+
font,
|
|
421
|
+
fontSize: mid,
|
|
422
|
+
maxWidth,
|
|
423
|
+
lineHeight: options.lineHeight,
|
|
424
|
+
wordBreaks
|
|
425
|
+
});
|
|
426
|
+
if (layout.height <= maxHeight && layout.lines.every((line) => line.width <= maxWidth)) lo = mid;
|
|
427
|
+
else hi = mid;
|
|
428
|
+
if (hi - lo < .1) break;
|
|
429
|
+
}
|
|
430
|
+
return Math.floor(lo * 10) / 10;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Layout a single line of text with optional alignment within bounds.
|
|
434
|
+
*
|
|
435
|
+
* Unlike {@link layoutMultilineText}, this does not perform any wrapping.
|
|
436
|
+
* It simply measures the text, computes alignment offsets, and returns
|
|
437
|
+
* the positioned line.
|
|
438
|
+
*
|
|
439
|
+
* @param text The text to lay out (single line, no newlines).
|
|
440
|
+
* @param options Font, size, bounds, alignment.
|
|
441
|
+
* @returns The measured line and its position offsets.
|
|
442
|
+
*/
|
|
443
|
+
function layoutSinglelineText(text, options) {
|
|
444
|
+
const { font, fontSize, bounds, alignment } = options;
|
|
445
|
+
const width = font.widthOfTextAtSize(text, fontSize);
|
|
446
|
+
const height = font.heightAtSize(fontSize);
|
|
447
|
+
let x = 0;
|
|
448
|
+
let y = 0;
|
|
449
|
+
if (bounds) {
|
|
450
|
+
y = (bounds.height - height) / 2;
|
|
451
|
+
switch (alignment) {
|
|
452
|
+
case 1:
|
|
453
|
+
x = (bounds.width - width) / 2;
|
|
454
|
+
break;
|
|
455
|
+
case 2:
|
|
456
|
+
x = bounds.width - width;
|
|
457
|
+
break;
|
|
458
|
+
default:
|
|
459
|
+
x = 0;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
line: {
|
|
465
|
+
text,
|
|
466
|
+
width,
|
|
467
|
+
height
|
|
468
|
+
},
|
|
469
|
+
x,
|
|
470
|
+
y
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/core/incrementalWriter.ts
|
|
476
|
+
/**
|
|
477
|
+
* @module core/incrementalWriter
|
|
478
|
+
*
|
|
479
|
+
* Incremental save support for PDF documents.
|
|
480
|
+
*
|
|
481
|
+
* An incremental save appends new/modified objects to the end of the
|
|
482
|
+
* original PDF file, followed by a new cross-reference section and
|
|
483
|
+
* trailer that references the original xref via the `/Prev` key.
|
|
484
|
+
*
|
|
485
|
+
* This is critical for:
|
|
486
|
+
* 1. **Signature preservation** — Digital signatures are invalidated by
|
|
487
|
+
* full rewrites. Incremental saves preserve the original bytes
|
|
488
|
+
* exactly, so signatures remain valid.
|
|
489
|
+
* 2. **Performance on large files** — Only modified objects are written,
|
|
490
|
+
* so saving a small change to a 100MB PDF takes milliseconds.
|
|
491
|
+
* 3. **Undo history** — Each incremental save is a layer that can be
|
|
492
|
+
* peeled back to restore the previous state.
|
|
493
|
+
*
|
|
494
|
+
* Reference: PDF 1.7 spec, SS7.5.6 (Incremental Updates).
|
|
495
|
+
*
|
|
496
|
+
* @packageDocumentation
|
|
497
|
+
*/
|
|
498
|
+
/**
|
|
499
|
+
* Tracks which objects have been added or modified since the document
|
|
500
|
+
* was loaded. Only these objects are written during an incremental save.
|
|
501
|
+
*/
|
|
502
|
+
var ChangeTracker = class {
|
|
503
|
+
/** Set of object numbers that are new (not in the original file). */
|
|
504
|
+
newObjects = /* @__PURE__ */ new Set();
|
|
505
|
+
/** Set of object numbers that existed but have been modified. */
|
|
506
|
+
modifiedObjects = /* @__PURE__ */ new Set();
|
|
507
|
+
/** The highest object number from the original file. */
|
|
508
|
+
originalMaxObjNum;
|
|
509
|
+
constructor(originalMaxObjNum) {
|
|
510
|
+
this.originalMaxObjNum = originalMaxObjNum;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Mark an object as new (did not exist in the original file).
|
|
514
|
+
*/
|
|
515
|
+
markNew(objectNumber) {
|
|
516
|
+
this.newObjects.add(objectNumber);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Mark an object as modified (existed in the original file).
|
|
520
|
+
*/
|
|
521
|
+
markModified(objectNumber) {
|
|
522
|
+
if (objectNumber <= this.originalMaxObjNum) this.modifiedObjects.add(objectNumber);
|
|
523
|
+
else this.newObjects.add(objectNumber);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if an object is new or modified.
|
|
527
|
+
*/
|
|
528
|
+
isChanged(objectNumber) {
|
|
529
|
+
return this.newObjects.has(objectNumber) || this.modifiedObjects.has(objectNumber);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get all changed object numbers (new + modified).
|
|
533
|
+
*/
|
|
534
|
+
getChangedObjects() {
|
|
535
|
+
return this.newObjects.union(this.modifiedObjects);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get the count of changed objects.
|
|
539
|
+
*/
|
|
540
|
+
get changedCount() {
|
|
541
|
+
return this.newObjects.union(this.modifiedObjects).size;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const encoder$2 = new TextEncoder();
|
|
545
|
+
/**
|
|
546
|
+
* Growable byte buffer for building the incremental appendix.
|
|
547
|
+
*/
|
|
548
|
+
var ByteBuffer = class {
|
|
549
|
+
chunks = [];
|
|
550
|
+
_offset = 0;
|
|
551
|
+
get offset() {
|
|
552
|
+
return this._offset;
|
|
553
|
+
}
|
|
554
|
+
write(data) {
|
|
555
|
+
this.chunks.push(data);
|
|
556
|
+
this._offset += data.length;
|
|
557
|
+
}
|
|
558
|
+
writeString(str) {
|
|
559
|
+
this.write(encoder$2.encode(str));
|
|
560
|
+
}
|
|
561
|
+
toUint8Array() {
|
|
562
|
+
const result = new Uint8Array(this._offset);
|
|
563
|
+
let pos = 0;
|
|
564
|
+
for (const chunk of this.chunks) {
|
|
565
|
+
result.set(chunk, pos);
|
|
566
|
+
pos += chunk.length;
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* Find the `startxref` offset in the original PDF bytes.
|
|
573
|
+
*
|
|
574
|
+
* Scans backward from the end of the file looking for the `startxref`
|
|
575
|
+
* keyword, then parses the numeric offset on the following line.
|
|
576
|
+
*
|
|
577
|
+
* @param data The original PDF bytes.
|
|
578
|
+
* @returns The byte offset of the original cross-reference section.
|
|
579
|
+
* @throws Error if `startxref` is not found.
|
|
580
|
+
*/
|
|
581
|
+
function findOriginalXrefOffset(data) {
|
|
582
|
+
const decoder = new TextDecoder("latin1");
|
|
583
|
+
const searchStart = Math.max(0, data.length - 1024);
|
|
584
|
+
const tail = decoder.decode(data.subarray(searchStart));
|
|
585
|
+
const idx = tail.lastIndexOf("startxref");
|
|
586
|
+
if (idx === -1) throw new Error("Incremental save: could not find \"startxref\" in the original PDF. The file may be corrupted.");
|
|
587
|
+
const match = tail.substring(idx + 9).trim().match(/^(\d+)/);
|
|
588
|
+
if (!match) throw new Error("Incremental save: could not parse xref offset after \"startxref\".");
|
|
589
|
+
return parseInt(match[1], 10);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Find the /Size value from the original trailer.
|
|
593
|
+
*
|
|
594
|
+
* @param data The original PDF bytes.
|
|
595
|
+
* @returns The /Size value from the trailer.
|
|
596
|
+
*/
|
|
597
|
+
function findOriginalTrailerSize(data) {
|
|
598
|
+
const decoder = new TextDecoder("latin1");
|
|
599
|
+
const searchStart = Math.max(0, data.length - 2048);
|
|
600
|
+
const sizeMatch = decoder.decode(data.subarray(searchStart)).match(/\/Size\s+(\d+)/);
|
|
601
|
+
if (sizeMatch) return parseInt(sizeMatch[1], 10);
|
|
602
|
+
throw new Error("Incremental save: could not find /Size in the original trailer.");
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Perform an incremental save of a PDF document.
|
|
606
|
+
*
|
|
607
|
+
* Takes the original file bytes and a registry of objects (some new,
|
|
608
|
+
* some modified), and appends only the changed objects plus a new xref
|
|
609
|
+
* section and trailer.
|
|
610
|
+
*
|
|
611
|
+
* The resulting bytes form a valid PDF file that preserves the original
|
|
612
|
+
* content byte-for-byte and appends the modifications.
|
|
613
|
+
*
|
|
614
|
+
* @param originalBytes The original PDF file bytes (unmodified).
|
|
615
|
+
* @param registry The object registry containing all objects
|
|
616
|
+
* (original + new/modified).
|
|
617
|
+
* @param structure Document structure references (catalog, info, pages).
|
|
618
|
+
* @param changedObjects Set of object numbers that are new or modified.
|
|
619
|
+
* @param options Optional save options (compression, etc.).
|
|
620
|
+
* @returns The complete incremental save result.
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```ts
|
|
624
|
+
* const result = saveIncremental(originalBytes, registry, structure, changedObjects);
|
|
625
|
+
* await writeFile('output.pdf', result.bytes);
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
function saveIncremental(originalBytes, registry, structure, changedObjects, options) {
|
|
629
|
+
const compress = options?.compress ?? true;
|
|
630
|
+
const compressionLevel = options?.compressionLevel ?? 6;
|
|
631
|
+
const prevXrefOffset = findOriginalXrefOffset(originalBytes);
|
|
632
|
+
let originalSize;
|
|
633
|
+
try {
|
|
634
|
+
originalSize = findOriginalTrailerSize(originalBytes);
|
|
635
|
+
} catch {
|
|
636
|
+
originalSize = registry.nextNumber;
|
|
637
|
+
}
|
|
638
|
+
const buf = new ByteBuffer();
|
|
639
|
+
buf.writeString("\n");
|
|
640
|
+
const xrefOffsets = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const entry of registry) {
|
|
642
|
+
if (!changedObjects.has(entry.ref.objectNumber)) continue;
|
|
643
|
+
if (compress && entry.object.kind === "stream") compressStream(entry.object, compressionLevel);
|
|
644
|
+
const offset = originalBytes.length + buf.offset;
|
|
645
|
+
xrefOffsets.set(entry.ref.objectNumber, offset);
|
|
646
|
+
buf.writeString(`${entry.ref.toObjectHeader()}\n`);
|
|
647
|
+
entry.object.serialize(buf);
|
|
648
|
+
buf.writeString(`\n${entry.ref.toObjectFooter()}\n`);
|
|
649
|
+
}
|
|
650
|
+
let newSize = originalSize;
|
|
651
|
+
for (const objNum of changedObjects) if (objNum + 1 > newSize) newSize = objNum + 1;
|
|
652
|
+
if (registry.nextNumber > newSize) newSize = registry.nextNumber;
|
|
653
|
+
const xrefOffset = originalBytes.length + buf.offset;
|
|
654
|
+
const subsections = buildXrefSubsections(xrefOffsets.keys().toArray().sort((a, b) => a - b), xrefOffsets);
|
|
655
|
+
buf.writeString("xref\n");
|
|
656
|
+
for (const subsection of subsections) {
|
|
657
|
+
buf.writeString(`${subsection.startObjNum} ${subsection.entries.length}\n`);
|
|
658
|
+
for (const entry of subsection.entries) buf.writeString(`${entry.offset.toString().padStart(10, "0")} ${entry.generation.toString().padStart(5, "0")} ${entry.type} \n`);
|
|
659
|
+
}
|
|
660
|
+
buf.writeString("trailer\n");
|
|
661
|
+
buf.writeString("<<\n");
|
|
662
|
+
buf.writeString(`/Size ${newSize}\n`);
|
|
663
|
+
buf.writeString(`/Root ${structure.catalogRef.objectNumber} ${structure.catalogRef.generationNumber} R\n`);
|
|
664
|
+
buf.writeString(`/Info ${structure.infoRef.objectNumber} ${structure.infoRef.generationNumber} R\n`);
|
|
665
|
+
buf.writeString(`/Prev ${prevXrefOffset}\n`);
|
|
666
|
+
buf.writeString(">>\n");
|
|
667
|
+
buf.writeString("startxref\n");
|
|
668
|
+
buf.writeString(`${xrefOffset}\n`);
|
|
669
|
+
buf.writeString("%%EOF\n");
|
|
670
|
+
const appendix = buf.toUint8Array();
|
|
671
|
+
const result = new Uint8Array(originalBytes.length + appendix.length);
|
|
672
|
+
result.set(originalBytes, 0);
|
|
673
|
+
result.set(appendix, originalBytes.length);
|
|
674
|
+
return {
|
|
675
|
+
bytes: result,
|
|
676
|
+
newXrefOffset: xrefOffset
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Group consecutive object numbers into xref subsections.
|
|
681
|
+
*
|
|
682
|
+
* For example, objects [3, 4, 5, 10, 11] become two subsections:
|
|
683
|
+
* - `3 3` (objects 3, 4, 5)
|
|
684
|
+
* - `10 2` (objects 10, 11)
|
|
685
|
+
*/
|
|
686
|
+
function buildXrefSubsections(sortedObjNums, offsets) {
|
|
687
|
+
if (sortedObjNums.length === 0) return [];
|
|
688
|
+
const subsections = [];
|
|
689
|
+
let currentStart = sortedObjNums[0];
|
|
690
|
+
let currentEntries = [{
|
|
691
|
+
offset: offsets.get(currentStart),
|
|
692
|
+
generation: 0,
|
|
693
|
+
type: "n"
|
|
694
|
+
}];
|
|
695
|
+
for (let i = 1; i < sortedObjNums.length; i++) {
|
|
696
|
+
const objNum = sortedObjNums[i];
|
|
697
|
+
if (objNum === sortedObjNums[i - 1] + 1) currentEntries.push({
|
|
698
|
+
offset: offsets.get(objNum),
|
|
699
|
+
generation: 0,
|
|
700
|
+
type: "n"
|
|
701
|
+
});
|
|
702
|
+
else {
|
|
703
|
+
subsections.push({
|
|
704
|
+
startObjNum: currentStart,
|
|
705
|
+
entries: currentEntries
|
|
706
|
+
});
|
|
707
|
+
currentStart = objNum;
|
|
708
|
+
currentEntries = [{
|
|
709
|
+
offset: offsets.get(objNum),
|
|
710
|
+
generation: 0,
|
|
711
|
+
type: "n"
|
|
712
|
+
}];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
subsections.push({
|
|
716
|
+
startObjNum: currentStart,
|
|
717
|
+
entries: currentEntries
|
|
718
|
+
});
|
|
719
|
+
return subsections;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Apply FlateDecode compression to a stream if it's not already compressed.
|
|
723
|
+
*/
|
|
724
|
+
function compressStream(stream, level) {
|
|
725
|
+
if (stream.dict.has("/Filter")) return;
|
|
726
|
+
if (stream.data.length === 0) return;
|
|
727
|
+
const compressed = deflateSync(stream.data, { level });
|
|
728
|
+
if (compressed.length < stream.data.length) {
|
|
729
|
+
stream.dict.set("/Filter", PdfName.of("FlateDecode"));
|
|
730
|
+
stream.data = compressed;
|
|
731
|
+
stream.syncLength();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Perform an incremental save given the original bytes and a PdfDocument.
|
|
736
|
+
*
|
|
737
|
+
* This is a convenience wrapper that builds the document structure,
|
|
738
|
+
* determines which objects have changed, and calls `saveIncremental`.
|
|
739
|
+
*
|
|
740
|
+
* @param originalBytes The original PDF file bytes.
|
|
741
|
+
* @param doc The modified PdfDocument.
|
|
742
|
+
* @param options Optional save options.
|
|
743
|
+
* @returns The incremental save result.
|
|
744
|
+
*/
|
|
745
|
+
async function saveDocumentIncremental(originalBytes, doc, options) {
|
|
746
|
+
const { buildDocumentStructure } = await import("./pdfCatalog-Dk4qUVvx.mjs").then((n) => n.o);
|
|
747
|
+
const { PdfPage: _PdfPage } = await import("./pdfPage-DZA6XJzR.mjs").then((n) => n.r);
|
|
748
|
+
const registry = doc.getRegistry();
|
|
749
|
+
const structure = buildDocumentStructure(doc.getInternalPages().map((p) => p.finalize()), {
|
|
750
|
+
producer: doc.getProducer(),
|
|
751
|
+
creationDate: doc.getCreationDate(),
|
|
752
|
+
modDate: doc.getModDate() ?? /* @__PURE__ */ new Date(),
|
|
753
|
+
title: doc.getTitle(),
|
|
754
|
+
author: doc.getAuthor(),
|
|
755
|
+
subject: doc.getSubject(),
|
|
756
|
+
keywords: doc.getKeywords(),
|
|
757
|
+
creator: doc.getCreator()
|
|
758
|
+
}, registry);
|
|
759
|
+
const originalSize = findOriginalTrailerSize(originalBytes);
|
|
760
|
+
const changedObjects = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const entry of registry) if (entry.ref.objectNumber >= originalSize) changedObjects.add(entry.ref.objectNumber);
|
|
762
|
+
changedObjects.add(structure.catalogRef.objectNumber);
|
|
763
|
+
changedObjects.add(structure.infoRef.objectNumber);
|
|
764
|
+
changedObjects.add(structure.pagesRef.objectNumber);
|
|
765
|
+
return saveIncremental(originalBytes, registry, structure, changedObjects, options);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/core/operators/index.ts
|
|
770
|
+
/**
|
|
771
|
+
* A first-class representation of a single PDF content-stream operator.
|
|
772
|
+
*
|
|
773
|
+
* In pdf-lib, operators are typed objects rather than raw strings. This
|
|
774
|
+
* class provides the same capability while remaining interoperable with
|
|
775
|
+
* the string-based operator functions above.
|
|
776
|
+
*
|
|
777
|
+
* ```ts
|
|
778
|
+
* const op = PDFOperator.of('m', 100, 200); // moveTo(100, 200)
|
|
779
|
+
* page.pushOperators(op.toString());
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
var PDFOperator = class PDFOperator {
|
|
783
|
+
/**
|
|
784
|
+
* Create a new operator.
|
|
785
|
+
*
|
|
786
|
+
* @param name The PDF operator name (e.g. `'m'`, `'l'`, `'re'`, `'Tj'`).
|
|
787
|
+
* @param operands Numeric, string, or name operands.
|
|
788
|
+
*/
|
|
789
|
+
static of(name, ...operands) {
|
|
790
|
+
return new PDFOperator(name, operands);
|
|
791
|
+
}
|
|
792
|
+
constructor(name, operands) {
|
|
793
|
+
this.name = name;
|
|
794
|
+
this.operands = operands;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Serialize this operator to its PDF content-stream representation.
|
|
798
|
+
*
|
|
799
|
+
* @returns A string like `"100 200 m\n"`.
|
|
800
|
+
*/
|
|
801
|
+
toString() {
|
|
802
|
+
if (this.operands.length === 0) return `${this.name}\n`;
|
|
803
|
+
return `${this.operands.map((op) => {
|
|
804
|
+
if (typeof op === "number") {
|
|
805
|
+
if (Number.isInteger(op)) return op.toString();
|
|
806
|
+
const s = op.toFixed(6).replace(/\.?0+$/, "");
|
|
807
|
+
return s === "-0" ? "0" : s;
|
|
808
|
+
}
|
|
809
|
+
return op;
|
|
810
|
+
}).join(" ")} ${this.name}\n`;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/annotation/types/textAnnotation.ts
|
|
816
|
+
/**
|
|
817
|
+
* @module annotation/types/textAnnotation
|
|
818
|
+
*
|
|
819
|
+
* Sticky note (text) annotation — a small icon that, when clicked,
|
|
820
|
+
* opens a popup window containing the annotation text.
|
|
821
|
+
*
|
|
822
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.4 (Text Annotations).
|
|
823
|
+
*/
|
|
824
|
+
/**
|
|
825
|
+
* A sticky note annotation (subtype /Text).
|
|
826
|
+
*
|
|
827
|
+
* Displays a small icon on the page; clicking the icon opens a popup
|
|
828
|
+
* containing the annotation's text.
|
|
829
|
+
*/
|
|
830
|
+
var PdfTextAnnotation = class PdfTextAnnotation extends PdfAnnotation {
|
|
831
|
+
constructor(dict) {
|
|
832
|
+
super("Text", dict);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Create a new text (sticky note) annotation.
|
|
836
|
+
*/
|
|
837
|
+
static create(options) {
|
|
838
|
+
const annot = new PdfTextAnnotation(buildAnnotationDict("Text", options));
|
|
839
|
+
if (options.icon !== void 0) annot.setIcon(options.icon);
|
|
840
|
+
if (options.open !== void 0) annot.setOpen(options.open);
|
|
841
|
+
return annot;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Create a PdfTextAnnotation from an existing dictionary.
|
|
845
|
+
*/
|
|
846
|
+
static fromDict(dict, resolver) {
|
|
847
|
+
return new PdfTextAnnotation(dict);
|
|
848
|
+
}
|
|
849
|
+
/** Get the icon name. Defaults to 'Note'. */
|
|
850
|
+
getIcon() {
|
|
851
|
+
const obj = this.dict.get("/Name");
|
|
852
|
+
if (obj && obj.kind === "name") return obj.value.startsWith("/") ? obj.value.slice(1) : obj.value;
|
|
853
|
+
return "Note";
|
|
854
|
+
}
|
|
855
|
+
/** Set the icon name. */
|
|
856
|
+
setIcon(icon) {
|
|
857
|
+
this.dict.set("/Name", PdfName.of(icon));
|
|
858
|
+
}
|
|
859
|
+
/** Whether the popup is initially open. */
|
|
860
|
+
isOpen() {
|
|
861
|
+
const obj = this.dict.get("/Open");
|
|
862
|
+
if (obj && obj.kind === "bool") return obj.value;
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
/** Set the initial open state. */
|
|
866
|
+
setOpen(open) {
|
|
867
|
+
this.dict.set("/Open", PdfBool.of(open));
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/annotation/types/linkAnnotation.ts
|
|
873
|
+
/**
|
|
874
|
+
* @module annotation/types/linkAnnotation
|
|
875
|
+
*
|
|
876
|
+
* Link annotation — a clickable region that navigates to a destination
|
|
877
|
+
* within the document or opens an external URL.
|
|
878
|
+
*
|
|
879
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.5 (Link Annotations).
|
|
880
|
+
*/
|
|
881
|
+
/**
|
|
882
|
+
* A link annotation (subtype /Link).
|
|
883
|
+
*
|
|
884
|
+
* Provides navigation to a destination within the document or to an
|
|
885
|
+
* external URI.
|
|
886
|
+
*/
|
|
887
|
+
var PdfLinkAnnotation = class PdfLinkAnnotation extends PdfAnnotation {
|
|
888
|
+
constructor(dict) {
|
|
889
|
+
super("Link", dict);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Create a new link annotation.
|
|
893
|
+
*/
|
|
894
|
+
static create(options) {
|
|
895
|
+
const annot = new PdfLinkAnnotation(buildAnnotationDict("Link", options));
|
|
896
|
+
if (options.url !== void 0) annot.setUrl(options.url);
|
|
897
|
+
if (options.pageIndex !== void 0) annot.setDestination(options.pageIndex, options.fit);
|
|
898
|
+
if (options.highlightMode !== void 0) annot.setHighlightMode(options.highlightMode);
|
|
899
|
+
return annot;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Create from an existing dictionary.
|
|
903
|
+
*/
|
|
904
|
+
static fromDict(dict, resolver) {
|
|
905
|
+
return new PdfLinkAnnotation(dict);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Get the destination (named dest string or explicit dest array).
|
|
909
|
+
*
|
|
910
|
+
* Returns:
|
|
911
|
+
* - A string for named destinations.
|
|
912
|
+
* - An array `[pageIndex, fitMode, ...params]` for explicit destinations.
|
|
913
|
+
* - `undefined` if no destination is set.
|
|
914
|
+
*/
|
|
915
|
+
getDestination() {
|
|
916
|
+
const obj = this.dict.get("/Dest");
|
|
917
|
+
if (!obj) return void 0;
|
|
918
|
+
if (obj.kind === "string") return obj.value;
|
|
919
|
+
if (obj.kind === "array" && obj.items.length >= 2) {
|
|
920
|
+
const fitObj = obj.items[1];
|
|
921
|
+
const fitName = fitObj && fitObj.kind === "name" ? fitObj.value.startsWith("/") ? fitObj.value.slice(1) : fitObj.value : "Fit";
|
|
922
|
+
const pageNum = obj.items[0] && obj.items[0].kind === "number" ? obj.items[0].value : 0;
|
|
923
|
+
const extra = [];
|
|
924
|
+
for (let i = 2; i < obj.items.length; i++) {
|
|
925
|
+
const item = obj.items[i];
|
|
926
|
+
if (item && item.kind === "number") extra.push(item.value);
|
|
927
|
+
}
|
|
928
|
+
return [
|
|
929
|
+
pageNum,
|
|
930
|
+
fitName,
|
|
931
|
+
...extra
|
|
932
|
+
];
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Set an explicit destination (page index + fit mode).
|
|
937
|
+
*
|
|
938
|
+
* @param pageIndex Zero-based page index.
|
|
939
|
+
* @param fit Fit mode (defaults to 'Fit').
|
|
940
|
+
*/
|
|
941
|
+
setDestination(pageIndex, fit) {
|
|
942
|
+
const fitName = fit ?? "Fit";
|
|
943
|
+
this.dict.set("/Dest", PdfArray.of([PdfNumber.of(pageIndex), PdfName.of(fitName)]));
|
|
944
|
+
}
|
|
945
|
+
/** Get the URL if this is a URI link. */
|
|
946
|
+
getUrl() {
|
|
947
|
+
const action = this.dict.get("/A");
|
|
948
|
+
if (action && action.kind === "dict") {
|
|
949
|
+
const uri = action.get("/URI");
|
|
950
|
+
if (uri && uri.kind === "string") return uri.value;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** Set the URL (creates a /URI action). */
|
|
954
|
+
setUrl(url) {
|
|
955
|
+
const action = new PdfDict();
|
|
956
|
+
action.set("/S", PdfName.of("URI"));
|
|
957
|
+
action.set("/URI", PdfString.literal(url));
|
|
958
|
+
this.dict.set("/A", action);
|
|
959
|
+
}
|
|
960
|
+
/** Get the highlight mode. Defaults to 'Invert'. */
|
|
961
|
+
getHighlightMode() {
|
|
962
|
+
const obj = this.dict.get("/H");
|
|
963
|
+
if (obj && obj.kind === "name") return {
|
|
964
|
+
N: "None",
|
|
965
|
+
I: "Invert",
|
|
966
|
+
O: "Outline",
|
|
967
|
+
P: "Push",
|
|
968
|
+
None: "None",
|
|
969
|
+
Invert: "Invert",
|
|
970
|
+
Outline: "Outline",
|
|
971
|
+
Push: "Push"
|
|
972
|
+
}[obj.value.startsWith("/") ? obj.value.slice(1) : obj.value] ?? "Invert";
|
|
973
|
+
return "Invert";
|
|
974
|
+
}
|
|
975
|
+
/** Set the highlight mode. */
|
|
976
|
+
setHighlightMode(mode) {
|
|
977
|
+
this.dict.set("/H", PdfName.of({
|
|
978
|
+
None: "N",
|
|
979
|
+
Invert: "I",
|
|
980
|
+
Outline: "O",
|
|
981
|
+
Push: "P"
|
|
982
|
+
}[mode]));
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region src/annotation/appearanceGenerator.ts
|
|
988
|
+
/**
|
|
989
|
+
* @module annotation/appearanceGenerator
|
|
990
|
+
*
|
|
991
|
+
* Generates /AP (appearance) streams for annotations.
|
|
992
|
+
*
|
|
993
|
+
* Appearance streams are Form XObjects that define how an annotation
|
|
994
|
+
* looks when rendered. Each appearance is a content stream with
|
|
995
|
+
* its own resource dictionary and bounding box.
|
|
996
|
+
*
|
|
997
|
+
* Reference: PDF 1.7 spec, Section 12.5.5 (Appearance Streams).
|
|
998
|
+
*/
|
|
999
|
+
/** Format a number for content stream operators. */
|
|
1000
|
+
function n(value) {
|
|
1001
|
+
if (Number.isInteger(value)) return value.toString();
|
|
1002
|
+
const s = value.toFixed(4).replace(/\.?0+$/, "");
|
|
1003
|
+
return s === "-0" ? "0" : s;
|
|
1004
|
+
}
|
|
1005
|
+
/** Bezier curve constant for approximating a quarter circle. */
|
|
1006
|
+
const KAPPA = .5522847498;
|
|
1007
|
+
/**
|
|
1008
|
+
* Build a Form XObject (appearance stream) from content operators.
|
|
1009
|
+
*
|
|
1010
|
+
* @param content The content stream operators.
|
|
1011
|
+
* @param bbox The bounding box [x1, y1, x2, y2].
|
|
1012
|
+
* @param resources Optional resources dictionary.
|
|
1013
|
+
* @returns A PdfStream configured as a Form XObject.
|
|
1014
|
+
*/
|
|
1015
|
+
function buildAppearanceStream(content, bbox, resources) {
|
|
1016
|
+
const dict = new PdfDict();
|
|
1017
|
+
dict.set("/Type", PdfName.of("XObject"));
|
|
1018
|
+
dict.set("/Subtype", PdfName.of("Form"));
|
|
1019
|
+
dict.set("/BBox", PdfArray.fromNumbers(bbox));
|
|
1020
|
+
if (resources) dict.set("/Resources", resources);
|
|
1021
|
+
return PdfStream.fromString(content, dict);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Generate appearance stream for a Square annotation.
|
|
1025
|
+
*/
|
|
1026
|
+
function generateSquareAppearance(annot) {
|
|
1027
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1028
|
+
const w = x2 - x1;
|
|
1029
|
+
const h = y2 - y1;
|
|
1030
|
+
const color = annot.getColor();
|
|
1031
|
+
const opacity = annot.getOpacity();
|
|
1032
|
+
const dict = annot.getDict();
|
|
1033
|
+
const icObj = dict.get("/IC");
|
|
1034
|
+
let interiorColor;
|
|
1035
|
+
if (icObj && icObj.kind === "array" && icObj.items.length >= 3) interiorColor = {
|
|
1036
|
+
r: icObj.items[0]?.value ?? 0,
|
|
1037
|
+
g: icObj.items[1]?.value ?? 0,
|
|
1038
|
+
b: icObj.items[2]?.value ?? 0
|
|
1039
|
+
};
|
|
1040
|
+
let borderWidth = 1;
|
|
1041
|
+
const bsObj = dict.get("/BS");
|
|
1042
|
+
if (bsObj && bsObj.kind === "dict") {
|
|
1043
|
+
const wObj = bsObj.get("/W");
|
|
1044
|
+
if (wObj && wObj.kind === "number") borderWidth = wObj.value;
|
|
1045
|
+
}
|
|
1046
|
+
let ops = "";
|
|
1047
|
+
if (opacity < 1) ops += `${n(opacity)} ca ${n(opacity)} CA\n`;
|
|
1048
|
+
if (interiorColor) {
|
|
1049
|
+
ops += `${n(interiorColor.r)} ${n(interiorColor.g)} ${n(interiorColor.b)} rg\n`;
|
|
1050
|
+
ops += `${n(borderWidth / 2)} ${n(borderWidth / 2)} ${n(w - borderWidth)} ${n(h - borderWidth)} re\n`;
|
|
1051
|
+
ops += "f\n";
|
|
1052
|
+
}
|
|
1053
|
+
if (color) ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1054
|
+
else ops += "0 0 0 RG\n";
|
|
1055
|
+
ops += `${n(borderWidth)} w\n`;
|
|
1056
|
+
ops += `${n(borderWidth / 2)} ${n(borderWidth / 2)} ${n(w - borderWidth)} ${n(h - borderWidth)} re\n`;
|
|
1057
|
+
ops += "S\n";
|
|
1058
|
+
return buildAppearanceStream(ops, [
|
|
1059
|
+
0,
|
|
1060
|
+
0,
|
|
1061
|
+
w,
|
|
1062
|
+
h
|
|
1063
|
+
]);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Generate appearance stream for a Circle annotation.
|
|
1067
|
+
*/
|
|
1068
|
+
function generateCircleAppearance(annot) {
|
|
1069
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1070
|
+
const w = x2 - x1;
|
|
1071
|
+
const h = y2 - y1;
|
|
1072
|
+
const cx = w / 2;
|
|
1073
|
+
const cy = h / 2;
|
|
1074
|
+
const rx = w / 2;
|
|
1075
|
+
const ry = h / 2;
|
|
1076
|
+
const color = annot.getColor();
|
|
1077
|
+
const opacity = annot.getOpacity();
|
|
1078
|
+
const dict = annot.getDict();
|
|
1079
|
+
const icObj = dict.get("/IC");
|
|
1080
|
+
let interiorColor;
|
|
1081
|
+
if (icObj && icObj.kind === "array" && icObj.items.length >= 3) interiorColor = {
|
|
1082
|
+
r: icObj.items[0]?.value ?? 0,
|
|
1083
|
+
g: icObj.items[1]?.value ?? 0,
|
|
1084
|
+
b: icObj.items[2]?.value ?? 0
|
|
1085
|
+
};
|
|
1086
|
+
let borderWidth = 1;
|
|
1087
|
+
const bsObj = dict.get("/BS");
|
|
1088
|
+
if (bsObj && bsObj.kind === "dict") {
|
|
1089
|
+
const wObj = bsObj.get("/W");
|
|
1090
|
+
if (wObj && wObj.kind === "number") borderWidth = wObj.value;
|
|
1091
|
+
}
|
|
1092
|
+
const adjRx = rx - borderWidth / 2;
|
|
1093
|
+
const adjRy = ry - borderWidth / 2;
|
|
1094
|
+
const kx = KAPPA * adjRx;
|
|
1095
|
+
const ky = KAPPA * adjRy;
|
|
1096
|
+
let ops = "";
|
|
1097
|
+
if (opacity < 1) ops += `${n(opacity)} ca ${n(opacity)} CA\n`;
|
|
1098
|
+
const ellipsePath = `${n(cx)} ${n(cy + adjRy)} m\n${n(cx + kx)} ${n(cy + adjRy)} ${n(cx + adjRx)} ${n(cy + ky)} ${n(cx + adjRx)} ${n(cy)} c\n${n(cx + adjRx)} ${n(cy - ky)} ${n(cx + kx)} ${n(cy - adjRy)} ${n(cx)} ${n(cy - adjRy)} c\n${n(cx - kx)} ${n(cy - adjRy)} ${n(cx - adjRx)} ${n(cy - ky)} ${n(cx - adjRx)} ${n(cy)} c\n${n(cx - adjRx)} ${n(cy + ky)} ${n(cx - kx)} ${n(cy + adjRy)} ${n(cx)} ${n(cy + adjRy)} c\n`;
|
|
1099
|
+
if (interiorColor) {
|
|
1100
|
+
ops += `${n(interiorColor.r)} ${n(interiorColor.g)} ${n(interiorColor.b)} rg\n`;
|
|
1101
|
+
if (color) ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1102
|
+
else ops += "0 0 0 RG\n";
|
|
1103
|
+
ops += `${n(borderWidth)} w\n`;
|
|
1104
|
+
ops += ellipsePath;
|
|
1105
|
+
ops += "B\n";
|
|
1106
|
+
} else {
|
|
1107
|
+
if (color) ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1108
|
+
else ops += "0 0 0 RG\n";
|
|
1109
|
+
ops += `${n(borderWidth)} w\n`;
|
|
1110
|
+
ops += ellipsePath;
|
|
1111
|
+
ops += "S\n";
|
|
1112
|
+
}
|
|
1113
|
+
return buildAppearanceStream(ops, [
|
|
1114
|
+
0,
|
|
1115
|
+
0,
|
|
1116
|
+
w,
|
|
1117
|
+
h
|
|
1118
|
+
]);
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Generate appearance stream for a Line annotation.
|
|
1122
|
+
*/
|
|
1123
|
+
function generateLineAppearance(annot) {
|
|
1124
|
+
const [rx1, ry1, rx2, ry2] = annot.getRect();
|
|
1125
|
+
const w = rx2 - rx1;
|
|
1126
|
+
const h = ry2 - ry1;
|
|
1127
|
+
const color = annot.getColor();
|
|
1128
|
+
const opacity = annot.getOpacity();
|
|
1129
|
+
const dict = annot.getDict();
|
|
1130
|
+
const lObj = dict.get("/L");
|
|
1131
|
+
let lx1 = 0, ly1 = 0, lx2 = w, ly2 = h;
|
|
1132
|
+
if (lObj && lObj.kind === "array" && lObj.items.length >= 4) {
|
|
1133
|
+
lx1 = lObj.items[0]?.value ?? 0;
|
|
1134
|
+
ly1 = lObj.items[1]?.value ?? 0;
|
|
1135
|
+
lx2 = lObj.items[2]?.value ?? 0;
|
|
1136
|
+
ly2 = lObj.items[3]?.value ?? 0;
|
|
1137
|
+
lx1 -= rx1;
|
|
1138
|
+
ly1 -= ry1;
|
|
1139
|
+
lx2 -= rx1;
|
|
1140
|
+
ly2 -= ry1;
|
|
1141
|
+
}
|
|
1142
|
+
let borderWidth = 1;
|
|
1143
|
+
const bsObj = dict.get("/BS");
|
|
1144
|
+
if (bsObj && bsObj.kind === "dict") {
|
|
1145
|
+
const wObj = bsObj.get("/W");
|
|
1146
|
+
if (wObj && wObj.kind === "number") borderWidth = wObj.value;
|
|
1147
|
+
}
|
|
1148
|
+
let ops = "";
|
|
1149
|
+
if (opacity < 1) ops += `${n(opacity)} ca ${n(opacity)} CA\n`;
|
|
1150
|
+
if (color) ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1151
|
+
else ops += "0 0 0 RG\n";
|
|
1152
|
+
ops += `${n(borderWidth)} w\n`;
|
|
1153
|
+
ops += `${n(lx1)} ${n(ly1)} m\n`;
|
|
1154
|
+
ops += `${n(lx2)} ${n(ly2)} l\n`;
|
|
1155
|
+
ops += "S\n";
|
|
1156
|
+
return buildAppearanceStream(ops, [
|
|
1157
|
+
0,
|
|
1158
|
+
0,
|
|
1159
|
+
w,
|
|
1160
|
+
h
|
|
1161
|
+
]);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Generate appearance stream for a Highlight annotation.
|
|
1165
|
+
*/
|
|
1166
|
+
function generateHighlightAppearance(annot) {
|
|
1167
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1168
|
+
const w = x2 - x1;
|
|
1169
|
+
const h = y2 - y1;
|
|
1170
|
+
const color = annot.getColor() ?? {
|
|
1171
|
+
r: 1,
|
|
1172
|
+
g: 1,
|
|
1173
|
+
b: 0
|
|
1174
|
+
};
|
|
1175
|
+
const opacity = annot.getOpacity();
|
|
1176
|
+
let ops = "";
|
|
1177
|
+
if (opacity < 1) ops += `${n(opacity)} ca\n`;
|
|
1178
|
+
ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} rg\n`;
|
|
1179
|
+
const qpObj = annot.getDict().get("/QuadPoints");
|
|
1180
|
+
if (qpObj && qpObj.kind === "array" && qpObj.items.length >= 8) {
|
|
1181
|
+
const points = qpObj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1182
|
+
for (let i = 0; i + 7 < points.length; i += 8) {
|
|
1183
|
+
const qx1 = points[i] - x1;
|
|
1184
|
+
const qy1 = points[i + 1] - y1;
|
|
1185
|
+
const qx2 = points[i + 2] - x1;
|
|
1186
|
+
const qy2 = points[i + 3] - y1;
|
|
1187
|
+
const qx3 = points[i + 4] - x1;
|
|
1188
|
+
const qy3 = points[i + 5] - y1;
|
|
1189
|
+
const qx4 = points[i + 6] - x1;
|
|
1190
|
+
const qy4 = points[i + 7] - y1;
|
|
1191
|
+
ops += `${n(qx1)} ${n(qy1)} m\n`;
|
|
1192
|
+
ops += `${n(qx2)} ${n(qy2)} l\n`;
|
|
1193
|
+
ops += `${n(qx4)} ${n(qy4)} l\n`;
|
|
1194
|
+
ops += `${n(qx3)} ${n(qy3)} l\n`;
|
|
1195
|
+
ops += "f\n";
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
ops += `0 0 ${n(w)} ${n(h)} re\n`;
|
|
1199
|
+
ops += "f\n";
|
|
1200
|
+
}
|
|
1201
|
+
return buildAppearanceStream(ops, [
|
|
1202
|
+
0,
|
|
1203
|
+
0,
|
|
1204
|
+
w,
|
|
1205
|
+
h
|
|
1206
|
+
]);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Generate appearance stream for an Underline annotation.
|
|
1210
|
+
*/
|
|
1211
|
+
function generateUnderlineAppearance(annot) {
|
|
1212
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1213
|
+
const w = x2 - x1;
|
|
1214
|
+
const h = y2 - y1;
|
|
1215
|
+
const color = annot.getColor() ?? {
|
|
1216
|
+
r: 0,
|
|
1217
|
+
g: 0,
|
|
1218
|
+
b: 1
|
|
1219
|
+
};
|
|
1220
|
+
let ops = "";
|
|
1221
|
+
ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1222
|
+
ops += "1 w\n";
|
|
1223
|
+
const qpObj = annot.getDict().get("/QuadPoints");
|
|
1224
|
+
if (qpObj && qpObj.kind === "array" && qpObj.items.length >= 8) {
|
|
1225
|
+
const points = qpObj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1226
|
+
for (let i = 0; i + 7 < points.length; i += 8) {
|
|
1227
|
+
const bx1 = points[i + 4] - x1;
|
|
1228
|
+
const by1 = points[i + 5] - y1;
|
|
1229
|
+
const bx2 = points[i + 6] - x1;
|
|
1230
|
+
const by2 = points[i + 7] - y1;
|
|
1231
|
+
ops += `${n(bx1)} ${n(by1)} m\n`;
|
|
1232
|
+
ops += `${n(bx2)} ${n(by2)} l\n`;
|
|
1233
|
+
ops += "S\n";
|
|
1234
|
+
}
|
|
1235
|
+
} else {
|
|
1236
|
+
ops += `0 0 m\n`;
|
|
1237
|
+
ops += `${n(w)} 0 l\n`;
|
|
1238
|
+
ops += "S\n";
|
|
1239
|
+
}
|
|
1240
|
+
return buildAppearanceStream(ops, [
|
|
1241
|
+
0,
|
|
1242
|
+
0,
|
|
1243
|
+
w,
|
|
1244
|
+
h
|
|
1245
|
+
]);
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Generate appearance stream for a Squiggly annotation.
|
|
1249
|
+
*/
|
|
1250
|
+
function generateSquigglyAppearance(annot) {
|
|
1251
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1252
|
+
const w = x2 - x1;
|
|
1253
|
+
const h = y2 - y1;
|
|
1254
|
+
const color = annot.getColor() ?? {
|
|
1255
|
+
r: 0,
|
|
1256
|
+
g: .5,
|
|
1257
|
+
b: 0
|
|
1258
|
+
};
|
|
1259
|
+
let ops = "";
|
|
1260
|
+
ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1261
|
+
ops += "0.5 w\n";
|
|
1262
|
+
const amplitude = 2;
|
|
1263
|
+
const period = 4;
|
|
1264
|
+
ops += `0 ${n(amplitude)} m\n`;
|
|
1265
|
+
for (let x = 0; x < w; x += period) {
|
|
1266
|
+
const x1p = Math.min(x + period / 2, w);
|
|
1267
|
+
const x2p = Math.min(x + period, w);
|
|
1268
|
+
const y1p = x % (period * 2) < period ? 0 : amplitude * 2;
|
|
1269
|
+
const y2p = x % (period * 2) < period ? amplitude * 2 : 0;
|
|
1270
|
+
ops += `${n(x1p)} ${n(y1p)} l\n`;
|
|
1271
|
+
if (x2p <= w) ops += `${n(x2p)} ${n(y2p)} l\n`;
|
|
1272
|
+
}
|
|
1273
|
+
ops += "S\n";
|
|
1274
|
+
return buildAppearanceStream(ops, [
|
|
1275
|
+
0,
|
|
1276
|
+
0,
|
|
1277
|
+
w,
|
|
1278
|
+
h
|
|
1279
|
+
]);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Generate appearance stream for a StrikeOut annotation.
|
|
1283
|
+
*/
|
|
1284
|
+
function generateStrikeOutAppearance(annot) {
|
|
1285
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1286
|
+
const w = x2 - x1;
|
|
1287
|
+
const h = y2 - y1;
|
|
1288
|
+
const color = annot.getColor() ?? {
|
|
1289
|
+
r: 1,
|
|
1290
|
+
g: 0,
|
|
1291
|
+
b: 0
|
|
1292
|
+
};
|
|
1293
|
+
let ops = "";
|
|
1294
|
+
ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1295
|
+
ops += "1 w\n";
|
|
1296
|
+
const qpObj = annot.getDict().get("/QuadPoints");
|
|
1297
|
+
if (qpObj && qpObj.kind === "array" && qpObj.items.length >= 8) {
|
|
1298
|
+
const points = qpObj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1299
|
+
for (let i = 0; i + 7 < points.length; i += 8) {
|
|
1300
|
+
const midY = ((points[i + 1] - y1 + (points[i + 3] - y1)) / 2 + (points[i + 5] - y1 + (points[i + 7] - y1)) / 2) / 2;
|
|
1301
|
+
const leftX = Math.min(points[i] - x1, points[i + 4] - x1);
|
|
1302
|
+
const rightX = Math.max(points[i + 2] - x1, points[i + 6] - x1);
|
|
1303
|
+
ops += `${n(leftX)} ${n(midY)} m\n`;
|
|
1304
|
+
ops += `${n(rightX)} ${n(midY)} l\n`;
|
|
1305
|
+
ops += "S\n";
|
|
1306
|
+
}
|
|
1307
|
+
} else {
|
|
1308
|
+
const midY = h / 2;
|
|
1309
|
+
ops += `0 ${n(midY)} m\n`;
|
|
1310
|
+
ops += `${n(w)} ${n(midY)} l\n`;
|
|
1311
|
+
ops += "S\n";
|
|
1312
|
+
}
|
|
1313
|
+
return buildAppearanceStream(ops, [
|
|
1314
|
+
0,
|
|
1315
|
+
0,
|
|
1316
|
+
w,
|
|
1317
|
+
h
|
|
1318
|
+
]);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Generate appearance stream for an Ink annotation.
|
|
1322
|
+
*/
|
|
1323
|
+
function generateInkAppearance(annot) {
|
|
1324
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1325
|
+
const w = x2 - x1;
|
|
1326
|
+
const h = y2 - y1;
|
|
1327
|
+
const color = annot.getColor() ?? {
|
|
1328
|
+
r: 0,
|
|
1329
|
+
g: 0,
|
|
1330
|
+
b: 0
|
|
1331
|
+
};
|
|
1332
|
+
const opacity = annot.getOpacity();
|
|
1333
|
+
let ops = "";
|
|
1334
|
+
if (opacity < 1) ops += `${n(opacity)} ca ${n(opacity)} CA\n`;
|
|
1335
|
+
ops += `${n(color.r)} ${n(color.g)} ${n(color.b)} RG\n`;
|
|
1336
|
+
ops += "1 w\n";
|
|
1337
|
+
ops += "1 J\n";
|
|
1338
|
+
const inkListObj = annot.getDict().get("/InkList");
|
|
1339
|
+
if (inkListObj && inkListObj.kind === "array") for (const pathObj of inkListObj.items) {
|
|
1340
|
+
if (pathObj.kind !== "array") continue;
|
|
1341
|
+
const points = pathObj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1342
|
+
if (points.length >= 2) {
|
|
1343
|
+
ops += `${n(points[0] - x1)} ${n(points[1] - y1)} m\n`;
|
|
1344
|
+
for (let i = 2; i + 1 < points.length; i += 2) ops += `${n(points[i] - x1)} ${n(points[i + 1] - y1)} l\n`;
|
|
1345
|
+
ops += "S\n";
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return buildAppearanceStream(ops, [
|
|
1349
|
+
0,
|
|
1350
|
+
0,
|
|
1351
|
+
w,
|
|
1352
|
+
h
|
|
1353
|
+
]);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Generate appearance stream for a FreeText annotation.
|
|
1357
|
+
*
|
|
1358
|
+
* This requires access to the annotation's text, default appearance
|
|
1359
|
+
* string, and alignment. We accept the annotation object directly
|
|
1360
|
+
* to access these properties.
|
|
1361
|
+
*/
|
|
1362
|
+
function generateFreeTextAppearance(annot) {
|
|
1363
|
+
const [x1, y1, x2, y2] = annot.getRect();
|
|
1364
|
+
const w = x2 - x1;
|
|
1365
|
+
const h = y2 - y1;
|
|
1366
|
+
const text = annot.getContents() ?? "";
|
|
1367
|
+
const dict = annot.getDict();
|
|
1368
|
+
let da = "0 0 0 rg /Helv 12 Tf";
|
|
1369
|
+
const daObj = dict.get("/DA");
|
|
1370
|
+
if (daObj && daObj.kind === "string") da = daObj.value;
|
|
1371
|
+
let align = 0;
|
|
1372
|
+
const qObj = dict.get("/Q");
|
|
1373
|
+
if (qObj && qObj.kind === "number") align = qObj.value;
|
|
1374
|
+
let fontSize = 12;
|
|
1375
|
+
const fsMatch = /\/\w+\s+([\d.]+)\s+Tf/.exec(da);
|
|
1376
|
+
if (fsMatch?.[1]) fontSize = parseFloat(fsMatch[1]);
|
|
1377
|
+
let ops = "";
|
|
1378
|
+
ops += `${da}\n`;
|
|
1379
|
+
ops += "BT\n";
|
|
1380
|
+
const margin = 2;
|
|
1381
|
+
const textY = h - fontSize - margin;
|
|
1382
|
+
if (align === 1) ops += `${n(w / 2)} ${n(textY)} Td\n`;
|
|
1383
|
+
else if (align === 2) ops += `${n(w - margin)} ${n(textY)} Td\n`;
|
|
1384
|
+
else ops += `${n(margin)} ${n(textY)} Td\n`;
|
|
1385
|
+
const escapedText = text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
1386
|
+
ops += `(${escapedText}) Tj\n`;
|
|
1387
|
+
ops += "ET\n";
|
|
1388
|
+
const resources = new PdfDict();
|
|
1389
|
+
const fontDict = new PdfDict();
|
|
1390
|
+
const helvetica = new PdfDict();
|
|
1391
|
+
helvetica.set("/Type", PdfName.of("Font"));
|
|
1392
|
+
helvetica.set("/Subtype", PdfName.of("Type1"));
|
|
1393
|
+
helvetica.set("/BaseFont", PdfName.of("Helvetica"));
|
|
1394
|
+
fontDict.set("/Helv", helvetica);
|
|
1395
|
+
resources.set("/Font", fontDict);
|
|
1396
|
+
return buildAppearanceStream(ops, [
|
|
1397
|
+
0,
|
|
1398
|
+
0,
|
|
1399
|
+
w,
|
|
1400
|
+
h
|
|
1401
|
+
], resources);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/annotation/types/freeTextAnnotation.ts
|
|
1406
|
+
/**
|
|
1407
|
+
* @module annotation/types/freeTextAnnotation
|
|
1408
|
+
*
|
|
1409
|
+
* FreeText annotation — displays text directly on the page without
|
|
1410
|
+
* requiring the user to click an icon.
|
|
1411
|
+
*
|
|
1412
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.6 (Free Text Annotations).
|
|
1413
|
+
*/
|
|
1414
|
+
const ALIGNMENT_MAP = {
|
|
1415
|
+
left: 0,
|
|
1416
|
+
center: 1,
|
|
1417
|
+
right: 2
|
|
1418
|
+
};
|
|
1419
|
+
const ALIGNMENT_REVERSE = {
|
|
1420
|
+
0: "left",
|
|
1421
|
+
1: "center",
|
|
1422
|
+
2: "right"
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* A free text annotation (subtype /FreeText).
|
|
1426
|
+
*
|
|
1427
|
+
* Displays text directly on the page as if it were part of the page
|
|
1428
|
+
* content. Does not require opening a popup.
|
|
1429
|
+
*/
|
|
1430
|
+
var PdfFreeTextAnnotation = class PdfFreeTextAnnotation extends PdfAnnotation {
|
|
1431
|
+
constructor(dict) {
|
|
1432
|
+
super("FreeText", dict);
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Create a new free text annotation.
|
|
1436
|
+
*/
|
|
1437
|
+
static create(options) {
|
|
1438
|
+
const annot = new PdfFreeTextAnnotation(buildAnnotationDict("FreeText", options));
|
|
1439
|
+
const fontSize = options.fontSize ?? 12;
|
|
1440
|
+
const da = options.defaultAppearance ?? `0 0 0 rg /Helv ${fontSize} Tf`;
|
|
1441
|
+
annot.setDefaultAppearance(da);
|
|
1442
|
+
if (options.text !== void 0) annot.setText(options.text);
|
|
1443
|
+
if (options.alignment !== void 0) annot.setAlignment(options.alignment);
|
|
1444
|
+
return annot;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Create from an existing dictionary.
|
|
1448
|
+
*/
|
|
1449
|
+
static fromDict(dict, resolver) {
|
|
1450
|
+
return new PdfFreeTextAnnotation(dict);
|
|
1451
|
+
}
|
|
1452
|
+
/** Get the displayed text. */
|
|
1453
|
+
getText() {
|
|
1454
|
+
return this.getContents() ?? "";
|
|
1455
|
+
}
|
|
1456
|
+
/** Set the displayed text. */
|
|
1457
|
+
setText(text) {
|
|
1458
|
+
this.setContents(text);
|
|
1459
|
+
}
|
|
1460
|
+
/** Get the font size from the default appearance string. */
|
|
1461
|
+
getFontSize() {
|
|
1462
|
+
const da = this.getDefaultAppearance();
|
|
1463
|
+
const match = /\/\w+\s+([\d.]+)\s+Tf/.exec(da);
|
|
1464
|
+
if (match?.[1]) return parseFloat(match[1]);
|
|
1465
|
+
return 12;
|
|
1466
|
+
}
|
|
1467
|
+
/** Set the font size (rebuilds the default appearance string). */
|
|
1468
|
+
setFontSize(size) {
|
|
1469
|
+
let da = this.getDefaultAppearance();
|
|
1470
|
+
const replaced = da.replace(/(\/\w+)\s+[\d.]+\s+Tf/, `$1 ${size} Tf`);
|
|
1471
|
+
if (replaced !== da) this.setDefaultAppearance(replaced);
|
|
1472
|
+
else this.setDefaultAppearance(`${da} /Helv ${size} Tf`);
|
|
1473
|
+
}
|
|
1474
|
+
/** Get the text alignment. Defaults to 'left'. */
|
|
1475
|
+
getAlignment() {
|
|
1476
|
+
const obj = this.dict.get("/Q");
|
|
1477
|
+
if (obj && obj.kind === "number") return ALIGNMENT_REVERSE[obj.value] ?? "left";
|
|
1478
|
+
return "left";
|
|
1479
|
+
}
|
|
1480
|
+
/** Set the text alignment. */
|
|
1481
|
+
setAlignment(align) {
|
|
1482
|
+
this.dict.set("/Q", PdfNumber.of(ALIGNMENT_MAP[align]));
|
|
1483
|
+
}
|
|
1484
|
+
/** Get the default appearance string (/DA). */
|
|
1485
|
+
getDefaultAppearance() {
|
|
1486
|
+
const obj = this.dict.get("/DA");
|
|
1487
|
+
if (obj && obj.kind === "string") return obj.value;
|
|
1488
|
+
return "0 0 0 rg /Helv 12 Tf";
|
|
1489
|
+
}
|
|
1490
|
+
/** Set the default appearance string. */
|
|
1491
|
+
setDefaultAppearance(da) {
|
|
1492
|
+
this.dict.set("/DA", PdfString.literal(da));
|
|
1493
|
+
}
|
|
1494
|
+
/** Generate the appearance stream for this free text annotation. */
|
|
1495
|
+
generateAppearance() {
|
|
1496
|
+
return generateFreeTextAppearance(this);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
//#endregion
|
|
1501
|
+
//#region src/annotation/types/markupAnnotations.ts
|
|
1502
|
+
/**
|
|
1503
|
+
* @module annotation/types/markupAnnotations
|
|
1504
|
+
*
|
|
1505
|
+
* Text markup annotations: Highlight, Underline, Squiggly, StrikeOut.
|
|
1506
|
+
*
|
|
1507
|
+
* These annotations use QuadPoints to define the regions of text they
|
|
1508
|
+
* mark. Each quad is 8 numbers: the four corners of a quadrilateral
|
|
1509
|
+
* covering the text.
|
|
1510
|
+
*
|
|
1511
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.10 (Text Markup Annotations).
|
|
1512
|
+
*/
|
|
1513
|
+
/** Extract QuadPoints from a dictionary. */
|
|
1514
|
+
function getQuadPointsFromDict(dict) {
|
|
1515
|
+
const obj = dict.get("/QuadPoints");
|
|
1516
|
+
if (obj && obj.kind === "array") return obj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1517
|
+
return [];
|
|
1518
|
+
}
|
|
1519
|
+
/** Set QuadPoints on a dictionary. */
|
|
1520
|
+
function setQuadPointsOnDict(dict, points) {
|
|
1521
|
+
dict.set("/QuadPoints", PdfArray.fromNumbers(points));
|
|
1522
|
+
}
|
|
1523
|
+
/** Create quad points from a rectangle (for convenience). */
|
|
1524
|
+
function rectToQuadPoints(rect) {
|
|
1525
|
+
const [x1, y1, x2, y2] = rect;
|
|
1526
|
+
return [
|
|
1527
|
+
x1,
|
|
1528
|
+
y2,
|
|
1529
|
+
x2,
|
|
1530
|
+
y2,
|
|
1531
|
+
x1,
|
|
1532
|
+
y1,
|
|
1533
|
+
x2,
|
|
1534
|
+
y1
|
|
1535
|
+
];
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Highlight annotation (subtype /Highlight).
|
|
1539
|
+
*
|
|
1540
|
+
* Highlights text with a translucent colour overlay.
|
|
1541
|
+
*/
|
|
1542
|
+
var PdfHighlightAnnotation = class PdfHighlightAnnotation extends PdfAnnotation {
|
|
1543
|
+
constructor(dict) {
|
|
1544
|
+
super("Highlight", dict);
|
|
1545
|
+
}
|
|
1546
|
+
/** Create a new highlight annotation. */
|
|
1547
|
+
static create(options) {
|
|
1548
|
+
const annot = new PdfHighlightAnnotation(buildAnnotationDict("Highlight", options));
|
|
1549
|
+
const qp = options.quadPoints ?? rectToQuadPoints(options.rect);
|
|
1550
|
+
annot.setQuadPoints(qp);
|
|
1551
|
+
return annot;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Convenience: create a highlight for a rectangle region.
|
|
1555
|
+
*/
|
|
1556
|
+
static createForRect(rect, color) {
|
|
1557
|
+
return PdfHighlightAnnotation.create({
|
|
1558
|
+
rect,
|
|
1559
|
+
color: color ?? {
|
|
1560
|
+
r: 1,
|
|
1561
|
+
g: 1,
|
|
1562
|
+
b: 0
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
static fromDict(dict, resolver) {
|
|
1567
|
+
return new PdfHighlightAnnotation(dict);
|
|
1568
|
+
}
|
|
1569
|
+
/** Get the quad points array. */
|
|
1570
|
+
getQuadPoints() {
|
|
1571
|
+
return getQuadPointsFromDict(this.dict);
|
|
1572
|
+
}
|
|
1573
|
+
/** Set the quad points array. */
|
|
1574
|
+
setQuadPoints(points) {
|
|
1575
|
+
setQuadPointsOnDict(this.dict, points);
|
|
1576
|
+
}
|
|
1577
|
+
generateAppearance() {
|
|
1578
|
+
return generateHighlightAppearance(this);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
/**
|
|
1582
|
+
* Underline annotation (subtype /Underline).
|
|
1583
|
+
*/
|
|
1584
|
+
var PdfUnderlineAnnotation = class PdfUnderlineAnnotation extends PdfAnnotation {
|
|
1585
|
+
constructor(dict) {
|
|
1586
|
+
super("Underline", dict);
|
|
1587
|
+
}
|
|
1588
|
+
static create(options) {
|
|
1589
|
+
const annot = new PdfUnderlineAnnotation(buildAnnotationDict("Underline", options));
|
|
1590
|
+
const qp = options.quadPoints ?? rectToQuadPoints(options.rect);
|
|
1591
|
+
annot.setQuadPoints(qp);
|
|
1592
|
+
return annot;
|
|
1593
|
+
}
|
|
1594
|
+
static fromDict(dict, resolver) {
|
|
1595
|
+
return new PdfUnderlineAnnotation(dict);
|
|
1596
|
+
}
|
|
1597
|
+
/** Get the quad points array. */
|
|
1598
|
+
getQuadPoints() {
|
|
1599
|
+
return getQuadPointsFromDict(this.dict);
|
|
1600
|
+
}
|
|
1601
|
+
/** Set the quad points array. */
|
|
1602
|
+
setQuadPoints(points) {
|
|
1603
|
+
setQuadPointsOnDict(this.dict, points);
|
|
1604
|
+
}
|
|
1605
|
+
generateAppearance() {
|
|
1606
|
+
return generateUnderlineAppearance(this);
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
/**
|
|
1610
|
+
* Squiggly underline annotation (subtype /Squiggly).
|
|
1611
|
+
*/
|
|
1612
|
+
var PdfSquigglyAnnotation = class PdfSquigglyAnnotation extends PdfAnnotation {
|
|
1613
|
+
constructor(dict) {
|
|
1614
|
+
super("Squiggly", dict);
|
|
1615
|
+
}
|
|
1616
|
+
static create(options) {
|
|
1617
|
+
const annot = new PdfSquigglyAnnotation(buildAnnotationDict("Squiggly", options));
|
|
1618
|
+
const qp = options.quadPoints ?? rectToQuadPoints(options.rect);
|
|
1619
|
+
annot.setQuadPoints(qp);
|
|
1620
|
+
return annot;
|
|
1621
|
+
}
|
|
1622
|
+
static fromDict(dict, resolver) {
|
|
1623
|
+
return new PdfSquigglyAnnotation(dict);
|
|
1624
|
+
}
|
|
1625
|
+
/** Get the quad points array. */
|
|
1626
|
+
getQuadPoints() {
|
|
1627
|
+
return getQuadPointsFromDict(this.dict);
|
|
1628
|
+
}
|
|
1629
|
+
/** Set the quad points array. */
|
|
1630
|
+
setQuadPoints(points) {
|
|
1631
|
+
setQuadPointsOnDict(this.dict, points);
|
|
1632
|
+
}
|
|
1633
|
+
generateAppearance() {
|
|
1634
|
+
return generateSquigglyAppearance(this);
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
/**
|
|
1638
|
+
* Strike-out annotation (subtype /StrikeOut).
|
|
1639
|
+
*/
|
|
1640
|
+
var PdfStrikeOutAnnotation = class PdfStrikeOutAnnotation extends PdfAnnotation {
|
|
1641
|
+
constructor(dict) {
|
|
1642
|
+
super("StrikeOut", dict);
|
|
1643
|
+
}
|
|
1644
|
+
static create(options) {
|
|
1645
|
+
const annot = new PdfStrikeOutAnnotation(buildAnnotationDict("StrikeOut", options));
|
|
1646
|
+
const qp = options.quadPoints ?? rectToQuadPoints(options.rect);
|
|
1647
|
+
annot.setQuadPoints(qp);
|
|
1648
|
+
return annot;
|
|
1649
|
+
}
|
|
1650
|
+
static fromDict(dict, resolver) {
|
|
1651
|
+
return new PdfStrikeOutAnnotation(dict);
|
|
1652
|
+
}
|
|
1653
|
+
/** Get the quad points array. */
|
|
1654
|
+
getQuadPoints() {
|
|
1655
|
+
return getQuadPointsFromDict(this.dict);
|
|
1656
|
+
}
|
|
1657
|
+
/** Set the quad points array. */
|
|
1658
|
+
setQuadPoints(points) {
|
|
1659
|
+
setQuadPointsOnDict(this.dict, points);
|
|
1660
|
+
}
|
|
1661
|
+
generateAppearance() {
|
|
1662
|
+
return generateStrikeOutAppearance(this);
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/annotation/types/shapeAnnotations.ts
|
|
1668
|
+
/**
|
|
1669
|
+
* @module annotation/types/shapeAnnotations
|
|
1670
|
+
*
|
|
1671
|
+
* Geometric shape annotations: Line, Square, Circle, Polygon, PolyLine.
|
|
1672
|
+
*
|
|
1673
|
+
* Reference: PDF 1.7 spec, Sections 12.5.6.7-12.5.6.9.
|
|
1674
|
+
*/
|
|
1675
|
+
function getInteriorColor(dict) {
|
|
1676
|
+
const obj = dict.get("/IC");
|
|
1677
|
+
if (obj && obj.kind === "array" && obj.items.length >= 3) return {
|
|
1678
|
+
r: obj.items[0]?.value ?? 0,
|
|
1679
|
+
g: obj.items[1]?.value ?? 0,
|
|
1680
|
+
b: obj.items[2]?.value ?? 0
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function setInteriorColor(dict, color) {
|
|
1684
|
+
dict.set("/IC", PdfArray.fromNumbers([
|
|
1685
|
+
color.r,
|
|
1686
|
+
color.g,
|
|
1687
|
+
color.b
|
|
1688
|
+
]));
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Line annotation (subtype /Line).
|
|
1692
|
+
*
|
|
1693
|
+
* Draws a straight line between two points on the page.
|
|
1694
|
+
*/
|
|
1695
|
+
var PdfLineAnnotation = class PdfLineAnnotation extends PdfAnnotation {
|
|
1696
|
+
constructor(dict) {
|
|
1697
|
+
super("Line", dict);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Create a new line annotation.
|
|
1701
|
+
*/
|
|
1702
|
+
static create(options) {
|
|
1703
|
+
const annot = new PdfLineAnnotation(buildAnnotationDict("Line", options));
|
|
1704
|
+
if (options.linePoints !== void 0) annot.setLinePoints(options.linePoints);
|
|
1705
|
+
if (options.lineEndingStart !== void 0 || options.lineEndingEnd !== void 0) annot.setLineEndingStyles(options.lineEndingStart ?? "None", options.lineEndingEnd ?? "None");
|
|
1706
|
+
return annot;
|
|
1707
|
+
}
|
|
1708
|
+
static fromDict(dict, resolver) {
|
|
1709
|
+
return new PdfLineAnnotation(dict);
|
|
1710
|
+
}
|
|
1711
|
+
/** Get the line endpoints [x1, y1, x2, y2]. */
|
|
1712
|
+
getLinePoints() {
|
|
1713
|
+
const obj = this.dict.get("/L");
|
|
1714
|
+
if (obj && obj.kind === "array" && obj.items.length >= 4) return [
|
|
1715
|
+
obj.items[0]?.value ?? 0,
|
|
1716
|
+
obj.items[1]?.value ?? 0,
|
|
1717
|
+
obj.items[2]?.value ?? 0,
|
|
1718
|
+
obj.items[3]?.value ?? 0
|
|
1719
|
+
];
|
|
1720
|
+
return [
|
|
1721
|
+
0,
|
|
1722
|
+
0,
|
|
1723
|
+
0,
|
|
1724
|
+
0
|
|
1725
|
+
];
|
|
1726
|
+
}
|
|
1727
|
+
/** Set the line endpoints. */
|
|
1728
|
+
setLinePoints(points) {
|
|
1729
|
+
this.dict.set("/L", PdfArray.fromNumbers(points));
|
|
1730
|
+
}
|
|
1731
|
+
/** Get the line ending styles [start, end]. */
|
|
1732
|
+
getLineEndingStyles() {
|
|
1733
|
+
const obj = this.dict.get("/LE");
|
|
1734
|
+
if (obj && obj.kind === "array" && obj.items.length >= 2) {
|
|
1735
|
+
const start = obj.items[0];
|
|
1736
|
+
const end = obj.items[1];
|
|
1737
|
+
return [start && start.kind === "name" ? start.value.startsWith("/") ? start.value.slice(1) : start.value : "None", end && end.kind === "name" ? end.value.startsWith("/") ? end.value.slice(1) : end.value : "None"];
|
|
1738
|
+
}
|
|
1739
|
+
return ["None", "None"];
|
|
1740
|
+
}
|
|
1741
|
+
/** Set the line ending styles. */
|
|
1742
|
+
setLineEndingStyles(start, end) {
|
|
1743
|
+
this.dict.set("/LE", PdfArray.of([PdfName.of(start), PdfName.of(end)]));
|
|
1744
|
+
}
|
|
1745
|
+
generateAppearance() {
|
|
1746
|
+
return generateLineAppearance(this);
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
/**
|
|
1750
|
+
* Square annotation (subtype /Square).
|
|
1751
|
+
*
|
|
1752
|
+
* Draws a rectangle on the page.
|
|
1753
|
+
*/
|
|
1754
|
+
var PdfSquareAnnotation = class PdfSquareAnnotation extends PdfAnnotation {
|
|
1755
|
+
constructor(dict) {
|
|
1756
|
+
super("Square", dict);
|
|
1757
|
+
}
|
|
1758
|
+
static create(options) {
|
|
1759
|
+
const annot = new PdfSquareAnnotation(buildAnnotationDict("Square", options));
|
|
1760
|
+
if (options.interiorColor !== void 0) annot.setInteriorColor(options.interiorColor);
|
|
1761
|
+
return annot;
|
|
1762
|
+
}
|
|
1763
|
+
static fromDict(dict, resolver) {
|
|
1764
|
+
return new PdfSquareAnnotation(dict);
|
|
1765
|
+
}
|
|
1766
|
+
/** Get the interior (fill) color. */
|
|
1767
|
+
getInteriorColor() {
|
|
1768
|
+
return getInteriorColor(this.dict);
|
|
1769
|
+
}
|
|
1770
|
+
/** Set the interior (fill) color. */
|
|
1771
|
+
setInteriorColor(color) {
|
|
1772
|
+
setInteriorColor(this.dict, color);
|
|
1773
|
+
}
|
|
1774
|
+
generateAppearance() {
|
|
1775
|
+
return generateSquareAppearance(this);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
/**
|
|
1779
|
+
* Circle annotation (subtype /Circle).
|
|
1780
|
+
*
|
|
1781
|
+
* Draws an ellipse inscribed within the annotation rectangle.
|
|
1782
|
+
*/
|
|
1783
|
+
var PdfCircleAnnotation = class PdfCircleAnnotation extends PdfAnnotation {
|
|
1784
|
+
constructor(dict) {
|
|
1785
|
+
super("Circle", dict);
|
|
1786
|
+
}
|
|
1787
|
+
static create(options) {
|
|
1788
|
+
const annot = new PdfCircleAnnotation(buildAnnotationDict("Circle", options));
|
|
1789
|
+
if (options.interiorColor !== void 0) annot.setInteriorColor(options.interiorColor);
|
|
1790
|
+
return annot;
|
|
1791
|
+
}
|
|
1792
|
+
static fromDict(dict, resolver) {
|
|
1793
|
+
return new PdfCircleAnnotation(dict);
|
|
1794
|
+
}
|
|
1795
|
+
/** Get the interior (fill) color. */
|
|
1796
|
+
getInteriorColor() {
|
|
1797
|
+
return getInteriorColor(this.dict);
|
|
1798
|
+
}
|
|
1799
|
+
/** Set the interior (fill) color. */
|
|
1800
|
+
setInteriorColor(color) {
|
|
1801
|
+
setInteriorColor(this.dict, color);
|
|
1802
|
+
}
|
|
1803
|
+
generateAppearance() {
|
|
1804
|
+
return generateCircleAppearance(this);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
/**
|
|
1808
|
+
* Polygon annotation (subtype /Polygon).
|
|
1809
|
+
*
|
|
1810
|
+
* Draws a closed polygon on the page.
|
|
1811
|
+
*/
|
|
1812
|
+
var PdfPolygonAnnotation = class PdfPolygonAnnotation extends PdfAnnotation {
|
|
1813
|
+
constructor(dict) {
|
|
1814
|
+
super("Polygon", dict);
|
|
1815
|
+
}
|
|
1816
|
+
static create(options) {
|
|
1817
|
+
const annot = new PdfPolygonAnnotation(buildAnnotationDict("Polygon", options));
|
|
1818
|
+
if (options.vertices !== void 0) annot.setVertices(options.vertices);
|
|
1819
|
+
if (options.interiorColor !== void 0) annot.setInteriorColor(options.interiorColor);
|
|
1820
|
+
return annot;
|
|
1821
|
+
}
|
|
1822
|
+
static fromDict(dict, resolver) {
|
|
1823
|
+
return new PdfPolygonAnnotation(dict);
|
|
1824
|
+
}
|
|
1825
|
+
/** Get the polygon vertices as a flat array [x1,y1,x2,y2,...]. */
|
|
1826
|
+
getVertices() {
|
|
1827
|
+
const obj = this.dict.get("/Vertices");
|
|
1828
|
+
if (obj && obj.kind === "array") return obj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1829
|
+
return [];
|
|
1830
|
+
}
|
|
1831
|
+
/** Set the polygon vertices. */
|
|
1832
|
+
setVertices(vertices) {
|
|
1833
|
+
this.dict.set("/Vertices", PdfArray.fromNumbers(vertices));
|
|
1834
|
+
}
|
|
1835
|
+
/** Get the interior (fill) color. */
|
|
1836
|
+
getInteriorColor() {
|
|
1837
|
+
return getInteriorColor(this.dict);
|
|
1838
|
+
}
|
|
1839
|
+
/** Set the interior (fill) color. */
|
|
1840
|
+
setInteriorColor(color) {
|
|
1841
|
+
setInteriorColor(this.dict, color);
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
/**
|
|
1845
|
+
* PolyLine annotation (subtype /PolyLine).
|
|
1846
|
+
*
|
|
1847
|
+
* Draws an open polyline (series of connected line segments).
|
|
1848
|
+
*/
|
|
1849
|
+
var PdfPolyLineAnnotation = class PdfPolyLineAnnotation extends PdfAnnotation {
|
|
1850
|
+
constructor(dict) {
|
|
1851
|
+
super("PolyLine", dict);
|
|
1852
|
+
}
|
|
1853
|
+
static create(options) {
|
|
1854
|
+
const annot = new PdfPolyLineAnnotation(buildAnnotationDict("PolyLine", options));
|
|
1855
|
+
if (options.vertices !== void 0) annot.setVertices(options.vertices);
|
|
1856
|
+
return annot;
|
|
1857
|
+
}
|
|
1858
|
+
static fromDict(dict, resolver) {
|
|
1859
|
+
return new PdfPolyLineAnnotation(dict);
|
|
1860
|
+
}
|
|
1861
|
+
/** Get the polyline vertices as a flat array [x1,y1,x2,y2,...]. */
|
|
1862
|
+
getVertices() {
|
|
1863
|
+
const obj = this.dict.get("/Vertices");
|
|
1864
|
+
if (obj && obj.kind === "array") return obj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
1865
|
+
return [];
|
|
1866
|
+
}
|
|
1867
|
+
/** Set the polyline vertices. */
|
|
1868
|
+
setVertices(vertices) {
|
|
1869
|
+
this.dict.set("/Vertices", PdfArray.fromNumbers(vertices));
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/annotation/types/stampAnnotation.ts
|
|
1875
|
+
/**
|
|
1876
|
+
* @module annotation/types/stampAnnotation
|
|
1877
|
+
*
|
|
1878
|
+
* Stamp annotation — displays a predefined or custom rubber stamp
|
|
1879
|
+
* on the page (e.g. "Approved", "Draft", "Confidential").
|
|
1880
|
+
*
|
|
1881
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.12 (Rubber Stamp Annotations).
|
|
1882
|
+
*/
|
|
1883
|
+
/**
|
|
1884
|
+
* A stamp annotation (subtype /Stamp).
|
|
1885
|
+
*
|
|
1886
|
+
* Displays a graphical stamp on the page, similar to a rubber stamp
|
|
1887
|
+
* applied to a physical document.
|
|
1888
|
+
*/
|
|
1889
|
+
var PdfStampAnnotation = class PdfStampAnnotation extends PdfAnnotation {
|
|
1890
|
+
constructor(dict) {
|
|
1891
|
+
super("Stamp", dict);
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Create a new stamp annotation.
|
|
1895
|
+
*/
|
|
1896
|
+
static create(options) {
|
|
1897
|
+
const annot = new PdfStampAnnotation(buildAnnotationDict("Stamp", options));
|
|
1898
|
+
annot.setStampName(options.stampName ?? "Draft");
|
|
1899
|
+
return annot;
|
|
1900
|
+
}
|
|
1901
|
+
static fromDict(dict, resolver) {
|
|
1902
|
+
return new PdfStampAnnotation(dict);
|
|
1903
|
+
}
|
|
1904
|
+
/** Get the stamp name (e.g. 'Approved', 'Draft'). */
|
|
1905
|
+
getStampName() {
|
|
1906
|
+
const obj = this.dict.get("/Name");
|
|
1907
|
+
if (obj && obj.kind === "name") return obj.value.startsWith("/") ? obj.value.slice(1) : obj.value;
|
|
1908
|
+
return "Draft";
|
|
1909
|
+
}
|
|
1910
|
+
/** Set the stamp name. */
|
|
1911
|
+
setStampName(name) {
|
|
1912
|
+
this.dict.set("/Name", PdfName.of(name));
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
//#endregion
|
|
1917
|
+
//#region src/annotation/types/inkAnnotation.ts
|
|
1918
|
+
/**
|
|
1919
|
+
* @module annotation/types/inkAnnotation
|
|
1920
|
+
*
|
|
1921
|
+
* Ink annotation — represents freehand "scribble" composed of one
|
|
1922
|
+
* or more disjoint paths (ink lists).
|
|
1923
|
+
*
|
|
1924
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.13 (Ink Annotations).
|
|
1925
|
+
*/
|
|
1926
|
+
/**
|
|
1927
|
+
* An ink annotation (subtype /Ink).
|
|
1928
|
+
*
|
|
1929
|
+
* Contains one or more ink paths, each being an array of coordinate
|
|
1930
|
+
* pairs [x1,y1,x2,y2,...] representing a freehand stroke.
|
|
1931
|
+
*/
|
|
1932
|
+
var PdfInkAnnotation = class PdfInkAnnotation extends PdfAnnotation {
|
|
1933
|
+
constructor(dict) {
|
|
1934
|
+
super("Ink", dict);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Create a new ink annotation.
|
|
1938
|
+
*/
|
|
1939
|
+
static create(options) {
|
|
1940
|
+
const annot = new PdfInkAnnotation(buildAnnotationDict("Ink", options));
|
|
1941
|
+
if (options.inkLists !== void 0) for (const list of options.inkLists) annot.addInkList(list);
|
|
1942
|
+
return annot;
|
|
1943
|
+
}
|
|
1944
|
+
static fromDict(dict, resolver) {
|
|
1945
|
+
return new PdfInkAnnotation(dict);
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Get all ink lists.
|
|
1949
|
+
*
|
|
1950
|
+
* Each ink list is an array of numbers [x1,y1,x2,y2,...] representing
|
|
1951
|
+
* a single stroke path.
|
|
1952
|
+
*/
|
|
1953
|
+
getInkLists() {
|
|
1954
|
+
const obj = this.dict.get("/InkList");
|
|
1955
|
+
if (!obj || obj.kind !== "array") return [];
|
|
1956
|
+
const lists = [];
|
|
1957
|
+
for (const item of obj.items) if (item.kind === "array") {
|
|
1958
|
+
const points = item.items.filter((p) => p.kind === "number").map((p) => p.value);
|
|
1959
|
+
lists.push(points);
|
|
1960
|
+
}
|
|
1961
|
+
return lists;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Add a new ink stroke path.
|
|
1965
|
+
*
|
|
1966
|
+
* @param points Array of coordinate pairs [x1,y1,x2,y2,...].
|
|
1967
|
+
*/
|
|
1968
|
+
addInkList(points) {
|
|
1969
|
+
let inkListArr = this.dict.get("/InkList");
|
|
1970
|
+
if (!inkListArr || inkListArr.kind !== "array") {
|
|
1971
|
+
inkListArr = new PdfArray();
|
|
1972
|
+
this.dict.set("/InkList", inkListArr);
|
|
1973
|
+
}
|
|
1974
|
+
inkListArr.push(PdfArray.fromNumbers(points));
|
|
1975
|
+
}
|
|
1976
|
+
/** Remove all ink stroke paths. */
|
|
1977
|
+
clearInkLists() {
|
|
1978
|
+
this.dict.set("/InkList", new PdfArray());
|
|
1979
|
+
}
|
|
1980
|
+
generateAppearance() {
|
|
1981
|
+
return generateInkAppearance(this);
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
//#endregion
|
|
1986
|
+
//#region src/annotation/types/redactAnnotation.ts
|
|
1987
|
+
/**
|
|
1988
|
+
* @module annotation/types/redactAnnotation
|
|
1989
|
+
*
|
|
1990
|
+
* Redact annotation — marks a region of the page for redaction.
|
|
1991
|
+
* When applied, the redacted content is permanently removed and
|
|
1992
|
+
* the area is covered with an overlay.
|
|
1993
|
+
*
|
|
1994
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.23 (Redaction Annotations).
|
|
1995
|
+
*/
|
|
1996
|
+
/**
|
|
1997
|
+
* A redaction annotation (subtype /Redact).
|
|
1998
|
+
*
|
|
1999
|
+
* Marks content for redaction. The annotation itself is a marker;
|
|
2000
|
+
* the actual redaction (content removal) must be applied separately.
|
|
2001
|
+
*/
|
|
2002
|
+
var PdfRedactAnnotation = class PdfRedactAnnotation extends PdfAnnotation {
|
|
2003
|
+
constructor(dict) {
|
|
2004
|
+
super("Redact", dict);
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Create a new redact annotation.
|
|
2008
|
+
*/
|
|
2009
|
+
static create(options) {
|
|
2010
|
+
const annot = new PdfRedactAnnotation(buildAnnotationDict("Redact", options));
|
|
2011
|
+
if (options.overlayText !== void 0) annot.setOverlayText(options.overlayText);
|
|
2012
|
+
if (options.interiorColor !== void 0) annot.setInteriorColor(options.interiorColor);
|
|
2013
|
+
if (options.quadPoints !== void 0) annot.setQuadPoints(options.quadPoints);
|
|
2014
|
+
return annot;
|
|
2015
|
+
}
|
|
2016
|
+
static fromDict(dict, resolver) {
|
|
2017
|
+
return new PdfRedactAnnotation(dict);
|
|
2018
|
+
}
|
|
2019
|
+
/** Get the overlay text displayed after redaction is applied. */
|
|
2020
|
+
getOverlayText() {
|
|
2021
|
+
const obj = this.dict.get("/OverlayText");
|
|
2022
|
+
if (obj && obj.kind === "string") return obj.value;
|
|
2023
|
+
}
|
|
2024
|
+
/** Set the overlay text. */
|
|
2025
|
+
setOverlayText(text) {
|
|
2026
|
+
this.dict.set("/OverlayText", PdfString.literal(text));
|
|
2027
|
+
}
|
|
2028
|
+
/** Get the interior (fill) color used after redaction. */
|
|
2029
|
+
getInteriorColor() {
|
|
2030
|
+
const obj = this.dict.get("/IC");
|
|
2031
|
+
if (obj && obj.kind === "array" && obj.items.length >= 3) return {
|
|
2032
|
+
r: obj.items[0]?.value ?? 0,
|
|
2033
|
+
g: obj.items[1]?.value ?? 0,
|
|
2034
|
+
b: obj.items[2]?.value ?? 0
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
/** Set the interior color. */
|
|
2038
|
+
setInteriorColor(color) {
|
|
2039
|
+
this.dict.set("/IC", PdfArray.fromNumbers([
|
|
2040
|
+
color.r,
|
|
2041
|
+
color.g,
|
|
2042
|
+
color.b
|
|
2043
|
+
]));
|
|
2044
|
+
}
|
|
2045
|
+
/** Get the quad points (regions to redact). */
|
|
2046
|
+
getQuadPoints() {
|
|
2047
|
+
const obj = this.dict.get("/QuadPoints");
|
|
2048
|
+
if (obj && obj.kind === "array") return obj.items.filter((item) => item.kind === "number").map((item) => item.value);
|
|
2049
|
+
}
|
|
2050
|
+
/** Set the quad points. */
|
|
2051
|
+
setQuadPoints(points) {
|
|
2052
|
+
this.dict.set("/QuadPoints", PdfArray.fromNumbers(points));
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
//#endregion
|
|
2057
|
+
//#region src/parser/textExtractor.ts
|
|
2058
|
+
/**
|
|
2059
|
+
* @module parser/textExtractor
|
|
2060
|
+
*
|
|
2061
|
+
* Extract text content from parsed PDF content streams. Supports both
|
|
2062
|
+
* simple text extraction (concatenated strings) and position-aware
|
|
2063
|
+
* extraction that tracks the text matrix to compute x/y coordinates.
|
|
2064
|
+
*
|
|
2065
|
+
* Handles:
|
|
2066
|
+
* - All PDF text-showing operators: `Tj`, `TJ`, `'`, `"`
|
|
2067
|
+
* - Text-positioning operators: `Td`, `TD`, `Tm`, `T*`
|
|
2068
|
+
* - Font selection: `Tf`
|
|
2069
|
+
* - Graphics state: `q`/`Q`, `cm`
|
|
2070
|
+
* - WinAnsiEncoding (standard single-byte)
|
|
2071
|
+
* - Identity-H CID fonts with ToUnicode CMap
|
|
2072
|
+
*
|
|
2073
|
+
* Reference: PDF 1.7 spec, §9 (Text).
|
|
2074
|
+
*
|
|
2075
|
+
* @packageDocumentation
|
|
2076
|
+
*/
|
|
2077
|
+
/**
|
|
2078
|
+
* Extract plain text from a sequence of parsed content-stream operators.
|
|
2079
|
+
*
|
|
2080
|
+
* This function concatenates all text-showing operator strings, inserting
|
|
2081
|
+
* spaces between text objects (BT/ET blocks) and newlines at line breaks
|
|
2082
|
+
* (`T*`, `Td`, `TD`).
|
|
2083
|
+
*
|
|
2084
|
+
* @param operators - Parsed content-stream operators.
|
|
2085
|
+
* @param resources - Optional page `/Resources` dictionary (used to look
|
|
2086
|
+
* up font encodings and ToUnicode CMaps).
|
|
2087
|
+
* @param options - Extraction options.
|
|
2088
|
+
* @returns The extracted text as a single string.
|
|
2089
|
+
*/
|
|
2090
|
+
function extractText(operators, resources, options) {
|
|
2091
|
+
if (options?.withPositions) return extractTextWithPositions(operators, resources).map((item) => item.text).join(" ");
|
|
2092
|
+
const state = new TextState(resources);
|
|
2093
|
+
const parts = [];
|
|
2094
|
+
let lineHasContent = false;
|
|
2095
|
+
for (const op of operators) switch (op.operator) {
|
|
2096
|
+
case "BT":
|
|
2097
|
+
state.resetTextMatrix();
|
|
2098
|
+
if (parts.length > 0 && lineHasContent) parts.push(" ");
|
|
2099
|
+
lineHasContent = false;
|
|
2100
|
+
break;
|
|
2101
|
+
case "ET": break;
|
|
2102
|
+
case "Tf":
|
|
2103
|
+
state.setFont(operandAsString(op.operands[0]), operandAsNumber(op.operands[1]));
|
|
2104
|
+
break;
|
|
2105
|
+
case "Tc":
|
|
2106
|
+
state.charSpacing = operandAsNumber(op.operands[0]);
|
|
2107
|
+
break;
|
|
2108
|
+
case "Tw":
|
|
2109
|
+
state.wordSpacing = operandAsNumber(op.operands[0]);
|
|
2110
|
+
break;
|
|
2111
|
+
case "TL":
|
|
2112
|
+
state.leading = operandAsNumber(op.operands[0]);
|
|
2113
|
+
break;
|
|
2114
|
+
case "Tz":
|
|
2115
|
+
state.horizontalScaling = operandAsNumber(op.operands[0]);
|
|
2116
|
+
break;
|
|
2117
|
+
case "Ts":
|
|
2118
|
+
state.rise = operandAsNumber(op.operands[0]);
|
|
2119
|
+
break;
|
|
2120
|
+
case "Td": {
|
|
2121
|
+
const tx = operandAsNumber(op.operands[0]);
|
|
2122
|
+
const ty = operandAsNumber(op.operands[1]);
|
|
2123
|
+
state.moveText(tx, ty);
|
|
2124
|
+
if (Math.abs(ty) > .5 && lineHasContent) {
|
|
2125
|
+
parts.push("\n");
|
|
2126
|
+
lineHasContent = false;
|
|
2127
|
+
} else if (Math.abs(tx) > state.fontSize * .3 && lineHasContent) parts.push(" ");
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
case "TD": {
|
|
2131
|
+
const tx = operandAsNumber(op.operands[0]);
|
|
2132
|
+
const ty = operandAsNumber(op.operands[1]);
|
|
2133
|
+
state.leading = -ty;
|
|
2134
|
+
state.moveText(tx, ty);
|
|
2135
|
+
if (Math.abs(ty) > .5 && lineHasContent) {
|
|
2136
|
+
parts.push("\n");
|
|
2137
|
+
lineHasContent = false;
|
|
2138
|
+
}
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
case "Tm":
|
|
2142
|
+
state.setTextMatrix(operandAsNumber(op.operands[0]), operandAsNumber(op.operands[1]), operandAsNumber(op.operands[2]), operandAsNumber(op.operands[3]), operandAsNumber(op.operands[4]), operandAsNumber(op.operands[5]));
|
|
2143
|
+
break;
|
|
2144
|
+
case "T*":
|
|
2145
|
+
state.nextLine();
|
|
2146
|
+
if (lineHasContent) {
|
|
2147
|
+
parts.push("\n");
|
|
2148
|
+
lineHasContent = false;
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
case "Tj": {
|
|
2152
|
+
const text = state.decodeString(op.operands[0]);
|
|
2153
|
+
if (text.length > 0) {
|
|
2154
|
+
parts.push(text);
|
|
2155
|
+
lineHasContent = true;
|
|
2156
|
+
}
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
case "TJ": {
|
|
2160
|
+
const text = state.decodeTJArray(op.operands[0]);
|
|
2161
|
+
if (text.length > 0) {
|
|
2162
|
+
parts.push(text);
|
|
2163
|
+
lineHasContent = true;
|
|
2164
|
+
}
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
case "'": {
|
|
2168
|
+
state.nextLine();
|
|
2169
|
+
if (lineHasContent) {
|
|
2170
|
+
parts.push("\n");
|
|
2171
|
+
lineHasContent = false;
|
|
2172
|
+
}
|
|
2173
|
+
const text = state.decodeString(op.operands[0]);
|
|
2174
|
+
if (text.length > 0) {
|
|
2175
|
+
parts.push(text);
|
|
2176
|
+
lineHasContent = true;
|
|
2177
|
+
}
|
|
2178
|
+
break;
|
|
2179
|
+
}
|
|
2180
|
+
case "\"": {
|
|
2181
|
+
state.wordSpacing = operandAsNumber(op.operands[0]);
|
|
2182
|
+
state.charSpacing = operandAsNumber(op.operands[1]);
|
|
2183
|
+
state.nextLine();
|
|
2184
|
+
if (lineHasContent) {
|
|
2185
|
+
parts.push("\n");
|
|
2186
|
+
lineHasContent = false;
|
|
2187
|
+
}
|
|
2188
|
+
const text = state.decodeString(op.operands[2]);
|
|
2189
|
+
if (text.length > 0) {
|
|
2190
|
+
parts.push(text);
|
|
2191
|
+
lineHasContent = true;
|
|
2192
|
+
}
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
case "q":
|
|
2196
|
+
state.save();
|
|
2197
|
+
break;
|
|
2198
|
+
case "Q":
|
|
2199
|
+
state.restore();
|
|
2200
|
+
break;
|
|
2201
|
+
case "cm":
|
|
2202
|
+
state.concatCTM(operandAsNumber(op.operands[0]), operandAsNumber(op.operands[1]), operandAsNumber(op.operands[2]), operandAsNumber(op.operands[3]), operandAsNumber(op.operands[4]), operandAsNumber(op.operands[5]));
|
|
2203
|
+
break;
|
|
2204
|
+
default: break;
|
|
2205
|
+
}
|
|
2206
|
+
return parts.join("");
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Extract text with position information from a parsed content stream.
|
|
2210
|
+
*
|
|
2211
|
+
* Each returned {@link TextItem} includes the text string, its position
|
|
2212
|
+
* (x, y), dimensions (width, height), font size, and font name.
|
|
2213
|
+
*
|
|
2214
|
+
* @param operators - Parsed content-stream operators.
|
|
2215
|
+
* @param resources - Optional page `/Resources` dictionary.
|
|
2216
|
+
* @returns An array of positioned text items.
|
|
2217
|
+
*/
|
|
2218
|
+
function extractTextWithPositions(operators, resources) {
|
|
2219
|
+
const state = new TextState(resources);
|
|
2220
|
+
const items = [];
|
|
2221
|
+
for (const op of operators) switch (op.operator) {
|
|
2222
|
+
case "BT":
|
|
2223
|
+
state.resetTextMatrix();
|
|
2224
|
+
break;
|
|
2225
|
+
case "ET": break;
|
|
2226
|
+
case "Tf":
|
|
2227
|
+
state.setFont(operandAsString(op.operands[0]), operandAsNumber(op.operands[1]));
|
|
2228
|
+
break;
|
|
2229
|
+
case "Tc":
|
|
2230
|
+
state.charSpacing = operandAsNumber(op.operands[0]);
|
|
2231
|
+
break;
|
|
2232
|
+
case "Tw":
|
|
2233
|
+
state.wordSpacing = operandAsNumber(op.operands[0]);
|
|
2234
|
+
break;
|
|
2235
|
+
case "TL":
|
|
2236
|
+
state.leading = operandAsNumber(op.operands[0]);
|
|
2237
|
+
break;
|
|
2238
|
+
case "Tz":
|
|
2239
|
+
state.horizontalScaling = operandAsNumber(op.operands[0]);
|
|
2240
|
+
break;
|
|
2241
|
+
case "Ts":
|
|
2242
|
+
state.rise = operandAsNumber(op.operands[0]);
|
|
2243
|
+
break;
|
|
2244
|
+
case "Td":
|
|
2245
|
+
state.moveText(operandAsNumber(op.operands[0]), operandAsNumber(op.operands[1]));
|
|
2246
|
+
break;
|
|
2247
|
+
case "TD": {
|
|
2248
|
+
const tx = operandAsNumber(op.operands[0]);
|
|
2249
|
+
const ty = operandAsNumber(op.operands[1]);
|
|
2250
|
+
state.leading = -ty;
|
|
2251
|
+
state.moveText(tx, ty);
|
|
2252
|
+
break;
|
|
2253
|
+
}
|
|
2254
|
+
case "Tm":
|
|
2255
|
+
state.setTextMatrix(operandAsNumber(op.operands[0]), operandAsNumber(op.operands[1]), operandAsNumber(op.operands[2]), operandAsNumber(op.operands[3]), operandAsNumber(op.operands[4]), operandAsNumber(op.operands[5]));
|
|
2256
|
+
break;
|
|
2257
|
+
case "T*":
|
|
2258
|
+
state.nextLine();
|
|
2259
|
+
break;
|
|
2260
|
+
case "Tj": {
|
|
2261
|
+
const text = state.decodeString(op.operands[0]);
|
|
2262
|
+
if (text.length > 0) {
|
|
2263
|
+
const pos = state.getTextPosition();
|
|
2264
|
+
items.push({
|
|
2265
|
+
text,
|
|
2266
|
+
x: pos.x,
|
|
2267
|
+
y: pos.y,
|
|
2268
|
+
width: state.estimateWidth(text),
|
|
2269
|
+
height: state.fontSize,
|
|
2270
|
+
fontSize: state.fontSize,
|
|
2271
|
+
fontName: state.fontName
|
|
2272
|
+
});
|
|
2273
|
+
state.advanceByText(text);
|
|
2274
|
+
}
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
case "TJ": {
|
|
2278
|
+
const operand = op.operands[0];
|
|
2279
|
+
if (Array.isArray(operand)) for (const elem of operand) if (typeof elem === "number") state.advanceByDisplacement(-elem);
|
|
2280
|
+
else {
|
|
2281
|
+
const text = state.decodeString(elem);
|
|
2282
|
+
if (text.length > 0) {
|
|
2283
|
+
const pos = state.getTextPosition();
|
|
2284
|
+
items.push({
|
|
2285
|
+
text,
|
|
2286
|
+
x: pos.x,
|
|
2287
|
+
y: pos.y,
|
|
2288
|
+
width: state.estimateWidth(text),
|
|
2289
|
+
height: state.fontSize,
|
|
2290
|
+
fontSize: state.fontSize,
|
|
2291
|
+
fontName: state.fontName
|
|
2292
|
+
});
|
|
2293
|
+
state.advanceByText(text);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
case "'":
|
|
2299
|
+
state.nextLine();
|
|
2300
|
+
{
|
|
2301
|
+
const text = state.decodeString(op.operands[0]);
|
|
2302
|
+
if (text.length > 0) {
|
|
2303
|
+
const pos = state.getTextPosition();
|
|
2304
|
+
items.push({
|
|
2305
|
+
text,
|
|
2306
|
+
x: pos.x,
|
|
2307
|
+
y: pos.y,
|
|
2308
|
+
width: state.estimateWidth(text),
|
|
2309
|
+
height: state.fontSize,
|
|
2310
|
+
fontSize: state.fontSize,
|
|
2311
|
+
fontName: state.fontName
|
|
2312
|
+
});
|
|
2313
|
+
state.advanceByText(text);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
break;
|
|
2317
|
+
case "\"":
|
|
2318
|
+
state.wordSpacing = operandAsNumber(op.operands[0]);
|
|
2319
|
+
state.charSpacing = operandAsNumber(op.operands[1]);
|
|
2320
|
+
state.nextLine();
|
|
2321
|
+
{
|
|
2322
|
+
const text = state.decodeString(op.operands[2]);
|
|
2323
|
+
if (text.length > 0) {
|
|
2324
|
+
const pos = state.getTextPosition();
|
|
2325
|
+
items.push({
|
|
2326
|
+
text,
|
|
2327
|
+
x: pos.x,
|
|
2328
|
+
y: pos.y,
|
|
2329
|
+
width: state.estimateWidth(text),
|
|
2330
|
+
height: state.fontSize,
|
|
2331
|
+
fontSize: state.fontSize,
|
|
2332
|
+
fontName: state.fontName
|
|
2333
|
+
});
|
|
2334
|
+
state.advanceByText(text);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
break;
|
|
2338
|
+
case "q":
|
|
2339
|
+
state.save();
|
|
2340
|
+
break;
|
|
2341
|
+
case "Q":
|
|
2342
|
+
state.restore();
|
|
2343
|
+
break;
|
|
2344
|
+
case "cm":
|
|
2345
|
+
state.concatCTM(operandAsNumber(op.operands[0]), operandAsNumber(op.operands[1]), operandAsNumber(op.operands[2]), operandAsNumber(op.operands[3]), operandAsNumber(op.operands[4]), operandAsNumber(op.operands[5]));
|
|
2346
|
+
break;
|
|
2347
|
+
default: break;
|
|
2348
|
+
}
|
|
2349
|
+
return items;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Parse a ToUnicode CMap stream into a lookup map.
|
|
2353
|
+
*
|
|
2354
|
+
* Handles the two standard mapping constructs:
|
|
2355
|
+
* - `beginbfchar` / `endbfchar` — single code-to-Unicode mappings
|
|
2356
|
+
* - `beginbfrange` / `endbfrange` — range-based mappings
|
|
2357
|
+
*
|
|
2358
|
+
* @param data - The raw CMap stream bytes (already decompressed).
|
|
2359
|
+
* @returns A parsed CMap.
|
|
2360
|
+
*/
|
|
2361
|
+
function parseToUnicodeCMap(data) {
|
|
2362
|
+
const text = decodeText(data);
|
|
2363
|
+
const map = /* @__PURE__ */ new Map();
|
|
2364
|
+
parseBfCharSections(text, map);
|
|
2365
|
+
parseBfRangeSections(text, map);
|
|
2366
|
+
return { map };
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Parse all `beginbfchar`/`endbfchar` sections in a CMap.
|
|
2370
|
+
*/
|
|
2371
|
+
function parseBfCharSections(text, map) {
|
|
2372
|
+
const regex = /beginbfchar\s*([\s\S]*?)\s*endbfchar/g;
|
|
2373
|
+
let match;
|
|
2374
|
+
while ((match = regex.exec(text)) !== null) {
|
|
2375
|
+
const body = match[1];
|
|
2376
|
+
const lineRegex = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>/g;
|
|
2377
|
+
let lineMatch;
|
|
2378
|
+
while ((lineMatch = lineRegex.exec(body)) !== null) {
|
|
2379
|
+
const srcCode = parseInt(lineMatch[1], 16);
|
|
2380
|
+
const dstString = hexToUnicode(lineMatch[2]);
|
|
2381
|
+
map.set(srcCode, dstString);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Parse all `beginbfrange`/`endbfrange` sections in a CMap.
|
|
2387
|
+
*/
|
|
2388
|
+
function parseBfRangeSections(text, map) {
|
|
2389
|
+
const regex = /beginbfrange\s*([\s\S]*?)\s*endbfrange/g;
|
|
2390
|
+
let match;
|
|
2391
|
+
while ((match = regex.exec(text)) !== null) {
|
|
2392
|
+
const body = match[1];
|
|
2393
|
+
const lineRegex = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>\s*(?:<([0-9a-fA-F]+)>|\[([\s\S]*?)\])/g;
|
|
2394
|
+
let lineMatch;
|
|
2395
|
+
while ((lineMatch = lineRegex.exec(body)) !== null) {
|
|
2396
|
+
const srcLow = parseInt(lineMatch[1], 16);
|
|
2397
|
+
const srcHigh = parseInt(lineMatch[2], 16);
|
|
2398
|
+
if (lineMatch[3]) {
|
|
2399
|
+
let dstCode = parseInt(lineMatch[3], 16);
|
|
2400
|
+
for (let code = srcLow; code <= srcHigh; code++) {
|
|
2401
|
+
map.set(code, codePointToString(dstCode));
|
|
2402
|
+
dstCode++;
|
|
2403
|
+
}
|
|
2404
|
+
} else if (lineMatch[4]) {
|
|
2405
|
+
const arrRegex = /<([0-9a-fA-F]+)>/g;
|
|
2406
|
+
let arrMatch;
|
|
2407
|
+
let code = srcLow;
|
|
2408
|
+
while ((arrMatch = arrRegex.exec(lineMatch[4])) !== null && code <= srcHigh) {
|
|
2409
|
+
map.set(code, hexToUnicode(arrMatch[1]));
|
|
2410
|
+
code++;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Convert a hex string (2 or 4 hex chars per code point) to a Unicode
|
|
2418
|
+
* string.
|
|
2419
|
+
*/
|
|
2420
|
+
function hexToUnicode(hex) {
|
|
2421
|
+
let result = "";
|
|
2422
|
+
const step = hex.length <= 4 ? hex.length : 4;
|
|
2423
|
+
for (let i = 0; i < hex.length; i += step) {
|
|
2424
|
+
const chunk = hex.substring(i, i + step);
|
|
2425
|
+
const code = parseInt(chunk, 16);
|
|
2426
|
+
if (!isNaN(code)) result += String.fromCodePoint(code);
|
|
2427
|
+
}
|
|
2428
|
+
return result;
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Convert a numeric code point to a string.
|
|
2432
|
+
*/
|
|
2433
|
+
function codePointToString(code) {
|
|
2434
|
+
return String.fromCodePoint(code);
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* WinAnsiEncoding table for bytes 0x80-0x9F that differ from Latin-1.
|
|
2438
|
+
* Bytes 0x00-0x7F and 0xA0-0xFF map directly to their Unicode code points.
|
|
2439
|
+
*/
|
|
2440
|
+
const WIN_ANSI_SPECIAL = {
|
|
2441
|
+
128: 8364,
|
|
2442
|
+
130: 8218,
|
|
2443
|
+
131: 402,
|
|
2444
|
+
132: 8222,
|
|
2445
|
+
133: 8230,
|
|
2446
|
+
134: 8224,
|
|
2447
|
+
135: 8225,
|
|
2448
|
+
136: 710,
|
|
2449
|
+
137: 8240,
|
|
2450
|
+
138: 352,
|
|
2451
|
+
139: 8249,
|
|
2452
|
+
140: 338,
|
|
2453
|
+
142: 381,
|
|
2454
|
+
145: 8216,
|
|
2455
|
+
146: 8217,
|
|
2456
|
+
147: 8220,
|
|
2457
|
+
148: 8221,
|
|
2458
|
+
149: 8226,
|
|
2459
|
+
150: 8211,
|
|
2460
|
+
151: 8212,
|
|
2461
|
+
152: 732,
|
|
2462
|
+
153: 8482,
|
|
2463
|
+
154: 353,
|
|
2464
|
+
155: 8250,
|
|
2465
|
+
156: 339,
|
|
2466
|
+
158: 382,
|
|
2467
|
+
159: 376
|
|
2468
|
+
};
|
|
2469
|
+
/**
|
|
2470
|
+
* Decode a single byte using WinAnsiEncoding.
|
|
2471
|
+
*/
|
|
2472
|
+
function winAnsiDecode(byte) {
|
|
2473
|
+
if (Object.hasOwn(WIN_ANSI_SPECIAL, byte)) return String.fromCodePoint(WIN_ANSI_SPECIAL[byte]);
|
|
2474
|
+
return String.fromCharCode(byte);
|
|
2475
|
+
}
|
|
2476
|
+
/** Identity matrix. */
|
|
2477
|
+
function identityMatrix() {
|
|
2478
|
+
return [
|
|
2479
|
+
1,
|
|
2480
|
+
0,
|
|
2481
|
+
0,
|
|
2482
|
+
1,
|
|
2483
|
+
0,
|
|
2484
|
+
0
|
|
2485
|
+
];
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Multiply two 3x3 matrices (stored as 6-element arrays).
|
|
2489
|
+
* Result = A * B
|
|
2490
|
+
*/
|
|
2491
|
+
function multiplyMatrices(a, b) {
|
|
2492
|
+
return [
|
|
2493
|
+
a[0] * b[0] + a[1] * b[2],
|
|
2494
|
+
a[0] * b[1] + a[1] * b[3],
|
|
2495
|
+
a[2] * b[0] + a[3] * b[2],
|
|
2496
|
+
a[2] * b[1] + a[3] * b[3],
|
|
2497
|
+
a[4] * b[0] + a[5] * b[2] + b[4],
|
|
2498
|
+
a[4] * b[1] + a[5] * b[3] + b[5]
|
|
2499
|
+
];
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Tracks the graphics/text state needed for text extraction.
|
|
2503
|
+
*/
|
|
2504
|
+
var TextState = class {
|
|
2505
|
+
/** Current transformation matrix (CTM). */
|
|
2506
|
+
ctm = identityMatrix();
|
|
2507
|
+
/** Text matrix — set by Tm, updated by Td/TD/T*. */
|
|
2508
|
+
textMatrix = identityMatrix();
|
|
2509
|
+
/** Text line matrix — the matrix at the start of the current line. */
|
|
2510
|
+
textLineMatrix = identityMatrix();
|
|
2511
|
+
/** Current font resource name. */
|
|
2512
|
+
fontName = "";
|
|
2513
|
+
/** Current font size. */
|
|
2514
|
+
fontSize = 12;
|
|
2515
|
+
/** Character spacing (Tc). */
|
|
2516
|
+
charSpacing = 0;
|
|
2517
|
+
/** Word spacing (Tw). */
|
|
2518
|
+
wordSpacing = 0;
|
|
2519
|
+
/** Horizontal scaling (Tz) as a percentage (100 = normal). */
|
|
2520
|
+
horizontalScaling = 100;
|
|
2521
|
+
/** Text leading (TL). */
|
|
2522
|
+
leading = 0;
|
|
2523
|
+
/** Text rise (Ts). */
|
|
2524
|
+
rise = 0;
|
|
2525
|
+
/** Graphics state stack for q/Q. */
|
|
2526
|
+
stateStack = [];
|
|
2527
|
+
/** Page resources dictionary. */
|
|
2528
|
+
resources;
|
|
2529
|
+
/** Cache of parsed ToUnicode CMaps per font name. */
|
|
2530
|
+
cmapCache = /* @__PURE__ */ new Map();
|
|
2531
|
+
/** Cache of font encoding type per font name. */
|
|
2532
|
+
fontEncodingCache = /* @__PURE__ */ new Map();
|
|
2533
|
+
/** Cache of whether a font is a CID (2-byte) font. */
|
|
2534
|
+
cidFontCache = /* @__PURE__ */ new Map();
|
|
2535
|
+
constructor(resources) {
|
|
2536
|
+
this.resources = resources;
|
|
2537
|
+
if (resources) this.analyzeFonts(resources);
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Pre-analyze fonts from the resources dictionary to determine encoding
|
|
2541
|
+
* types and cache ToUnicode CMaps.
|
|
2542
|
+
*/
|
|
2543
|
+
analyzeFonts(resources) {
|
|
2544
|
+
const fonts = resources.get("/Font");
|
|
2545
|
+
if (!(fonts instanceof PdfDict)) return;
|
|
2546
|
+
for (const [name, fontObj] of fonts) {
|
|
2547
|
+
if (!(fontObj instanceof PdfDict)) continue;
|
|
2548
|
+
const subtype = fontObj.get("/Subtype");
|
|
2549
|
+
const isCid = subtype instanceof PdfName && (subtype.value === "/Type0" || subtype.value === "/CIDFontType0" || subtype.value === "/CIDFontType2");
|
|
2550
|
+
this.cidFontCache.set(name, isCid);
|
|
2551
|
+
const encoding = fontObj.get("/Encoding");
|
|
2552
|
+
if (encoding instanceof PdfName) this.fontEncodingCache.set(name, encoding.value.replace(/^\//, ""));
|
|
2553
|
+
const toUnicode = fontObj.get("/ToUnicode");
|
|
2554
|
+
if (toUnicode instanceof PdfStream) try {
|
|
2555
|
+
const cmap = parseToUnicodeCMap(toUnicode.data);
|
|
2556
|
+
this.cmapCache.set(name, cmap);
|
|
2557
|
+
} catch {
|
|
2558
|
+
this.cmapCache.set(name, null);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Save the current graphics state (q).
|
|
2564
|
+
*/
|
|
2565
|
+
save() {
|
|
2566
|
+
this.stateStack.push({
|
|
2567
|
+
ctm: [...this.ctm],
|
|
2568
|
+
fontName: this.fontName,
|
|
2569
|
+
fontSize: this.fontSize,
|
|
2570
|
+
charSpacing: this.charSpacing,
|
|
2571
|
+
wordSpacing: this.wordSpacing,
|
|
2572
|
+
horizontalScaling: this.horizontalScaling,
|
|
2573
|
+
leading: this.leading,
|
|
2574
|
+
rise: this.rise
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Restore the previously saved graphics state (Q).
|
|
2579
|
+
*/
|
|
2580
|
+
restore() {
|
|
2581
|
+
const saved = this.stateStack.pop();
|
|
2582
|
+
if (saved) {
|
|
2583
|
+
this.ctm = saved.ctm;
|
|
2584
|
+
this.fontName = saved.fontName;
|
|
2585
|
+
this.fontSize = saved.fontSize;
|
|
2586
|
+
this.charSpacing = saved.charSpacing;
|
|
2587
|
+
this.wordSpacing = saved.wordSpacing;
|
|
2588
|
+
this.horizontalScaling = saved.horizontalScaling;
|
|
2589
|
+
this.leading = saved.leading;
|
|
2590
|
+
this.rise = saved.rise;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Concatenate a matrix with the CTM (cm).
|
|
2595
|
+
*/
|
|
2596
|
+
concatCTM(a, b, c, d, e, f) {
|
|
2597
|
+
this.ctm = multiplyMatrices([
|
|
2598
|
+
a,
|
|
2599
|
+
b,
|
|
2600
|
+
c,
|
|
2601
|
+
d,
|
|
2602
|
+
e,
|
|
2603
|
+
f
|
|
2604
|
+
], this.ctm);
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Reset the text matrix to identity (called at BT).
|
|
2608
|
+
*/
|
|
2609
|
+
resetTextMatrix() {
|
|
2610
|
+
this.textMatrix = identityMatrix();
|
|
2611
|
+
this.textLineMatrix = identityMatrix();
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Set the font and size (Tf).
|
|
2615
|
+
*/
|
|
2616
|
+
setFont(name, size) {
|
|
2617
|
+
this.fontName = name.startsWith("/") ? name : `/${name}`;
|
|
2618
|
+
this.fontSize = size;
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Move text position (Td).
|
|
2622
|
+
*/
|
|
2623
|
+
moveText(tx, ty) {
|
|
2624
|
+
this.textLineMatrix = multiplyMatrices([
|
|
2625
|
+
1,
|
|
2626
|
+
0,
|
|
2627
|
+
0,
|
|
2628
|
+
1,
|
|
2629
|
+
tx,
|
|
2630
|
+
ty
|
|
2631
|
+
], this.textLineMatrix);
|
|
2632
|
+
this.textMatrix = [...this.textLineMatrix];
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Set the text matrix directly (Tm).
|
|
2636
|
+
*/
|
|
2637
|
+
setTextMatrix(a, b, c, d, e, f) {
|
|
2638
|
+
this.textMatrix = [
|
|
2639
|
+
a,
|
|
2640
|
+
b,
|
|
2641
|
+
c,
|
|
2642
|
+
d,
|
|
2643
|
+
e,
|
|
2644
|
+
f
|
|
2645
|
+
];
|
|
2646
|
+
this.textLineMatrix = [
|
|
2647
|
+
a,
|
|
2648
|
+
b,
|
|
2649
|
+
c,
|
|
2650
|
+
d,
|
|
2651
|
+
e,
|
|
2652
|
+
f
|
|
2653
|
+
];
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Move to the start of the next line (T*).
|
|
2657
|
+
* Equivalent to: 0 -TL Td
|
|
2658
|
+
*/
|
|
2659
|
+
nextLine() {
|
|
2660
|
+
this.moveText(0, -this.leading);
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Get the current text position in user-space coordinates.
|
|
2664
|
+
*/
|
|
2665
|
+
getTextPosition() {
|
|
2666
|
+
const combined = multiplyMatrices(this.textMatrix, this.ctm);
|
|
2667
|
+
return {
|
|
2668
|
+
x: combined[4],
|
|
2669
|
+
y: combined[5]
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Estimate the width of a text string in user-space units.
|
|
2674
|
+
*
|
|
2675
|
+
* Uses a rough heuristic: 0.5 * fontSize per character for standard
|
|
2676
|
+
* fonts. A production implementation would use font metrics.
|
|
2677
|
+
*/
|
|
2678
|
+
estimateWidth(text) {
|
|
2679
|
+
const avgCharWidth = .5;
|
|
2680
|
+
const hScale = this.horizontalScaling / 100;
|
|
2681
|
+
return text.length * this.fontSize * avgCharWidth * hScale;
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Advance the text matrix by the width of the given text.
|
|
2685
|
+
*/
|
|
2686
|
+
advanceByText(text) {
|
|
2687
|
+
this.textMatrix = multiplyMatrices([
|
|
2688
|
+
1,
|
|
2689
|
+
0,
|
|
2690
|
+
0,
|
|
2691
|
+
1,
|
|
2692
|
+
this.estimateWidth(text),
|
|
2693
|
+
0
|
|
2694
|
+
], this.textMatrix);
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Advance the text matrix by a TJ displacement value.
|
|
2698
|
+
*
|
|
2699
|
+
* The displacement is in thousandths of a unit of text space.
|
|
2700
|
+
*/
|
|
2701
|
+
advanceByDisplacement(displacement) {
|
|
2702
|
+
this.textMatrix = multiplyMatrices([
|
|
2703
|
+
1,
|
|
2704
|
+
0,
|
|
2705
|
+
0,
|
|
2706
|
+
1,
|
|
2707
|
+
displacement / 1e3 * this.fontSize * (this.horizontalScaling / 100),
|
|
2708
|
+
0
|
|
2709
|
+
], this.textMatrix);
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Decode an operand (string or hex string) into a readable text string.
|
|
2713
|
+
*
|
|
2714
|
+
* Uses the current font's ToUnicode CMap if available, otherwise falls
|
|
2715
|
+
* back to WinAnsiEncoding or direct code-point mapping.
|
|
2716
|
+
*/
|
|
2717
|
+
decodeString(operand) {
|
|
2718
|
+
if (operand == null) return "";
|
|
2719
|
+
if (typeof operand === "number") return "";
|
|
2720
|
+
const raw = typeof operand === "string" ? operand : String(operand);
|
|
2721
|
+
const cmap = this.cmapCache.get(this.fontName);
|
|
2722
|
+
const isCid = this.cidFontCache.get(this.fontName) ?? false;
|
|
2723
|
+
if (cmap) return this.decodeWithCMap(raw, cmap, isCid);
|
|
2724
|
+
if (isCid) return this.decodeCIDString(raw);
|
|
2725
|
+
return this.decodeWinAnsi(raw);
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Decode a TJ array operand (array of strings + numbers).
|
|
2729
|
+
*/
|
|
2730
|
+
decodeTJArray(operand) {
|
|
2731
|
+
if (!Array.isArray(operand)) return this.decodeString(operand);
|
|
2732
|
+
const parts = [];
|
|
2733
|
+
for (const elem of operand) if (typeof elem === "number") {
|
|
2734
|
+
if (elem <= -100) parts.push(" ");
|
|
2735
|
+
} else {
|
|
2736
|
+
const decoded = this.decodeString(elem);
|
|
2737
|
+
if (decoded.length > 0) parts.push(decoded);
|
|
2738
|
+
}
|
|
2739
|
+
return parts.join("");
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Decode a string using a ToUnicode CMap.
|
|
2743
|
+
*/
|
|
2744
|
+
decodeWithCMap(raw, cmap, isCid) {
|
|
2745
|
+
let result = "";
|
|
2746
|
+
if (isCid) for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
2747
|
+
const code = raw.charCodeAt(i) << 8 | raw.charCodeAt(i + 1);
|
|
2748
|
+
const mapped = cmap.map.get(code);
|
|
2749
|
+
if (mapped !== void 0) result += mapped;
|
|
2750
|
+
else if (code >= 32 && code <= 65535) result += String.fromCharCode(code);
|
|
2751
|
+
}
|
|
2752
|
+
else for (let i = 0; i < raw.length; i++) {
|
|
2753
|
+
const code = raw.charCodeAt(i);
|
|
2754
|
+
const mapped = cmap.map.get(code);
|
|
2755
|
+
if (mapped !== void 0) result += mapped;
|
|
2756
|
+
else result += winAnsiDecode(code);
|
|
2757
|
+
}
|
|
2758
|
+
return result;
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Decode a CID (Identity-H) encoded string without a ToUnicode CMap.
|
|
2762
|
+
*/
|
|
2763
|
+
decodeCIDString(raw) {
|
|
2764
|
+
let result = "";
|
|
2765
|
+
for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
2766
|
+
const code = raw.charCodeAt(i) << 8 | raw.charCodeAt(i + 1);
|
|
2767
|
+
if (code >= 32 && code <= 65535) result += String.fromCharCode(code);
|
|
2768
|
+
}
|
|
2769
|
+
return result;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Decode a string using WinAnsiEncoding.
|
|
2773
|
+
*/
|
|
2774
|
+
decodeWinAnsi(raw) {
|
|
2775
|
+
let result = "";
|
|
2776
|
+
for (let i = 0; i < raw.length; i++) {
|
|
2777
|
+
const code = raw.charCodeAt(i);
|
|
2778
|
+
result += winAnsiDecode(code);
|
|
2779
|
+
}
|
|
2780
|
+
return result;
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
/**
|
|
2784
|
+
* Extract a numeric value from an operand, defaulting to 0.
|
|
2785
|
+
*/
|
|
2786
|
+
function operandAsNumber(operand) {
|
|
2787
|
+
if (typeof operand === "number") return operand;
|
|
2788
|
+
if (operand instanceof PdfNumber) return operand.value;
|
|
2789
|
+
if (typeof operand === "string") {
|
|
2790
|
+
const n = parseFloat(operand);
|
|
2791
|
+
return isNaN(n) ? 0 : n;
|
|
2792
|
+
}
|
|
2793
|
+
return 0;
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Extract a string value from an operand.
|
|
2797
|
+
*/
|
|
2798
|
+
function operandAsString(operand) {
|
|
2799
|
+
if (typeof operand === "string") return operand;
|
|
2800
|
+
if (operand instanceof PdfName) return operand.value;
|
|
2801
|
+
if (typeof operand === "number") return String(operand);
|
|
2802
|
+
return "";
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Decode raw bytes to a string (ASCII/Latin-1 — sufficient for CMap
|
|
2806
|
+
* parsing which is ASCII-based).
|
|
2807
|
+
*/
|
|
2808
|
+
function decodeText(data) {
|
|
2809
|
+
return new TextDecoder("latin1").decode(data);
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
//#endregion
|
|
2813
|
+
//#region src/parser/contentStreamParser.ts
|
|
2814
|
+
/**
|
|
2815
|
+
* @module parser/contentStreamParser
|
|
2816
|
+
*
|
|
2817
|
+
* Parse PDF content streams (the operator/operand sequences that describe
|
|
2818
|
+
* page appearance) into a structured AST.
|
|
2819
|
+
*
|
|
2820
|
+
* A PDF content stream consists of a flat sequence of *operands* followed
|
|
2821
|
+
* by an *operator*. Operands are PDF objects (numbers, strings, names,
|
|
2822
|
+
* booleans, arrays, `null`); operators are unquoted letter sequences.
|
|
2823
|
+
*
|
|
2824
|
+
* Special handling is required for inline images (`BI … ID data EI`).
|
|
2825
|
+
*
|
|
2826
|
+
* Reference: PDF 1.7 spec, §7.8.2 (Content Streams).
|
|
2827
|
+
*
|
|
2828
|
+
* @packageDocumentation
|
|
2829
|
+
*/
|
|
2830
|
+
/**
|
|
2831
|
+
* Parse a PDF content stream into an ordered list of operators.
|
|
2832
|
+
*
|
|
2833
|
+
* @param data - The raw content-stream bytes (already decompressed).
|
|
2834
|
+
* @returns An array of operators in document order.
|
|
2835
|
+
*/
|
|
2836
|
+
function parseContentStream(data) {
|
|
2837
|
+
return new ContentStreamLexer(data).parse();
|
|
2838
|
+
}
|
|
2839
|
+
var TokenType = /* @__PURE__ */ function(TokenType) {
|
|
2840
|
+
TokenType[TokenType["Number"] = 0] = "Number";
|
|
2841
|
+
TokenType[TokenType["String"] = 1] = "String";
|
|
2842
|
+
TokenType[TokenType["HexString"] = 2] = "HexString";
|
|
2843
|
+
TokenType[TokenType["Name"] = 3] = "Name";
|
|
2844
|
+
TokenType[TokenType["Bool"] = 4] = "Bool";
|
|
2845
|
+
TokenType[TokenType["Null"] = 5] = "Null";
|
|
2846
|
+
TokenType[TokenType["ArrayStart"] = 6] = "ArrayStart";
|
|
2847
|
+
TokenType[TokenType["ArrayEnd"] = 7] = "ArrayEnd";
|
|
2848
|
+
TokenType[TokenType["Operator"] = 8] = "Operator";
|
|
2849
|
+
TokenType[TokenType["InlineImage"] = 9] = "InlineImage";
|
|
2850
|
+
TokenType[TokenType["EOF"] = 10] = "EOF";
|
|
2851
|
+
return TokenType;
|
|
2852
|
+
}(TokenType || {});
|
|
2853
|
+
/**
|
|
2854
|
+
* Combined lexer + parser for PDF content streams.
|
|
2855
|
+
*
|
|
2856
|
+
* Content streams are simpler than full PDF object syntax — there are no
|
|
2857
|
+
* dictionaries (except inside inline images), no indirect references, and
|
|
2858
|
+
* no comments outside of string literals.
|
|
2859
|
+
*/
|
|
2860
|
+
var ContentStreamLexer = class {
|
|
2861
|
+
data;
|
|
2862
|
+
pos = 0;
|
|
2863
|
+
constructor(data) {
|
|
2864
|
+
this.data = data;
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Parse the entire stream and return all operators.
|
|
2868
|
+
*/
|
|
2869
|
+
parse() {
|
|
2870
|
+
const result = [];
|
|
2871
|
+
const operandStack = [];
|
|
2872
|
+
while (true) {
|
|
2873
|
+
const token = this.nextToken();
|
|
2874
|
+
if (token.type === TokenType.EOF) break;
|
|
2875
|
+
switch (token.type) {
|
|
2876
|
+
case TokenType.Number:
|
|
2877
|
+
case TokenType.String:
|
|
2878
|
+
case TokenType.HexString:
|
|
2879
|
+
case TokenType.Bool:
|
|
2880
|
+
case TokenType.Null:
|
|
2881
|
+
case TokenType.Name:
|
|
2882
|
+
operandStack.push(token.value);
|
|
2883
|
+
break;
|
|
2884
|
+
case TokenType.ArrayStart: {
|
|
2885
|
+
const arr = this.parseArray();
|
|
2886
|
+
operandStack.push(arr);
|
|
2887
|
+
break;
|
|
2888
|
+
}
|
|
2889
|
+
case TokenType.ArrayEnd: break;
|
|
2890
|
+
case TokenType.Operator: {
|
|
2891
|
+
const op = token.value;
|
|
2892
|
+
if (op === "BI") {
|
|
2893
|
+
const inlineImg = this.parseInlineImage();
|
|
2894
|
+
result.push({
|
|
2895
|
+
operator: "BI",
|
|
2896
|
+
operands: [inlineImg]
|
|
2897
|
+
});
|
|
2898
|
+
} else result.push({
|
|
2899
|
+
operator: op,
|
|
2900
|
+
operands: operandStack.splice(0, operandStack.length)
|
|
2901
|
+
});
|
|
2902
|
+
break;
|
|
2903
|
+
}
|
|
2904
|
+
case TokenType.InlineImage: break;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
return result;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Parse a PDF array `[…]`. Called after the `[` token has been consumed.
|
|
2911
|
+
*/
|
|
2912
|
+
parseArray() {
|
|
2913
|
+
const items = [];
|
|
2914
|
+
while (true) {
|
|
2915
|
+
const token = this.nextToken();
|
|
2916
|
+
if (token.type === TokenType.EOF) break;
|
|
2917
|
+
if (token.type === TokenType.ArrayEnd) break;
|
|
2918
|
+
switch (token.type) {
|
|
2919
|
+
case TokenType.Number:
|
|
2920
|
+
case TokenType.String:
|
|
2921
|
+
case TokenType.HexString:
|
|
2922
|
+
case TokenType.Bool:
|
|
2923
|
+
case TokenType.Null:
|
|
2924
|
+
case TokenType.Name:
|
|
2925
|
+
items.push(token.value);
|
|
2926
|
+
break;
|
|
2927
|
+
case TokenType.ArrayStart:
|
|
2928
|
+
items.push(this.parseArray());
|
|
2929
|
+
break;
|
|
2930
|
+
default: break;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return items;
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Parse an inline image.
|
|
2937
|
+
*
|
|
2938
|
+
* After `BI` has been read, we expect key-value pairs (name + value)
|
|
2939
|
+
* until `ID`, then raw binary data until we find `EI` preceded by
|
|
2940
|
+
* whitespace.
|
|
2941
|
+
*/
|
|
2942
|
+
parseInlineImage() {
|
|
2943
|
+
const dict = {};
|
|
2944
|
+
while (true) {
|
|
2945
|
+
this.skipWhitespace();
|
|
2946
|
+
if (this.pos >= this.data.length) break;
|
|
2947
|
+
if (this.peekKeyword("ID")) {
|
|
2948
|
+
this.pos += 2;
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
const keyToken = this.nextToken();
|
|
2952
|
+
if (keyToken.type === TokenType.Operator) {
|
|
2953
|
+
const kw = keyToken.value;
|
|
2954
|
+
if (kw === "ID") break;
|
|
2955
|
+
dict[kw] = this.nextToken().value;
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
if (keyToken.type === TokenType.Name) {
|
|
2959
|
+
const name = keyToken.value.value;
|
|
2960
|
+
dict[name] = this.nextToken().value;
|
|
2961
|
+
} else if (keyToken.type === TokenType.EOF) break;
|
|
2962
|
+
}
|
|
2963
|
+
if (this.pos < this.data.length) {
|
|
2964
|
+
const ch = this.data[this.pos];
|
|
2965
|
+
if (ch === 32 || ch === 10 || ch === 13 || ch === 9) {
|
|
2966
|
+
this.pos++;
|
|
2967
|
+
if (ch === 13 && this.pos < this.data.length && this.data[this.pos] === 10) this.pos++;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const dataStart = this.pos;
|
|
2971
|
+
let dataEnd = this.pos;
|
|
2972
|
+
while (this.pos < this.data.length) {
|
|
2973
|
+
if (this.isWhitespace(this.data[this.pos])) {
|
|
2974
|
+
const wsPos = this.pos;
|
|
2975
|
+
let probe = wsPos + 1;
|
|
2976
|
+
if (probe + 1 < this.data.length && this.data[probe] === 69 && this.data[probe + 1] === 73) {
|
|
2977
|
+
const afterEI = probe + 2;
|
|
2978
|
+
if (afterEI >= this.data.length || this.isWhitespace(this.data[afterEI])) {
|
|
2979
|
+
dataEnd = wsPos;
|
|
2980
|
+
this.pos = afterEI;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
this.pos++;
|
|
2986
|
+
}
|
|
2987
|
+
return {
|
|
2988
|
+
dict,
|
|
2989
|
+
data: this.data.slice(dataStart, dataEnd)
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Peek ahead to see if the next characters form a given keyword
|
|
2994
|
+
* followed by whitespace.
|
|
2995
|
+
*/
|
|
2996
|
+
peekKeyword(keyword) {
|
|
2997
|
+
for (let i = 0; i < keyword.length; i++) {
|
|
2998
|
+
if (this.pos + i >= this.data.length) return false;
|
|
2999
|
+
if (this.data[this.pos + i] !== keyword.charCodeAt(i)) return false;
|
|
3000
|
+
}
|
|
3001
|
+
const afterPos = this.pos + keyword.length;
|
|
3002
|
+
if (afterPos >= this.data.length) return true;
|
|
3003
|
+
const after = this.data[afterPos];
|
|
3004
|
+
return this.isWhitespace(after) || this.isDelimiter(after);
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Read and return the next token from the stream.
|
|
3008
|
+
*/
|
|
3009
|
+
nextToken() {
|
|
3010
|
+
this.skipWhitespaceAndComments();
|
|
3011
|
+
if (this.pos >= this.data.length) return {
|
|
3012
|
+
type: TokenType.EOF,
|
|
3013
|
+
value: null
|
|
3014
|
+
};
|
|
3015
|
+
const ch = this.data[this.pos];
|
|
3016
|
+
if (ch === 40) return this.readLiteralString();
|
|
3017
|
+
if (ch === 60) {
|
|
3018
|
+
if (this.pos + 1 < this.data.length && this.data[this.pos + 1] === 60) {
|
|
3019
|
+
this.pos += 2;
|
|
3020
|
+
return this.nextToken();
|
|
3021
|
+
}
|
|
3022
|
+
return this.readHexString();
|
|
3023
|
+
}
|
|
3024
|
+
if (ch === 62 && this.pos + 1 < this.data.length && this.data[this.pos + 1] === 62) {
|
|
3025
|
+
this.pos += 2;
|
|
3026
|
+
return this.nextToken();
|
|
3027
|
+
}
|
|
3028
|
+
if (ch === 91) {
|
|
3029
|
+
this.pos++;
|
|
3030
|
+
return {
|
|
3031
|
+
type: TokenType.ArrayStart,
|
|
3032
|
+
value: null
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
if (ch === 93) {
|
|
3036
|
+
this.pos++;
|
|
3037
|
+
return {
|
|
3038
|
+
type: TokenType.ArrayEnd,
|
|
3039
|
+
value: null
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
if (ch === 47) return this.readName();
|
|
3043
|
+
if (this.isNumberStart(ch)) return this.readNumber();
|
|
3044
|
+
if (this.isRegularChar(ch)) return this.readKeyword();
|
|
3045
|
+
this.pos++;
|
|
3046
|
+
return this.nextToken();
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Read a literal string `(…)`, handling nested parentheses and escapes.
|
|
3050
|
+
*/
|
|
3051
|
+
readLiteralString() {
|
|
3052
|
+
this.pos++;
|
|
3053
|
+
let result = "";
|
|
3054
|
+
let depth = 1;
|
|
3055
|
+
while (this.pos < this.data.length && depth > 0) {
|
|
3056
|
+
const ch = this.data[this.pos];
|
|
3057
|
+
if (ch === 92) {
|
|
3058
|
+
this.pos++;
|
|
3059
|
+
if (this.pos >= this.data.length) break;
|
|
3060
|
+
const esc = this.data[this.pos];
|
|
3061
|
+
switch (esc) {
|
|
3062
|
+
case 110:
|
|
3063
|
+
result += "\n";
|
|
3064
|
+
this.pos++;
|
|
3065
|
+
break;
|
|
3066
|
+
case 114:
|
|
3067
|
+
result += "\r";
|
|
3068
|
+
this.pos++;
|
|
3069
|
+
break;
|
|
3070
|
+
case 116:
|
|
3071
|
+
result += " ";
|
|
3072
|
+
this.pos++;
|
|
3073
|
+
break;
|
|
3074
|
+
case 98:
|
|
3075
|
+
result += "\b";
|
|
3076
|
+
this.pos++;
|
|
3077
|
+
break;
|
|
3078
|
+
case 102:
|
|
3079
|
+
result += "\f";
|
|
3080
|
+
this.pos++;
|
|
3081
|
+
break;
|
|
3082
|
+
case 40:
|
|
3083
|
+
result += "(";
|
|
3084
|
+
this.pos++;
|
|
3085
|
+
break;
|
|
3086
|
+
case 41:
|
|
3087
|
+
result += ")";
|
|
3088
|
+
this.pos++;
|
|
3089
|
+
break;
|
|
3090
|
+
case 92:
|
|
3091
|
+
result += "\\";
|
|
3092
|
+
this.pos++;
|
|
3093
|
+
break;
|
|
3094
|
+
case 10:
|
|
3095
|
+
this.pos++;
|
|
3096
|
+
break;
|
|
3097
|
+
case 13:
|
|
3098
|
+
this.pos++;
|
|
3099
|
+
if (this.pos < this.data.length && this.data[this.pos] === 10) this.pos++;
|
|
3100
|
+
break;
|
|
3101
|
+
default:
|
|
3102
|
+
if (esc >= 48 && esc <= 55) {
|
|
3103
|
+
let octal = esc - 48;
|
|
3104
|
+
this.pos++;
|
|
3105
|
+
if (this.pos < this.data.length) {
|
|
3106
|
+
const d2 = this.data[this.pos];
|
|
3107
|
+
if (d2 >= 48 && d2 <= 55) {
|
|
3108
|
+
octal = octal * 8 + (d2 - 48);
|
|
3109
|
+
this.pos++;
|
|
3110
|
+
if (this.pos < this.data.length) {
|
|
3111
|
+
const d3 = this.data[this.pos];
|
|
3112
|
+
if (d3 >= 48 && d3 <= 55) {
|
|
3113
|
+
octal = octal * 8 + (d3 - 48);
|
|
3114
|
+
this.pos++;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
result += String.fromCharCode(octal & 255);
|
|
3120
|
+
} else {
|
|
3121
|
+
result += String.fromCharCode(esc);
|
|
3122
|
+
this.pos++;
|
|
3123
|
+
}
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
} else if (ch === 40) {
|
|
3127
|
+
depth++;
|
|
3128
|
+
result += "(";
|
|
3129
|
+
this.pos++;
|
|
3130
|
+
} else if (ch === 41) {
|
|
3131
|
+
depth--;
|
|
3132
|
+
if (depth > 0) result += ")";
|
|
3133
|
+
this.pos++;
|
|
3134
|
+
} else {
|
|
3135
|
+
result += String.fromCharCode(ch);
|
|
3136
|
+
this.pos++;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
return {
|
|
3140
|
+
type: TokenType.String,
|
|
3141
|
+
value: result
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Read a hex string `<…>`.
|
|
3146
|
+
*/
|
|
3147
|
+
readHexString() {
|
|
3148
|
+
this.pos++;
|
|
3149
|
+
let hex = "";
|
|
3150
|
+
while (this.pos < this.data.length) {
|
|
3151
|
+
const ch = this.data[this.pos];
|
|
3152
|
+
if (ch === 62) {
|
|
3153
|
+
this.pos++;
|
|
3154
|
+
break;
|
|
3155
|
+
}
|
|
3156
|
+
if (this.isWhitespace(ch)) {
|
|
3157
|
+
this.pos++;
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
hex += String.fromCharCode(ch);
|
|
3161
|
+
this.pos++;
|
|
3162
|
+
}
|
|
3163
|
+
if (hex.length % 2 !== 0) hex += "0";
|
|
3164
|
+
let result = "";
|
|
3165
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3166
|
+
const byte = parseInt(hex.substring(i, i + 2), 16);
|
|
3167
|
+
if (!isNaN(byte)) result += String.fromCharCode(byte);
|
|
3168
|
+
}
|
|
3169
|
+
return {
|
|
3170
|
+
type: TokenType.HexString,
|
|
3171
|
+
value: result
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Read a PDF name `/…`.
|
|
3176
|
+
*/
|
|
3177
|
+
readName() {
|
|
3178
|
+
this.pos++;
|
|
3179
|
+
let name = "/";
|
|
3180
|
+
while (this.pos < this.data.length) {
|
|
3181
|
+
const ch = this.data[this.pos];
|
|
3182
|
+
if (this.isWhitespace(ch) || this.isDelimiter(ch)) break;
|
|
3183
|
+
if (ch === 35 && this.pos + 2 < this.data.length) {
|
|
3184
|
+
const hi = this.data[this.pos + 1];
|
|
3185
|
+
const lo = this.data[this.pos + 2];
|
|
3186
|
+
const hex = String.fromCharCode(hi) + String.fromCharCode(lo);
|
|
3187
|
+
const code = parseInt(hex, 16);
|
|
3188
|
+
if (!isNaN(code)) {
|
|
3189
|
+
name += String.fromCharCode(code);
|
|
3190
|
+
this.pos += 3;
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
name += String.fromCharCode(ch);
|
|
3195
|
+
this.pos++;
|
|
3196
|
+
}
|
|
3197
|
+
return {
|
|
3198
|
+
type: TokenType.Name,
|
|
3199
|
+
value: PdfName.of(name)
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Read a numeric value (integer or real).
|
|
3204
|
+
*/
|
|
3205
|
+
readNumber() {
|
|
3206
|
+
const start = this.pos;
|
|
3207
|
+
let hasDecimal = false;
|
|
3208
|
+
if (this.data[this.pos] === 43 || this.data[this.pos] === 45) this.pos++;
|
|
3209
|
+
while (this.pos < this.data.length) {
|
|
3210
|
+
const ch = this.data[this.pos];
|
|
3211
|
+
if (ch === 46) {
|
|
3212
|
+
if (hasDecimal) break;
|
|
3213
|
+
hasDecimal = true;
|
|
3214
|
+
this.pos++;
|
|
3215
|
+
} else if (ch >= 48 && ch <= 57) this.pos++;
|
|
3216
|
+
else break;
|
|
3217
|
+
}
|
|
3218
|
+
const str = this.decodeAscii(start, this.pos);
|
|
3219
|
+
const value = parseFloat(str);
|
|
3220
|
+
return {
|
|
3221
|
+
type: TokenType.Number,
|
|
3222
|
+
value: isNaN(value) ? 0 : value
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Read a keyword — an operator name or one of the special keywords
|
|
3227
|
+
* `true`, `false`, `null`.
|
|
3228
|
+
*/
|
|
3229
|
+
readKeyword() {
|
|
3230
|
+
const start = this.pos;
|
|
3231
|
+
while (this.pos < this.data.length) {
|
|
3232
|
+
const ch = this.data[this.pos];
|
|
3233
|
+
if (this.isWhitespace(ch) || this.isDelimiter(ch)) break;
|
|
3234
|
+
this.pos++;
|
|
3235
|
+
}
|
|
3236
|
+
const word = this.decodeAscii(start, this.pos);
|
|
3237
|
+
if (word === "true") return {
|
|
3238
|
+
type: TokenType.Bool,
|
|
3239
|
+
value: true
|
|
3240
|
+
};
|
|
3241
|
+
if (word === "false") return {
|
|
3242
|
+
type: TokenType.Bool,
|
|
3243
|
+
value: false
|
|
3244
|
+
};
|
|
3245
|
+
if (word === "null") return {
|
|
3246
|
+
type: TokenType.Null,
|
|
3247
|
+
value: null
|
|
3248
|
+
};
|
|
3249
|
+
return {
|
|
3250
|
+
type: TokenType.Operator,
|
|
3251
|
+
value: word
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
/** PDF whitespace characters. */
|
|
3255
|
+
isWhitespace(ch) {
|
|
3256
|
+
return ch === 0 || ch === 9 || ch === 10 || ch === 12 || ch === 13 || ch === 32;
|
|
3257
|
+
}
|
|
3258
|
+
/** PDF delimiter characters. */
|
|
3259
|
+
isDelimiter(ch) {
|
|
3260
|
+
return ch === 40 || ch === 41 || ch === 60 || ch === 62 || ch === 91 || ch === 93 || ch === 123 || ch === 125 || ch === 47 || ch === 37;
|
|
3261
|
+
}
|
|
3262
|
+
/** Whether a character can begin a number. */
|
|
3263
|
+
isNumberStart(ch) {
|
|
3264
|
+
return ch >= 48 && ch <= 57 || ch === 43 || ch === 45 || ch === 46;
|
|
3265
|
+
}
|
|
3266
|
+
/** Whether a character is a regular (non-whitespace, non-delimiter) character. */
|
|
3267
|
+
isRegularChar(ch) {
|
|
3268
|
+
return !this.isWhitespace(ch) && !this.isDelimiter(ch);
|
|
3269
|
+
}
|
|
3270
|
+
/** Skip whitespace. */
|
|
3271
|
+
skipWhitespace() {
|
|
3272
|
+
while (this.pos < this.data.length && this.isWhitespace(this.data[this.pos])) this.pos++;
|
|
3273
|
+
}
|
|
3274
|
+
/** Skip whitespace and `%` comments. */
|
|
3275
|
+
skipWhitespaceAndComments() {
|
|
3276
|
+
while (this.pos < this.data.length) {
|
|
3277
|
+
const ch = this.data[this.pos];
|
|
3278
|
+
if (this.isWhitespace(ch)) this.pos++;
|
|
3279
|
+
else if (ch === 37) {
|
|
3280
|
+
this.pos++;
|
|
3281
|
+
while (this.pos < this.data.length) {
|
|
3282
|
+
const c = this.data[this.pos];
|
|
3283
|
+
if (c === 10 || c === 13) break;
|
|
3284
|
+
this.pos++;
|
|
3285
|
+
}
|
|
3286
|
+
} else break;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Decode a slice of the data as ASCII text.
|
|
3291
|
+
*/
|
|
3292
|
+
decodeAscii(start, end) {
|
|
3293
|
+
let s = "";
|
|
3294
|
+
for (let i = start; i < end; i++) s += String.fromCharCode(this.data[i]);
|
|
3295
|
+
return s;
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
//#endregion
|
|
3300
|
+
//#region src/signature/timestamp.ts
|
|
3301
|
+
/**
|
|
3302
|
+
* @module signature/timestamp
|
|
3303
|
+
*
|
|
3304
|
+
* RFC 3161 Timestamp Authority (TSA) client for timestamped signatures.
|
|
3305
|
+
*
|
|
3306
|
+
* Timestamps provide proof that a document was signed at a specific time,
|
|
3307
|
+
* independent of the signer's system clock. A TSA is a trusted third
|
|
3308
|
+
* party that cryptographically binds a hash to a specific moment in time.
|
|
3309
|
+
*
|
|
3310
|
+
* This module builds TimeStampReq messages, sends them to a TSA via
|
|
3311
|
+
* HTTP POST (using `fetch()`), and parses TimeStampResp messages.
|
|
3312
|
+
*
|
|
3313
|
+
* References:
|
|
3314
|
+
* - RFC 3161 (Internet X.509 PKI Time-Stamp Protocol)
|
|
3315
|
+
* - RFC 5816 (ESSCertIDv2 Update for RFC 3161)
|
|
3316
|
+
*
|
|
3317
|
+
* @packageDocumentation
|
|
3318
|
+
*/
|
|
3319
|
+
const HASH_OID_MAP = {
|
|
3320
|
+
"SHA-256": "2.16.840.1.101.3.4.2.1",
|
|
3321
|
+
"SHA-384": "2.16.840.1.101.3.4.2.2",
|
|
3322
|
+
"SHA-512": "2.16.840.1.101.3.4.2.3"
|
|
3323
|
+
};
|
|
3324
|
+
/**
|
|
3325
|
+
* Encode a BOOLEAN TRUE value in DER.
|
|
3326
|
+
*/
|
|
3327
|
+
function encodeBooleanTrue() {
|
|
3328
|
+
return new Uint8Array([
|
|
3329
|
+
1,
|
|
3330
|
+
1,
|
|
3331
|
+
255
|
|
3332
|
+
]);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Encode an INTEGER value in DER.
|
|
3336
|
+
*/
|
|
3337
|
+
function encodeIntegerValue(value) {
|
|
3338
|
+
if (value < 128) return new Uint8Array([
|
|
3339
|
+
2,
|
|
3340
|
+
1,
|
|
3341
|
+
value
|
|
3342
|
+
]);
|
|
3343
|
+
if (value < 32768) return new Uint8Array([
|
|
3344
|
+
2,
|
|
3345
|
+
2,
|
|
3346
|
+
value >> 8 & 255,
|
|
3347
|
+
value & 255
|
|
3348
|
+
]);
|
|
3349
|
+
return new Uint8Array([
|
|
3350
|
+
2,
|
|
3351
|
+
3,
|
|
3352
|
+
value >> 16 & 255,
|
|
3353
|
+
value >> 8 & 255,
|
|
3354
|
+
value & 255
|
|
3355
|
+
]);
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Parse a GeneralizedTime string (YYYYMMDDHHmmSS[.fff]Z) to a Date.
|
|
3359
|
+
*/
|
|
3360
|
+
function parseGeneralizedTime(timeStr) {
|
|
3361
|
+
const clean = timeStr.replace("Z", "").replace(/\..*$/, "");
|
|
3362
|
+
const year = parseInt(clean.substring(0, 4), 10);
|
|
3363
|
+
const month = parseInt(clean.substring(4, 6), 10) - 1;
|
|
3364
|
+
const day = parseInt(clean.substring(6, 8), 10);
|
|
3365
|
+
const hours = parseInt(clean.substring(8, 10), 10);
|
|
3366
|
+
const minutes = parseInt(clean.substring(10, 12), 10);
|
|
3367
|
+
const seconds = parseInt(clean.substring(12, 14), 10) || 0;
|
|
3368
|
+
return new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Parse a UTCTime string (YYMMDDHHmmSSZ) to a Date.
|
|
3372
|
+
*/
|
|
3373
|
+
function parseUtcTime(timeStr) {
|
|
3374
|
+
const clean = timeStr.replace("Z", "");
|
|
3375
|
+
const year = parseInt(clean.substring(0, 2), 10);
|
|
3376
|
+
const month = parseInt(clean.substring(2, 4), 10) - 1;
|
|
3377
|
+
const day = parseInt(clean.substring(4, 6), 10);
|
|
3378
|
+
const hours = parseInt(clean.substring(6, 8), 10);
|
|
3379
|
+
const minutes = parseInt(clean.substring(8, 10), 10);
|
|
3380
|
+
const seconds = parseInt(clean.substring(10, 12), 10) || 0;
|
|
3381
|
+
const fullYear = year < 50 ? 2e3 + year : 1900 + year;
|
|
3382
|
+
return new Date(Date.UTC(fullYear, month, day, hours, minutes, seconds));
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Build a DER-encoded TimeStampReq (RFC 3161 SS2.4.1).
|
|
3386
|
+
*
|
|
3387
|
+
* ```
|
|
3388
|
+
* TimeStampReq ::= SEQUENCE {
|
|
3389
|
+
* version INTEGER { v1(1) },
|
|
3390
|
+
* messageImprint MessageImprint,
|
|
3391
|
+
* reqPolicy TSAPolicyId OPTIONAL,
|
|
3392
|
+
* nonce INTEGER OPTIONAL,
|
|
3393
|
+
* certReq BOOLEAN DEFAULT FALSE,
|
|
3394
|
+
* extensions [0] IMPLICIT Extensions OPTIONAL
|
|
3395
|
+
* }
|
|
3396
|
+
*
|
|
3397
|
+
* MessageImprint ::= SEQUENCE {
|
|
3398
|
+
* hashAlgorithm AlgorithmIdentifier,
|
|
3399
|
+
* hashedMessage OCTET STRING
|
|
3400
|
+
* }
|
|
3401
|
+
* ```
|
|
3402
|
+
*
|
|
3403
|
+
* @param dataHash The hash of the data to timestamp.
|
|
3404
|
+
* @param hashAlgorithm The hash algorithm used.
|
|
3405
|
+
* @returns DER-encoded TimeStampReq.
|
|
3406
|
+
*/
|
|
3407
|
+
function buildTimestampRequest(dataHash, hashAlgorithm) {
|
|
3408
|
+
const hashOid = HASH_OID_MAP[hashAlgorithm];
|
|
3409
|
+
if (!hashOid) throw new Error(`Unsupported hash algorithm for timestamp: ${hashAlgorithm}`);
|
|
3410
|
+
const version = encodeIntegerValue(1);
|
|
3411
|
+
const messageImprint = encodeSequence([encodeSequence([encodeOID(hashOid), new Uint8Array([5, 0])]), encodeOctetString(dataHash)]);
|
|
3412
|
+
const nonceBytes = new Uint8Array(8);
|
|
3413
|
+
globalThis.crypto.getRandomValues(nonceBytes);
|
|
3414
|
+
nonceBytes[0] = nonceBytes[0] & 127;
|
|
3415
|
+
const nonce = new Uint8Array(2 + nonceBytes.length);
|
|
3416
|
+
nonce[0] = 2;
|
|
3417
|
+
nonce[1] = nonceBytes.length;
|
|
3418
|
+
nonce.set(nonceBytes, 2);
|
|
3419
|
+
return encodeSequence([
|
|
3420
|
+
version,
|
|
3421
|
+
messageImprint,
|
|
3422
|
+
nonce,
|
|
3423
|
+
encodeBooleanTrue()
|
|
3424
|
+
]);
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Parse a DER-encoded TimeStampResp (RFC 3161 SS2.4.2).
|
|
3428
|
+
*
|
|
3429
|
+
* ```
|
|
3430
|
+
* TimeStampResp ::= SEQUENCE {
|
|
3431
|
+
* status PKIStatusInfo,
|
|
3432
|
+
* timeStampToken ContentInfo OPTIONAL
|
|
3433
|
+
* }
|
|
3434
|
+
*
|
|
3435
|
+
* PKIStatusInfo ::= SEQUENCE {
|
|
3436
|
+
* status PKIStatus (INTEGER),
|
|
3437
|
+
* statusString PKIFreeText OPTIONAL,
|
|
3438
|
+
* failInfo PKIFailureInfo OPTIONAL
|
|
3439
|
+
* }
|
|
3440
|
+
* ```
|
|
3441
|
+
*
|
|
3442
|
+
* @param response DER-encoded TimeStampResp.
|
|
3443
|
+
* @returns The parsed timestamp result.
|
|
3444
|
+
* @throws Error if the TSA reported an error status.
|
|
3445
|
+
*/
|
|
3446
|
+
function parseTimestampResponse(response) {
|
|
3447
|
+
const resp = parseDerTlv(response, 0);
|
|
3448
|
+
if (resp.children.length < 1) throw new Error("Invalid TimeStampResp: empty SEQUENCE");
|
|
3449
|
+
const statusInfo = resp.children[0];
|
|
3450
|
+
const status = statusInfo.children[0].data[0];
|
|
3451
|
+
if (status !== 0 && status !== 1) {
|
|
3452
|
+
let errorMsg = `TSA returned error status ${status}`;
|
|
3453
|
+
if (statusInfo.children.length > 1) {
|
|
3454
|
+
const statusString = statusInfo.children[1];
|
|
3455
|
+
try {
|
|
3456
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
3457
|
+
errorMsg += `: ${textDecoder.decode(statusString.data)}`;
|
|
3458
|
+
} catch {}
|
|
3459
|
+
}
|
|
3460
|
+
throw new Error(errorMsg);
|
|
3461
|
+
}
|
|
3462
|
+
if (resp.children.length < 2) throw new Error("TimeStampResp contains no TimeStampToken");
|
|
3463
|
+
const tokenNode = resp.children[1];
|
|
3464
|
+
const tokenStart = tokenNode.offset;
|
|
3465
|
+
return {
|
|
3466
|
+
timestampToken: response.subarray(tokenStart, tokenStart + tokenNode.totalLength),
|
|
3467
|
+
signingTime: extractTstInfoTime(tokenNode)
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Extract the genTime from a TimeStampToken's TSTInfo.
|
|
3472
|
+
*
|
|
3473
|
+
* The token is a ContentInfo containing SignedData, which in turn
|
|
3474
|
+
* contains the TSTInfo as the encapsulated content.
|
|
3475
|
+
*/
|
|
3476
|
+
function extractTstInfoTime(contentInfo) {
|
|
3477
|
+
try {
|
|
3478
|
+
const signedData = contentInfo.children[1].children[0];
|
|
3479
|
+
for (const child of signedData.children) if (child.tag === 48 && child.children.length >= 1) {
|
|
3480
|
+
const oidNode = child.children[0];
|
|
3481
|
+
if (oidNode.tag === 6) {
|
|
3482
|
+
if (decodeOidBytes(oidNode.data) === "1.2.840.113549.1.9.16.1.4" && child.children.length >= 2) {
|
|
3483
|
+
const eContent = child.children[1].children[0];
|
|
3484
|
+
const tstInfo = parseDerTlv(eContent.data, 0);
|
|
3485
|
+
if (tstInfo.children.length >= 5) {
|
|
3486
|
+
const genTimeNode = tstInfo.children[4];
|
|
3487
|
+
const timeStr = new TextDecoder("ascii").decode(genTimeNode.data);
|
|
3488
|
+
if (genTimeNode.tag === 24) return parseGeneralizedTime(timeStr);
|
|
3489
|
+
return parseUtcTime(timeStr);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
} catch {}
|
|
3495
|
+
return /* @__PURE__ */ new Date();
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Request a timestamp from an RFC 3161 TSA.
|
|
3499
|
+
*
|
|
3500
|
+
* Sends a TimeStampReq via HTTP POST and parses the TimeStampResp.
|
|
3501
|
+
* Uses `fetch()` for universal runtime compatibility (Node.js 18+,
|
|
3502
|
+
* browsers, Deno, Bun, Cloudflare Workers).
|
|
3503
|
+
*
|
|
3504
|
+
* @param dataHash The hash of the data to timestamp.
|
|
3505
|
+
* @param tsaUrl The URL of the TSA service.
|
|
3506
|
+
* @param hashAlgorithm The hash algorithm. Default 'SHA-256'.
|
|
3507
|
+
* @returns The timestamp result.
|
|
3508
|
+
* @throws Error if the request fails or the TSA returns
|
|
3509
|
+
* an error status.
|
|
3510
|
+
*
|
|
3511
|
+
* @example
|
|
3512
|
+
* ```ts
|
|
3513
|
+
* const hash = await computeSignatureHash(pdfBytes, byteRange);
|
|
3514
|
+
* const timestamp = await requestTimestamp(
|
|
3515
|
+
* hash,
|
|
3516
|
+
* 'http://timestamp.digicert.com',
|
|
3517
|
+
* );
|
|
3518
|
+
* ```
|
|
3519
|
+
*/
|
|
3520
|
+
async function requestTimestamp(dataHash, tsaUrl, hashAlgorithm = "SHA-256") {
|
|
3521
|
+
const tsReq = buildTimestampRequest(dataHash, hashAlgorithm);
|
|
3522
|
+
const bodyBuffer = tsReq.buffer.slice(tsReq.byteOffset, tsReq.byteOffset + tsReq.byteLength);
|
|
3523
|
+
const response = await fetch(tsaUrl, {
|
|
3524
|
+
method: "POST",
|
|
3525
|
+
headers: { "Content-Type": "application/timestamp-query" },
|
|
3526
|
+
body: bodyBuffer
|
|
3527
|
+
});
|
|
3528
|
+
if (!response.ok) throw new Error(`TSA request failed: HTTP ${response.status} ${response.statusText}`);
|
|
3529
|
+
const contentType = response.headers.get("Content-Type");
|
|
3530
|
+
if (contentType && !contentType.includes("timestamp-reply")) {}
|
|
3531
|
+
return parseTimestampResponse(new Uint8Array(await response.arrayBuffer()));
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
//#endregion
|
|
3535
|
+
//#region src/core/linearization.ts
|
|
3536
|
+
const encoder$1 = new TextEncoder();
|
|
3537
|
+
const decoder$1 = new TextDecoder();
|
|
3538
|
+
/**
|
|
3539
|
+
* Check if a PDF is linearized.
|
|
3540
|
+
*
|
|
3541
|
+
* A linearized PDF has a linearization parameter dictionary as the
|
|
3542
|
+
* very first indirect object after the header. This dictionary
|
|
3543
|
+
* contains `/Linearized` as a key.
|
|
3544
|
+
*
|
|
3545
|
+
* @param pdfBytes The raw PDF bytes.
|
|
3546
|
+
* @returns `true` if the PDF appears to be linearized.
|
|
3547
|
+
*/
|
|
3548
|
+
function isLinearized(pdfBytes) {
|
|
3549
|
+
const searchWindow = Math.min(pdfBytes.length, 1024);
|
|
3550
|
+
const header = decoder$1.decode(pdfBytes.subarray(0, searchWindow));
|
|
3551
|
+
return /\/Linearized\s+[\d.]+/.test(header);
|
|
3552
|
+
}
|
|
3553
|
+
/** Find a string in the byte array, starting from `start`. */
|
|
3554
|
+
function findString(data, str, start = 0) {
|
|
3555
|
+
const bytes = encoder$1.encode(str);
|
|
3556
|
+
outer: for (let i = start; i <= data.length - bytes.length; i++) {
|
|
3557
|
+
for (let j = 0; j < bytes.length; j++) if (data[i + j] !== bytes[j]) continue outer;
|
|
3558
|
+
return i;
|
|
3559
|
+
}
|
|
3560
|
+
return -1;
|
|
3561
|
+
}
|
|
3562
|
+
/** Read an ASCII line starting at offset (until \n or \r\n). */
|
|
3563
|
+
function readLine(data, offset) {
|
|
3564
|
+
let end = offset;
|
|
3565
|
+
while (end < data.length && data[end] !== 10 && data[end] !== 13) end++;
|
|
3566
|
+
const line = decoder$1.decode(data.subarray(offset, end));
|
|
3567
|
+
let next = end;
|
|
3568
|
+
if (next < data.length && data[next] === 13) next++;
|
|
3569
|
+
if (next < data.length && data[next] === 10) next++;
|
|
3570
|
+
return {
|
|
3571
|
+
line,
|
|
3572
|
+
nextOffset: next
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
/** Parse the startxref offset from the end of the file. */
|
|
3576
|
+
function findStartXref(data) {
|
|
3577
|
+
const tail = decoder$1.decode(data.subarray(Math.max(0, data.length - 256)));
|
|
3578
|
+
const idx = tail.lastIndexOf("startxref");
|
|
3579
|
+
if (idx < 0) return -1;
|
|
3580
|
+
const lines = tail.slice(idx + 9).trim().split(/[\r\n]+/);
|
|
3581
|
+
const xrefOffset = parseInt(lines[0], 10);
|
|
3582
|
+
return isNaN(xrefOffset) ? -1 : xrefOffset;
|
|
3583
|
+
}
|
|
3584
|
+
function parseXrefTable(data, xrefOffset) {
|
|
3585
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3586
|
+
let pos = xrefOffset;
|
|
3587
|
+
pos = readLine(data, pos).nextOffset;
|
|
3588
|
+
while (pos < data.length) {
|
|
3589
|
+
const line = readLine(data, pos);
|
|
3590
|
+
if (line.line.startsWith("trailer")) break;
|
|
3591
|
+
const parts = line.line.trim().split(/\s+/);
|
|
3592
|
+
if (parts.length === 2) {
|
|
3593
|
+
const startObj = parseInt(parts[0], 10);
|
|
3594
|
+
const count = parseInt(parts[1], 10);
|
|
3595
|
+
pos = line.nextOffset;
|
|
3596
|
+
for (let i = 0; i < count; i++) {
|
|
3597
|
+
const entryLine = readLine(data, pos);
|
|
3598
|
+
pos = entryLine.nextOffset;
|
|
3599
|
+
const ep = entryLine.line.trim().split(/\s+/);
|
|
3600
|
+
if (ep.length >= 3) entries.set(startObj + i, {
|
|
3601
|
+
offset: parseInt(ep[0], 10),
|
|
3602
|
+
generation: parseInt(ep[1], 10),
|
|
3603
|
+
inUse: ep[2] === "n"
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
} else pos = line.nextOffset;
|
|
3607
|
+
}
|
|
3608
|
+
const trailerStart = findString(data, "trailer", xrefOffset);
|
|
3609
|
+
const trailerEnd = findString(data, ">>", trailerStart) + 2;
|
|
3610
|
+
const trailerStr = decoder$1.decode(data.subarray(trailerStart, trailerEnd));
|
|
3611
|
+
const sizeMatch = /\/Size\s+(\d+)/.exec(trailerStr);
|
|
3612
|
+
const rootMatch = /\/Root\s+(\d+)\s+(\d+)\s+R/.exec(trailerStr);
|
|
3613
|
+
const infoMatch = /\/Info\s+(\d+)\s+(\d+)\s+R/.exec(trailerStr);
|
|
3614
|
+
return {
|
|
3615
|
+
entries,
|
|
3616
|
+
trailer: {
|
|
3617
|
+
size: sizeMatch ? parseInt(sizeMatch[1], 10) : 0,
|
|
3618
|
+
rootRef: rootMatch ? `${rootMatch[1]} ${rootMatch[2]} R` : "1 0 R",
|
|
3619
|
+
infoRef: infoMatch ? `${infoMatch[1]} ${infoMatch[2]} R` : void 0
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
/** Extract the raw bytes of an indirect object from the PDF. */
|
|
3624
|
+
function extractObject(data, offset) {
|
|
3625
|
+
const endIdx = findString(data, "endobj", offset);
|
|
3626
|
+
if (endIdx < 0) return data.subarray(offset);
|
|
3627
|
+
return data.subarray(offset, endIdx + 6);
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Linearize a PDF document for fast web viewing.
|
|
3631
|
+
*
|
|
3632
|
+
* This reorganizes the PDF so that:
|
|
3633
|
+
* 1. A linearization parameter dictionary appears first
|
|
3634
|
+
* 2. Objects needed for the first page appear early in the file
|
|
3635
|
+
* 3. A hint table describes page offsets
|
|
3636
|
+
*
|
|
3637
|
+
* Note: This is a simplified linearization. For production use with
|
|
3638
|
+
* very large documents, a full implementation following PDF spec
|
|
3639
|
+
* Appendix F is recommended.
|
|
3640
|
+
*
|
|
3641
|
+
* @param pdfBytes The raw PDF bytes.
|
|
3642
|
+
* @param options Linearization options.
|
|
3643
|
+
* @returns The linearized PDF bytes.
|
|
3644
|
+
*/
|
|
3645
|
+
async function linearizePdf(pdfBytes, options) {
|
|
3646
|
+
const _firstPage = options?.firstPage ?? 0;
|
|
3647
|
+
if (isLinearized(pdfBytes)) return pdfBytes;
|
|
3648
|
+
const startXref = findStartXref(pdfBytes);
|
|
3649
|
+
if (startXref < 0) throw new Error("Cannot linearize: no startxref found");
|
|
3650
|
+
let xrefResult;
|
|
3651
|
+
try {
|
|
3652
|
+
xrefResult = parseXrefTable(pdfBytes, startXref);
|
|
3653
|
+
} catch {
|
|
3654
|
+
throw new Error("Cannot linearize: failed to parse xref table");
|
|
3655
|
+
}
|
|
3656
|
+
const { entries, trailer } = xrefResult;
|
|
3657
|
+
const objects = /* @__PURE__ */ new Map();
|
|
3658
|
+
for (const [objNum, entry] of entries) if (entry.inUse && objNum > 0) objects.set(objNum, extractObject(pdfBytes, entry.offset));
|
|
3659
|
+
const rootObjNum = parseInt(trailer.rootRef.split(" ")[0], 10);
|
|
3660
|
+
const catalogBytes = objects.get(rootObjNum);
|
|
3661
|
+
let pagesObjNum = 0;
|
|
3662
|
+
if (catalogBytes) {
|
|
3663
|
+
const catalogStr = decoder$1.decode(catalogBytes);
|
|
3664
|
+
const pagesMatch = /\/Pages\s+(\d+)\s+\d+\s+R/.exec(catalogStr);
|
|
3665
|
+
if (pagesMatch) pagesObjNum = parseInt(pagesMatch[1], 10);
|
|
3666
|
+
}
|
|
3667
|
+
let firstPageObjNum = 0;
|
|
3668
|
+
const pagesBytes = objects.get(pagesObjNum);
|
|
3669
|
+
if (pagesBytes) {
|
|
3670
|
+
const pagesStr = decoder$1.decode(pagesBytes);
|
|
3671
|
+
const kidsMatch = /\/Kids\s*\[([\s\S]*?)\]/.exec(pagesStr);
|
|
3672
|
+
if (kidsMatch) {
|
|
3673
|
+
const refs = kidsMatch[1].match(/(\d+)\s+\d+\s+R/g);
|
|
3674
|
+
if (refs && refs.length > _firstPage) {
|
|
3675
|
+
const ref = refs[_firstPage];
|
|
3676
|
+
firstPageObjNum = parseInt(ref.split(" ")[0], 10);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
const firstPageObjects = /* @__PURE__ */ new Set();
|
|
3681
|
+
firstPageObjects.add(rootObjNum);
|
|
3682
|
+
firstPageObjects.add(pagesObjNum);
|
|
3683
|
+
if (firstPageObjNum > 0) firstPageObjects.add(firstPageObjNum);
|
|
3684
|
+
if (firstPageObjNum > 0) {
|
|
3685
|
+
const pageBytes = objects.get(firstPageObjNum);
|
|
3686
|
+
if (pageBytes) {
|
|
3687
|
+
const refMatches = decoder$1.decode(pageBytes).matchAll(/(\d+)\s+\d+\s+R/g);
|
|
3688
|
+
for (const rm of refMatches) {
|
|
3689
|
+
const refNum = parseInt(rm[1], 10);
|
|
3690
|
+
if (objects.has(refNum)) {
|
|
3691
|
+
firstPageObjects.add(refNum);
|
|
3692
|
+
const innerBytes = objects.get(refNum);
|
|
3693
|
+
if (innerBytes) {
|
|
3694
|
+
const innerRefs = decoder$1.decode(innerBytes).matchAll(/(\d+)\s+\d+\s+R/g);
|
|
3695
|
+
for (const ir of innerRefs) {
|
|
3696
|
+
const irNum = parseInt(ir[1], 10);
|
|
3697
|
+
if (objects.has(irNum)) firstPageObjects.add(irNum);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
const remainingObjects = [...new Set(objects.keys()).difference(firstPageObjects)].sort((a, b) => a - b);
|
|
3705
|
+
const chunks = [];
|
|
3706
|
+
const newOffsets = /* @__PURE__ */ new Map();
|
|
3707
|
+
let currentOffset = 0;
|
|
3708
|
+
function writeStr(s) {
|
|
3709
|
+
const bytes = encoder$1.encode(s);
|
|
3710
|
+
chunks.push(bytes);
|
|
3711
|
+
currentOffset += bytes.length;
|
|
3712
|
+
}
|
|
3713
|
+
function writeBytes(data) {
|
|
3714
|
+
chunks.push(data);
|
|
3715
|
+
currentOffset += data.length;
|
|
3716
|
+
}
|
|
3717
|
+
writeStr("%PDF-1.7\n");
|
|
3718
|
+
writeStr("%âãÏÓ\n");
|
|
3719
|
+
const linObjNum = trailer.size;
|
|
3720
|
+
const linDictOffset = currentOffset;
|
|
3721
|
+
const linDictPlaceholder = currentOffset;
|
|
3722
|
+
const pageCount = pagesBytes ? decoder$1.decode(pagesBytes).match(/\/Count\s+(\d+)/)?.[1] ?? "1" : "1";
|
|
3723
|
+
writeStr(`${linObjNum} 0 obj\n`);
|
|
3724
|
+
writeStr(`<< /Linearized 1.0\n`);
|
|
3725
|
+
writeStr(` /L 0000000000\n`);
|
|
3726
|
+
writeStr(` /O ${firstPageObjNum}\n`);
|
|
3727
|
+
writeStr(` /E 0000000000\n`);
|
|
3728
|
+
writeStr(` /N ${pageCount}\n`);
|
|
3729
|
+
writeStr(` /T 0000000000\n`);
|
|
3730
|
+
writeStr(">>\n");
|
|
3731
|
+
writeStr("endobj\n");
|
|
3732
|
+
newOffsets.set(linObjNum, linDictOffset);
|
|
3733
|
+
const firstPageObjNums = firstPageObjects.values().toArray().sort((a, b) => a - b);
|
|
3734
|
+
for (const objNum of firstPageObjNums) {
|
|
3735
|
+
const objBytes = objects.get(objNum);
|
|
3736
|
+
if (objBytes) {
|
|
3737
|
+
newOffsets.set(objNum, currentOffset);
|
|
3738
|
+
writeBytes(objBytes);
|
|
3739
|
+
writeStr("\n");
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
const endOfFirstPage = currentOffset;
|
|
3743
|
+
for (const objNum of remainingObjects) {
|
|
3744
|
+
const objBytes = objects.get(objNum);
|
|
3745
|
+
if (objBytes) {
|
|
3746
|
+
newOffsets.set(objNum, currentOffset);
|
|
3747
|
+
writeBytes(objBytes);
|
|
3748
|
+
writeStr("\n");
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
const hintObjNum = linObjNum + 1;
|
|
3752
|
+
const hintData = buildSimpleHintStream(firstPageObjNums, remainingObjects, newOffsets);
|
|
3753
|
+
const hintOffset = currentOffset;
|
|
3754
|
+
writeStr(`${hintObjNum} 0 obj\n`);
|
|
3755
|
+
writeStr(`<< /Length ${hintData.length} /S ${hintData.length} >>\n`);
|
|
3756
|
+
writeStr("stream\n");
|
|
3757
|
+
writeBytes(hintData);
|
|
3758
|
+
writeStr("\nendstream\n");
|
|
3759
|
+
writeStr("endobj\n");
|
|
3760
|
+
newOffsets.set(hintObjNum, hintOffset);
|
|
3761
|
+
const totalSize = linObjNum + 2;
|
|
3762
|
+
const mainXrefOffset = currentOffset;
|
|
3763
|
+
writeStr("xref\n");
|
|
3764
|
+
writeStr(`0 ${totalSize}\n`);
|
|
3765
|
+
writeStr("0000000000 65535 f \n");
|
|
3766
|
+
for (let i = 1; i < totalSize; i++) {
|
|
3767
|
+
const off = newOffsets.get(i);
|
|
3768
|
+
if (off !== void 0) writeStr(`${off.toString().padStart(10, "0")} 00000 n \n`);
|
|
3769
|
+
else writeStr("0000000000 00000 f \n");
|
|
3770
|
+
}
|
|
3771
|
+
writeStr("trailer\n");
|
|
3772
|
+
writeStr("<<\n");
|
|
3773
|
+
writeStr(`/Size ${totalSize}\n`);
|
|
3774
|
+
writeStr(`/Root ${trailer.rootRef}\n`);
|
|
3775
|
+
if (trailer.infoRef) writeStr(`/Info ${trailer.infoRef}\n`);
|
|
3776
|
+
writeStr(">>\n");
|
|
3777
|
+
writeStr("startxref\n");
|
|
3778
|
+
writeStr(`${mainXrefOffset}\n`);
|
|
3779
|
+
writeStr("%%EOF\n");
|
|
3780
|
+
const fileLength = currentOffset;
|
|
3781
|
+
const result = new Uint8Array(fileLength);
|
|
3782
|
+
let pos = 0;
|
|
3783
|
+
for (const chunk of chunks) {
|
|
3784
|
+
result.set(chunk, pos);
|
|
3785
|
+
pos += chunk.length;
|
|
3786
|
+
}
|
|
3787
|
+
patchNumber(result, linDictPlaceholder, "/L ", fileLength);
|
|
3788
|
+
patchNumber(result, linDictPlaceholder, "/E ", endOfFirstPage);
|
|
3789
|
+
patchNumber(result, linDictPlaceholder, "/T ", mainXrefOffset);
|
|
3790
|
+
return result;
|
|
3791
|
+
}
|
|
3792
|
+
/** Build a simplified hint stream. */
|
|
3793
|
+
function buildSimpleHintStream(firstPageObjs, remainingObjs, offsets) {
|
|
3794
|
+
const allObjNums = firstPageObjs.concat(remainingObjs);
|
|
3795
|
+
const data = new Uint8Array(allObjNums.length * 4);
|
|
3796
|
+
for (let i = 0; i < allObjNums.length; i++) {
|
|
3797
|
+
const off = offsets.get(allObjNums[i]) ?? 0;
|
|
3798
|
+
data[i * 4] = off >>> 24 & 255;
|
|
3799
|
+
data[i * 4 + 1] = off >>> 16 & 255;
|
|
3800
|
+
data[i * 4 + 2] = off >>> 8 & 255;
|
|
3801
|
+
data[i * 4 + 3] = off & 255;
|
|
3802
|
+
}
|
|
3803
|
+
return data;
|
|
3804
|
+
}
|
|
3805
|
+
/** Patch a 10-digit number placeholder in the output. */
|
|
3806
|
+
function patchNumber(data, searchStart, prefix, value) {
|
|
3807
|
+
const prefixBytes = encoder$1.encode(prefix);
|
|
3808
|
+
const searchEnd = Math.min(searchStart + 500, data.length);
|
|
3809
|
+
outer: for (let i = searchStart; i < searchEnd - prefixBytes.length; i++) {
|
|
3810
|
+
for (let j = 0; j < prefixBytes.length; j++) if (data[i + j] !== prefixBytes[j]) continue outer;
|
|
3811
|
+
const numStr = value.toString().padStart(10, "0");
|
|
3812
|
+
const numBytes = encoder$1.encode(numStr);
|
|
3813
|
+
for (let k = 0; k < 10 && i + prefixBytes.length + k < data.length; k++) data[i + prefixBytes.length + k] = numBytes[k];
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
//#endregion
|
|
3819
|
+
//#region src/utils/binary.ts
|
|
3820
|
+
/**
|
|
3821
|
+
* Concatenate one or more `Uint8Array` buffers into a single contiguous
|
|
3822
|
+
* array.
|
|
3823
|
+
*
|
|
3824
|
+
* @param arrays The arrays to concatenate (in order).
|
|
3825
|
+
* @returns A new `Uint8Array` containing all bytes.
|
|
3826
|
+
*/
|
|
3827
|
+
function concatBytes(...arrays) {
|
|
3828
|
+
if (arrays.length === 0) return new Uint8Array(0);
|
|
3829
|
+
if (arrays.length === 1) return arrays[0];
|
|
3830
|
+
let totalLength = 0;
|
|
3831
|
+
for (const arr of arrays) totalLength += arr.length;
|
|
3832
|
+
const result = new Uint8Array(totalLength);
|
|
3833
|
+
let offset = 0;
|
|
3834
|
+
for (const arr of arrays) {
|
|
3835
|
+
result.set(arr, offset);
|
|
3836
|
+
offset += arr.length;
|
|
3837
|
+
}
|
|
3838
|
+
return result;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
//#endregion
|
|
3842
|
+
//#region src/compliance/pdfA.ts
|
|
3843
|
+
/**
|
|
3844
|
+
* @module compliance/pdfA
|
|
3845
|
+
*
|
|
3846
|
+
* PDF/A compliance validation and enforcement.
|
|
3847
|
+
*
|
|
3848
|
+
* PDF/A is an ISO standard for long-term archival of electronic documents.
|
|
3849
|
+
* It restricts certain PDF features (e.g. encryption, JavaScript,
|
|
3850
|
+
* transparency) and mandates others (e.g. embedded fonts, XMP metadata).
|
|
3851
|
+
*
|
|
3852
|
+
* Supported conformance levels:
|
|
3853
|
+
* - **1b**: PDF/A-1b (based on PDF 1.4) — basic visual preservation
|
|
3854
|
+
* - **1a**: PDF/A-1a — adds logical structure and tagged PDF
|
|
3855
|
+
* - **2b**: PDF/A-2b (based on PDF 1.7) — allows JPEG2000, transparency
|
|
3856
|
+
* - **2a**: PDF/A-2a — adds logical structure
|
|
3857
|
+
* - **2u**: PDF/A-2u — adds Unicode mapping
|
|
3858
|
+
* - **3b**: PDF/A-3b — allows embedded files of any type
|
|
3859
|
+
* - **3a**: PDF/A-3a — adds logical structure
|
|
3860
|
+
* - **3u**: PDF/A-3u — adds Unicode mapping
|
|
3861
|
+
*
|
|
3862
|
+
* Reference: ISO 19005-1:2005, ISO 19005-2:2011, ISO 19005-3:2012.
|
|
3863
|
+
*/
|
|
3864
|
+
const decoder = new TextDecoder();
|
|
3865
|
+
const encoder = new TextEncoder();
|
|
3866
|
+
/** Search for a pattern in the PDF bytes. */
|
|
3867
|
+
function findPattern(data, pattern, start = 0) {
|
|
3868
|
+
const bytes = encoder.encode(pattern);
|
|
3869
|
+
outer: for (let i = start; i <= data.length - bytes.length; i++) {
|
|
3870
|
+
for (let j = 0; j < bytes.length; j++) if (data[i + j] !== bytes[j]) continue outer;
|
|
3871
|
+
return i;
|
|
3872
|
+
}
|
|
3873
|
+
return -1;
|
|
3874
|
+
}
|
|
3875
|
+
/** Check if a pattern exists anywhere in the PDF. */
|
|
3876
|
+
function containsPattern(data, pattern) {
|
|
3877
|
+
return findPattern(data, pattern) >= 0;
|
|
3878
|
+
}
|
|
3879
|
+
/** Extract text between two markers. */
|
|
3880
|
+
function extractBetween(data, start, end) {
|
|
3881
|
+
const startIdx = findPattern(data, start);
|
|
3882
|
+
if (startIdx < 0) return void 0;
|
|
3883
|
+
const endIdx = findPattern(data, end, startIdx + start.length);
|
|
3884
|
+
if (endIdx < 0) return void 0;
|
|
3885
|
+
return decoder.decode(data.subarray(startIdx + start.length, endIdx));
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Validate a PDF against a specific PDF/A conformance level.
|
|
3889
|
+
*
|
|
3890
|
+
* This performs structural checks on the raw PDF bytes. It does NOT
|
|
3891
|
+
* fully render or deeply parse the PDF — it checks for the presence
|
|
3892
|
+
* or absence of features that PDF/A requires or forbids.
|
|
3893
|
+
*
|
|
3894
|
+
* @param pdfBytes The raw PDF bytes.
|
|
3895
|
+
* @param level The target PDF/A conformance level.
|
|
3896
|
+
* @returns A validation result with any issues found.
|
|
3897
|
+
*/
|
|
3898
|
+
function validatePdfA(pdfBytes, level) {
|
|
3899
|
+
const issues = [];
|
|
3900
|
+
const part = parseInt(level[0], 10);
|
|
3901
|
+
const conformance = level[1];
|
|
3902
|
+
if (!containsPattern(pdfBytes, "/Metadata")) issues.push({
|
|
3903
|
+
code: "PDFA-001",
|
|
3904
|
+
message: "XMP metadata stream is required but not found in the catalog.",
|
|
3905
|
+
severity: "error"
|
|
3906
|
+
});
|
|
3907
|
+
else {
|
|
3908
|
+
const xmpContent = extractBetween(pdfBytes, "<x:xmpmeta", "</x:xmpmeta>");
|
|
3909
|
+
if (xmpContent) {
|
|
3910
|
+
if (!xmpContent.includes("pdfaid:part") && !xmpContent.includes("pdfaSchema")) issues.push({
|
|
3911
|
+
code: "PDFA-002",
|
|
3912
|
+
message: "XMP metadata does not contain PDF/A identification (pdfaid:part).",
|
|
3913
|
+
severity: "error"
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
if (containsPattern(pdfBytes, "/Encrypt")) issues.push({
|
|
3918
|
+
code: "PDFA-003",
|
|
3919
|
+
message: "PDF/A documents must not be encrypted.",
|
|
3920
|
+
severity: "error"
|
|
3921
|
+
});
|
|
3922
|
+
if (containsPattern(pdfBytes, "/JavaScript") || containsPattern(pdfBytes, "/JS")) issues.push({
|
|
3923
|
+
code: "PDFA-004",
|
|
3924
|
+
message: "PDF/A documents must not contain JavaScript.",
|
|
3925
|
+
severity: "error"
|
|
3926
|
+
});
|
|
3927
|
+
const trailerIdx = findPattern(pdfBytes, "trailer");
|
|
3928
|
+
if (trailerIdx >= 0) {
|
|
3929
|
+
const trailerEnd = findPattern(pdfBytes, ">>", trailerIdx);
|
|
3930
|
+
if (trailerEnd >= 0) {
|
|
3931
|
+
if (!decoder.decode(pdfBytes.subarray(trailerIdx, trailerEnd + 2)).includes("/ID")) issues.push({
|
|
3932
|
+
code: "PDFA-005",
|
|
3933
|
+
message: "PDF/A requires a file identifier (/ID) in the trailer.",
|
|
3934
|
+
severity: "error"
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
checkFontEmbedding(pdfBytes, issues);
|
|
3939
|
+
if (part === 1) {
|
|
3940
|
+
if (containsPattern(pdfBytes, "/SMask") || containsPattern(pdfBytes, "/CA ") || containsPattern(pdfBytes, "/ca ")) {
|
|
3941
|
+
if (checkTransparency(pdfBytes)) issues.push({
|
|
3942
|
+
code: "PDFA-006",
|
|
3943
|
+
message: "PDF/A-1 does not allow transparency (SMask, non-1.0 CA/ca values).",
|
|
3944
|
+
severity: "error"
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
if (conformance === "a") {
|
|
3949
|
+
if (!containsPattern(pdfBytes, "/StructTreeRoot")) issues.push({
|
|
3950
|
+
code: "PDFA-007",
|
|
3951
|
+
message: `PDF/A-${part}a requires a document structure tree (/StructTreeRoot).`,
|
|
3952
|
+
severity: "error"
|
|
3953
|
+
});
|
|
3954
|
+
if (!containsPattern(pdfBytes, "/MarkInfo")) issues.push({
|
|
3955
|
+
code: "PDFA-008",
|
|
3956
|
+
message: `PDF/A-${part}a requires the document to be marked (/MarkInfo with /Marked true).`,
|
|
3957
|
+
severity: "error"
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
if ((conformance === "u" || conformance === "a") && part >= 2) {
|
|
3961
|
+
if (!containsPattern(pdfBytes, "/ToUnicode")) issues.push({
|
|
3962
|
+
code: "PDFA-009",
|
|
3963
|
+
message: `PDF/A-${part}${conformance} requires ToUnicode CMaps for all fonts.`,
|
|
3964
|
+
severity: "warning"
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
if (part < 3) {
|
|
3968
|
+
if (containsPattern(pdfBytes, "/EmbeddedFiles") || containsPattern(pdfBytes, "/AF ")) issues.push({
|
|
3969
|
+
code: "PDFA-010",
|
|
3970
|
+
message: `PDF/A-${part} does not allow embedded file attachments.`,
|
|
3971
|
+
severity: "error"
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
if (!containsPattern(pdfBytes, "/Lang")) issues.push({
|
|
3975
|
+
code: "PDFA-011",
|
|
3976
|
+
message: "PDF/A recommends setting the document language (/Lang in the catalog).",
|
|
3977
|
+
severity: "warning"
|
|
3978
|
+
});
|
|
3979
|
+
if (containsPattern(pdfBytes, "/DeviceRGB") || containsPattern(pdfBytes, "/DeviceCMYK")) {
|
|
3980
|
+
if (!containsPattern(pdfBytes, "/OutputIntents")) issues.push({
|
|
3981
|
+
code: "PDFA-012",
|
|
3982
|
+
message: "PDF/A requires /OutputIntents when using device-dependent color spaces.",
|
|
3983
|
+
severity: "warning"
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
return {
|
|
3987
|
+
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
3988
|
+
level,
|
|
3989
|
+
issues
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
/** Check if fonts are properly embedded. */
|
|
3993
|
+
function checkFontEmbedding(data, issues) {
|
|
3994
|
+
const fontMatches = decoder.decode(data).matchAll(/\/Subtype\s*\/Type1[\s\S]*?\/BaseFont\s*\/([\w-]+)/g);
|
|
3995
|
+
const standardFonts = new Set([
|
|
3996
|
+
"Courier",
|
|
3997
|
+
"Courier-Bold",
|
|
3998
|
+
"Courier-Oblique",
|
|
3999
|
+
"Courier-BoldOblique",
|
|
4000
|
+
"Helvetica",
|
|
4001
|
+
"Helvetica-Bold",
|
|
4002
|
+
"Helvetica-Oblique",
|
|
4003
|
+
"Helvetica-BoldOblique",
|
|
4004
|
+
"Times-Roman",
|
|
4005
|
+
"Times-Bold",
|
|
4006
|
+
"Times-Italic",
|
|
4007
|
+
"Times-BoldItalic",
|
|
4008
|
+
"Symbol",
|
|
4009
|
+
"ZapfDingbats"
|
|
4010
|
+
]);
|
|
4011
|
+
for (const m of fontMatches) {
|
|
4012
|
+
const fontName = m[1];
|
|
4013
|
+
if (standardFonts.has(fontName)) issues.push({
|
|
4014
|
+
code: "PDFA-013",
|
|
4015
|
+
message: `Font "${fontName}" must be embedded for PDF/A compliance (standard 14 fonts are not exempt).`,
|
|
4016
|
+
severity: "error"
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
/** Check if the PDF uses transparency features. */
|
|
4021
|
+
function checkTransparency(data) {
|
|
4022
|
+
const str = decoder.decode(data);
|
|
4023
|
+
if (/\/SMask\s+(?!\/None)/.test(str)) return true;
|
|
4024
|
+
const caMatch = /\/[Cc][Aa]\s+([\d.]+)/g;
|
|
4025
|
+
let m;
|
|
4026
|
+
while ((m = caMatch.exec(str)) !== null) if (parseFloat(m[1]) < 1) return true;
|
|
4027
|
+
return false;
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Attempt to make a PDF conform to PDF/A.
|
|
4031
|
+
*
|
|
4032
|
+
* This adds or corrects:
|
|
4033
|
+
* - XMP metadata with PDF/A identification
|
|
4034
|
+
* - File identifier (/ID) in the trailer
|
|
4035
|
+
* - Document language (if missing, defaults to "en")
|
|
4036
|
+
*
|
|
4037
|
+
* **Limitations:**
|
|
4038
|
+
* - Cannot embed fonts that are not already embedded
|
|
4039
|
+
* - Cannot remove encryption or JavaScript (throws an error)
|
|
4040
|
+
* - Cannot add structure tree for 'a' conformance
|
|
4041
|
+
* - For full PDF/A conversion, use a dedicated tool
|
|
4042
|
+
*
|
|
4043
|
+
* @param pdfBytes The raw PDF bytes.
|
|
4044
|
+
* @param level The target PDF/A conformance level.
|
|
4045
|
+
* @returns The modified PDF bytes.
|
|
4046
|
+
*/
|
|
4047
|
+
async function enforcePdfA(pdfBytes, level) {
|
|
4048
|
+
const validation = validatePdfA(pdfBytes, level);
|
|
4049
|
+
for (const issue of validation.issues) {
|
|
4050
|
+
if (issue.code === "PDFA-003") throw new Error("Cannot enforce PDF/A: document is encrypted. Remove encryption first.");
|
|
4051
|
+
if (issue.code === "PDFA-004") throw new Error("Cannot enforce PDF/A: document contains JavaScript. Remove it first.");
|
|
4052
|
+
}
|
|
4053
|
+
if (validation.valid) return pdfBytes;
|
|
4054
|
+
const part = parseInt(level[0], 10);
|
|
4055
|
+
const conformance = level[1];
|
|
4056
|
+
let result = pdfBytes;
|
|
4057
|
+
if (!validation.issues.every((i) => i.code !== "PDFA-001" && i.code !== "PDFA-002")) result = addPdfAXmpMetadata(result, part, conformance);
|
|
4058
|
+
if (validation.issues.some((i) => i.code === "PDFA-005")) result = addTrailerId(result);
|
|
4059
|
+
return result;
|
|
4060
|
+
}
|
|
4061
|
+
/** Generate XMP metadata with PDF/A identification and inject it. */
|
|
4062
|
+
function addPdfAXmpMetadata(data, part, conformance) {
|
|
4063
|
+
const conformanceUpper = conformance.toUpperCase();
|
|
4064
|
+
const xmp = [
|
|
4065
|
+
"<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>",
|
|
4066
|
+
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">",
|
|
4067
|
+
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">",
|
|
4068
|
+
" <rdf:Description rdf:about=\"\"",
|
|
4069
|
+
" xmlns:pdfaid=\"http://www.aiim.org/pdfa/ns/id/\"",
|
|
4070
|
+
" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"",
|
|
4071
|
+
" xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\">",
|
|
4072
|
+
` <pdfaid:part>${part}</pdfaid:part>`,
|
|
4073
|
+
` <pdfaid:conformance>${conformanceUpper}</pdfaid:conformance>`,
|
|
4074
|
+
" <xmp:CreatorTool>modern-pdf</xmp:CreatorTool>",
|
|
4075
|
+
" </rdf:Description>",
|
|
4076
|
+
" </rdf:RDF>",
|
|
4077
|
+
"</x:xmpmeta>",
|
|
4078
|
+
"<?xpacket end=\"w\"?>"
|
|
4079
|
+
].join("\n");
|
|
4080
|
+
const xmpBytes = encoder.encode(xmp);
|
|
4081
|
+
const objMatches = decoder.decode(data).matchAll(/(\d+)\s+0\s+obj/g);
|
|
4082
|
+
let maxObj = 0;
|
|
4083
|
+
for (const m of objMatches) {
|
|
4084
|
+
const num = parseInt(m[1], 10);
|
|
4085
|
+
if (num > maxObj) maxObj = num;
|
|
4086
|
+
}
|
|
4087
|
+
const metadataObjNum = maxObj + 1;
|
|
4088
|
+
const metaObj = [
|
|
4089
|
+
`${metadataObjNum} 0 obj`,
|
|
4090
|
+
`<< /Type /Metadata /Subtype /XML /Length ${xmpBytes.length} >>`,
|
|
4091
|
+
"stream"
|
|
4092
|
+
].join("\n");
|
|
4093
|
+
const metaObjEnd = "\nendstream\nendobj\n";
|
|
4094
|
+
const catalogIdx = findPattern(data, "/Type /Catalog");
|
|
4095
|
+
if (catalogIdx < 0) return data;
|
|
4096
|
+
const catalogEnd = findPattern(data, ">>", catalogIdx);
|
|
4097
|
+
if (catalogEnd < 0) return data;
|
|
4098
|
+
const metaRef = `\n/Metadata ${metadataObjNum} 0 R\n`;
|
|
4099
|
+
const parts = [];
|
|
4100
|
+
parts.push(data.subarray(0, catalogEnd));
|
|
4101
|
+
parts.push(encoder.encode(metaRef));
|
|
4102
|
+
parts.push(data.subarray(catalogEnd));
|
|
4103
|
+
parts.push(encoder.encode("\n"));
|
|
4104
|
+
parts.push(encoder.encode(metaObj + "\n"));
|
|
4105
|
+
parts.push(xmpBytes);
|
|
4106
|
+
parts.push(encoder.encode(metaObjEnd));
|
|
4107
|
+
return concatBytes(...parts);
|
|
4108
|
+
}
|
|
4109
|
+
/** Add a file identifier (/ID) to the trailer. */
|
|
4110
|
+
function addTrailerId(data) {
|
|
4111
|
+
const trailerIdx = findPattern(data, "trailer");
|
|
4112
|
+
if (trailerIdx < 0) return data;
|
|
4113
|
+
const trailerEnd = findPattern(data, ">>", trailerIdx);
|
|
4114
|
+
if (trailerEnd < 0) return data;
|
|
4115
|
+
const idEntry = `\n/ID [<${crypto.getRandomValues(new Uint8Array(16)).toHex()}> <${crypto.getRandomValues(new Uint8Array(16)).toHex()}>]\n`;
|
|
4116
|
+
const parts = [];
|
|
4117
|
+
parts.push(data.subarray(0, trailerEnd));
|
|
4118
|
+
parts.push(encoder.encode(idEntry));
|
|
4119
|
+
parts.push(data.subarray(trailerEnd));
|
|
4120
|
+
return concatBytes(...parts);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
//#endregion
|
|
4124
|
+
//#region src/errors.ts
|
|
4125
|
+
/**
|
|
4126
|
+
* @module errors
|
|
4127
|
+
*
|
|
4128
|
+
* Typed error classes for common failure modes in the modern-pdf library.
|
|
4129
|
+
* Each error class extends the native `Error` and carries a descriptive
|
|
4130
|
+
* `name` so callers can use `instanceof` checks or `error.name` comparisons.
|
|
4131
|
+
*
|
|
4132
|
+
* All constructors accept an optional `ErrorOptions` parameter to support
|
|
4133
|
+
* error chaining via the standard `{ cause }` option (ES2022+).
|
|
4134
|
+
*
|
|
4135
|
+
* These match the pdf-lib error hierarchy for API compatibility.
|
|
4136
|
+
*/
|
|
4137
|
+
/**
|
|
4138
|
+
* Thrown when attempting to load or manipulate an encrypted PDF without
|
|
4139
|
+
* providing the correct password.
|
|
4140
|
+
*/
|
|
4141
|
+
var EncryptedPdfError = class extends Error {
|
|
4142
|
+
name = "EncryptedPdfError";
|
|
4143
|
+
constructor(message = "The PDF is encrypted. Please provide a password.", options) {
|
|
4144
|
+
super(message, options);
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
/**
|
|
4148
|
+
* Thrown when a font operation requires an embedded font but none has been
|
|
4149
|
+
* registered or the font reference is invalid.
|
|
4150
|
+
*/
|
|
4151
|
+
var FontNotEmbeddedError = class extends Error {
|
|
4152
|
+
name = "FontNotEmbeddedError";
|
|
4153
|
+
constructor(fontName, options) {
|
|
4154
|
+
super(fontName ? `The font "${fontName}" has not been embedded in this document.` : "No font has been embedded. Call doc.embedFont() first.", options);
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
4157
|
+
/**
|
|
4158
|
+
* Thrown when attempting to use a page from a different document without
|
|
4159
|
+
* first copying it.
|
|
4160
|
+
*/
|
|
4161
|
+
var ForeignPageError = class extends Error {
|
|
4162
|
+
name = "ForeignPageError";
|
|
4163
|
+
constructor(options) {
|
|
4164
|
+
super("The page belongs to a different document. Use doc.copyPages() to import pages from another document before adding them.", options);
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4167
|
+
/**
|
|
4168
|
+
* Thrown when attempting to remove a page from a document that has no pages.
|
|
4169
|
+
*/
|
|
4170
|
+
var RemovePageFromEmptyDocumentError = class extends Error {
|
|
4171
|
+
name = "RemovePageFromEmptyDocumentError";
|
|
4172
|
+
constructor(options) {
|
|
4173
|
+
super("Cannot remove a page from a document with no pages.", options);
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
/**
|
|
4177
|
+
* Thrown when looking up a form field by name that does not exist.
|
|
4178
|
+
*/
|
|
4179
|
+
var NoSuchFieldError = class extends Error {
|
|
4180
|
+
name = "NoSuchFieldError";
|
|
4181
|
+
constructor(fieldName, options) {
|
|
4182
|
+
super(`No form field named "${fieldName}" exists in this document.`, options);
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
/**
|
|
4186
|
+
* Thrown when a form field is accessed via the wrong typed getter
|
|
4187
|
+
* (e.g. calling `getTextField()` on a checkbox field).
|
|
4188
|
+
*/
|
|
4189
|
+
var UnexpectedFieldTypeError = class extends Error {
|
|
4190
|
+
name = "UnexpectedFieldTypeError";
|
|
4191
|
+
constructor(fieldName, expected, actual, options) {
|
|
4192
|
+
super(`Expected field "${fieldName}" to be of type "${expected}", but it is of type "${actual}".`, options);
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
/**
|
|
4196
|
+
* Thrown when a checkbox or radio button is checked but no "on" value
|
|
4197
|
+
* can be determined from its appearance dictionary.
|
|
4198
|
+
*/
|
|
4199
|
+
var MissingOnValueCheckError = class extends Error {
|
|
4200
|
+
name = "MissingOnValueCheckError";
|
|
4201
|
+
constructor(fieldName, options) {
|
|
4202
|
+
super(`Cannot determine the "on" value for checkbox/radio "${fieldName}". The field is missing an /AP /N appearance dictionary with a non-/Off key.`, options);
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
/**
|
|
4206
|
+
* Thrown when creating a form field with a name that is already in use.
|
|
4207
|
+
*/
|
|
4208
|
+
var FieldAlreadyExistsError = class extends Error {
|
|
4209
|
+
name = "FieldAlreadyExistsError";
|
|
4210
|
+
constructor(fieldName, options) {
|
|
4211
|
+
super(`A form field named "${fieldName}" already exists.`, options);
|
|
4212
|
+
}
|
|
4213
|
+
};
|
|
4214
|
+
/**
|
|
4215
|
+
* Thrown when a field name part (between dots in a qualified name) is
|
|
4216
|
+
* empty or contains invalid characters.
|
|
4217
|
+
*/
|
|
4218
|
+
var InvalidFieldNamePartError = class extends Error {
|
|
4219
|
+
name = "InvalidFieldNamePartError";
|
|
4220
|
+
constructor(namePart, options) {
|
|
4221
|
+
super(`Invalid field name part: "${namePart}".`, options);
|
|
4222
|
+
}
|
|
4223
|
+
};
|
|
4224
|
+
/**
|
|
4225
|
+
* Thrown when attempting to create a terminal field but a non-terminal
|
|
4226
|
+
* node (a field with /Kids but no /FT) already uses the same name.
|
|
4227
|
+
*/
|
|
4228
|
+
var FieldExistsAsNonTerminalError = class extends Error {
|
|
4229
|
+
name = "FieldExistsAsNonTerminalError";
|
|
4230
|
+
constructor(fieldName, options) {
|
|
4231
|
+
super(`A non-terminal field node named "${fieldName}" already exists. Cannot create a terminal field with the same name.`, options);
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
/**
|
|
4235
|
+
* Thrown when attempting to read the rich text value (/RV) of a field
|
|
4236
|
+
* that does not support it or whose rich text is malformed.
|
|
4237
|
+
*/
|
|
4238
|
+
var RichTextFieldReadError = class extends Error {
|
|
4239
|
+
name = "RichTextFieldReadError";
|
|
4240
|
+
constructor(fieldName, options) {
|
|
4241
|
+
super(`Cannot read the rich text value of field "${fieldName}". Rich text reading is not currently supported.`, options);
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4244
|
+
/**
|
|
4245
|
+
* Thrown when a combed text field receives more characters than its
|
|
4246
|
+
* maximum length allows.
|
|
4247
|
+
*/
|
|
4248
|
+
var CombedTextLayoutError = class extends Error {
|
|
4249
|
+
name = "CombedTextLayoutError";
|
|
4250
|
+
constructor(textLength, maxLength, options) {
|
|
4251
|
+
super(`Combed text has ${textLength} characters but the field only allows a maximum of ${maxLength}.`, options);
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
/**
|
|
4255
|
+
* Thrown when a text field value exceeds the field's declared
|
|
4256
|
+
* maximum length (/MaxLen).
|
|
4257
|
+
*/
|
|
4258
|
+
var ExceededMaxLengthError = class extends Error {
|
|
4259
|
+
name = "ExceededMaxLengthError";
|
|
4260
|
+
constructor(textLength, maxLength, fieldName, options) {
|
|
4261
|
+
super(`The value for field "${fieldName}" is ${textLength} characters long, but the maximum length is ${maxLength}.`, options);
|
|
4262
|
+
}
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4265
|
+
//#endregion
|
|
4266
|
+
//#region src/index.ts
|
|
4267
|
+
/** Whether initWasm has already completed successfully. */
|
|
4268
|
+
let wasmInitialized = false;
|
|
4269
|
+
/**
|
|
4270
|
+
* Initialize the optional WASM acceleration modules.
|
|
4271
|
+
*
|
|
4272
|
+
* Call this once before `save()` if you want WASM-accelerated
|
|
4273
|
+
* compression, PNG decoding, or font subsetting. It is safe to call
|
|
4274
|
+
* multiple times -- subsequent calls are no-ops.
|
|
4275
|
+
*
|
|
4276
|
+
* If not called, the library falls back to pure-JS implementations
|
|
4277
|
+
* (fflate for compression, JS for PNG decoding).
|
|
4278
|
+
*
|
|
4279
|
+
* @param options Configuration for which WASM modules to load,
|
|
4280
|
+
* and optionally pre-loaded WASM binary bytes.
|
|
4281
|
+
* When a string or URL is passed, it is treated as
|
|
4282
|
+
* a legacy `wasmUrl` parameter (ignored for backward
|
|
4283
|
+
* compatibility).
|
|
4284
|
+
* @returns A promise that resolves when all requested modules
|
|
4285
|
+
* are ready.
|
|
4286
|
+
*/
|
|
4287
|
+
async function initWasm(options) {
|
|
4288
|
+
if (options === void 0 || typeof options === "string" || options instanceof URL) return;
|
|
4289
|
+
if (wasmInitialized) return;
|
|
4290
|
+
const inits = [];
|
|
4291
|
+
if (options.deflate || options.deflateWasm) inits.push(import("./libdeflateWasm-QVHmuzw-.mjs").then((n) => n.r).then(async ({ initDeflateWasm }) => {
|
|
4292
|
+
await initDeflateWasm(options.deflateWasm);
|
|
4293
|
+
}));
|
|
4294
|
+
if (options.png || options.pngWasm) inits.push(import("./pngEmbed-DgeNWlbS.mjs").then((n) => n.n).then(async ({ initPngWasm }) => {
|
|
4295
|
+
await initPngWasm(options.pngWasm);
|
|
4296
|
+
}));
|
|
4297
|
+
if (options.fonts || options.fontWasm) inits.push(import("./fontSubset-BOGts8y9.mjs").then((n) => n.r).then(async ({ initSubsetWasm }) => {
|
|
4298
|
+
await initSubsetWasm(options.fontWasm);
|
|
4299
|
+
}));
|
|
4300
|
+
await Promise.all(inits);
|
|
4301
|
+
wasmInitialized = true;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
//#endregion
|
|
4305
|
+
export { AnnotationFlags, BlendMode, ChangeTracker, CombedTextLayoutError, EmbeddedFont, EncryptedPdfError, ExceededMaxLengthError, FieldAlreadyExistsError, FieldExistsAsNonTerminalError, FieldFlags, FontNotEmbeddedError, ForeignPageError, ImageAlignment, InvalidFieldNamePartError, LineCapStyle, LineJoinStyle, MissingOnValueCheckError, NoSuchFieldError, PDFOperator, PageSizes, ParseSpeeds, PdfAnnotation, PdfArray, PdfBool, PdfButtonField, PdfCheckboxField, PdfCircleAnnotation, PdfDict, PdfDocument, PdfDropdownField, PdfEncryptionHandler, PdfField, PdfForm, PdfFreeTextAnnotation, PdfHighlightAnnotation, PdfInkAnnotation, PdfLayer, PdfLayerManager, PdfLineAnnotation, PdfLinkAnnotation, PdfListboxField, PdfName, PdfNull, PdfNumber, PdfObjectRegistry, PdfOutlineItem, PdfOutlineTree, PdfPage, PdfPolyLineAnnotation, PdfPolygonAnnotation, PdfRadioGroup, PdfRedactAnnotation, PdfRef, PdfSignatureField, PdfSquareAnnotation, PdfSquigglyAnnotation, PdfStampAnnotation, PdfStream, PdfStreamWriter, PdfStrikeOutAnnotation, PdfString, PdfStructureElement, PdfStructureTree, PdfTextAnnotation, PdfTextField, PdfUnderlineAnnotation, PdfViewerPreferences, PdfWriter, RemovePageFromEmptyDocumentError, RichTextFieldReadError, StandardFonts, TextAlignment, TextRenderingMode, UnexpectedFieldTypeError, addWatermark, addWatermarkToPage, aesDecryptCBC, aesEncryptCBC, annotationFromDict, applyFillColor, applyRedactions, applyStrokeColor, asNumber, asPDFName, asPDFNumber, attachFile, base64Decode, base64Encode, beginArtifact, beginArtifactWithType, beginLayerContent, beginMarkedContent, beginMarkedContentSequence, beginMarkedContentWithProperties, beginText, buildAnnotationDict, buildCatalog, buildDocumentStructure, buildEmbeddedFilesNameTree, buildInfoDict, buildPageTree, buildPkcs7Signature, buildTimestampRequest, buildViewerPreferencesDict, buildXmpMetadata, checkAccessibility, circlePath, clipEvenOdd, clip as clipOp, closeAndStroke, closeFillAndStroke, closeFillEvenOddAndStroke, closePath as closePathOp, cmyk, colorToComponents, componentsToColor, computeFileEncryptionKey, computeFontSize, computeSignatureHash, concatMatrix, concatMatrix as concatTransformationMatrix, copyPages, createAnnotation, createMarkedContentScope, createPdf, createXmpStream, cropPage, curveToFinal, curveToInitial, curveTo as curveToOp, decodePermissions, decodeStream, degrees, degreesToRadians, drawImageWithMatrix, drawImageXObject, drawXObject as drawObject, drawXObject, drawSvgOnPage, ellipsePath, embedPageAsFormXObject, embedSignature, encodeContextTag, encodeInteger, encodeLength, encodeOID, encodeOctetString, encodePermissions, encodePrintableString, encodeSequence, encodeSet, encodeUTCTime, encodeUtf8String, endArtifact, endLayerContent, endMarkedContent, endPath as endPathOp, endText, enforcePdfA, extractMetrics, extractText, extractTextWithPositions, fillAndStroke as fillAndStrokeOp, fillEvenOdd, fillEvenOddAndStroke, fill as fillOp, findSignatures, formatPdfDate, generateButtonAppearance, generateCheckboxAppearance, generateCircleAppearance, generateDropdownAppearance, generateFreeTextAppearance, generateHighlightAppearance, generateInkAppearance, generateLineAppearance, generateListboxAppearance, generateRadioAppearance, generateSignatureAppearance, generateSquareAppearance, generateSquigglyAppearance, generateStrikeOutAppearance, generateTextAppearance, generateUnderlineAppearance, getAttachments, getPageSize, getRedactionMarks, getSignatures, grayscale, initWasm, insertPage, isAccessible, isLinearized, isOpenTypeCFF, isTrueType, layoutCombedText, layoutMultilineText, layoutSinglelineText, lineTo as lineToOp, linearizePdf, loadPdf, markForRedaction, md5, mergePdfs, movePage, moveText as moveTextOp, moveTextSetLeading, moveTo as moveToOp, nextLine as nextLineOp, parseContentStream, parseSvg, parseSvgColor, parseSvgPath, parseSvgTransform, parseTimestampResponse, parseViewerPreferences, parseXmpMetadata, restoreState as popGraphicsState, restoreState, prepareForSigning, saveState as pushGraphicsState, saveState, radians, radiansToDegrees, rc4, rectangle as rectangleOp, removePage, removePages, requestTimestamp, resizePage, reversePages, rgb, rotateAllPages, rotate as rotateOp, rotatePage, rotationMatrix, saveDocumentIncremental, saveIncremental, scale as scaleOp, serializePdf, setCharacterSpacing as setCharacterSpacingOp, setCharacterSpacing as setCharacterSqueeze, setColorSpace, setDashPattern as setDashPatternOp, setFillColor, setFillColorCmyk, setFillColorGray, setFillColorRgb, setFillingColor, setFlatness, setFont as setFontAndSize, setFont as setFontOp, setFontSize as setFontSizeOp, setGraphicsState as setGraphicsStateOp, setLeading as setLeadingOp, setLeading as setLineHeight, setLineCap as setLineCapOp, setLineJoin as setLineJoinOp, setLineWidth as setLineWidthOp, setMiterLimit, setStrokeColor, setStrokeColorCmyk, setStrokeColorGray, setStrokeColorRgb, setStrokeColorSpace, setStrokingColor, setTextMatrix as setTextMatrixOp, setTextRenderingMode as setTextRenderingModeOp, setTextRise as setTextRiseOp, setWordSpacing as setWordSpacingOp, sha256, sha384, sha512, showTextArray, showTextHex, showTextNextLine, showText as showTextOp, showTextWithSpacing, signPdf, skew as skewOp, splitPdf, stroke as strokeOp, summarizeIssues, svgToPdfOperators, translate as translateOp, validatePdfA, verifyOwnerPassword, verifySignature, verifySignatures, verifyUserPassword, wrapInMarkedContent };
|
|
4306
|
+
//# sourceMappingURL=index.mjs.map
|