@cj-tech-master/excelts 9.1.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/browser/modules/archive/compression/crc32.js +1 -1
- package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/browser/modules/archive/crypto/aes.js +1 -20
- package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
- package/dist/browser/modules/archive/crypto/index.js +3 -1
- package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/browser/modules/excel/utils/encryptor.js +4 -7
- package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
- package/dist/browser/modules/pdf/core/encryption.js +8 -7
- package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/browser/modules/pdf/core/pdfa.js +261 -0
- package/dist/browser/modules/pdf/index.d.ts +11 -0
- package/dist/browser/modules/pdf/index.js +9 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
- package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/browser/modules/pdf/render/style-converter.js +1 -1
- package/dist/browser/modules/pdf/types.d.ts +14 -1
- package/dist/browser/modules/stream/browser/readable.js +8 -2
- package/dist/browser/utils/crypto.browser.d.ts +64 -0
- package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/browser/utils/crypto.d.ts +97 -0
- package/dist/browser/utils/crypto.js +209 -0
- package/dist/cjs/modules/archive/compression/crc32.js +1 -1
- package/dist/cjs/modules/archive/crypto/aes.js +2 -23
- package/dist/cjs/modules/archive/crypto/index.js +3 -1
- package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
- package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
- package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
- package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
- package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
- package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
- package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
- package/dist/cjs/modules/pdf/core/encryption.js +8 -7
- package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
- package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
- package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
- package/dist/cjs/modules/pdf/index.js +19 -1
- package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
- package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
- package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
- package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
- package/dist/cjs/modules/stream/browser/readable.js +8 -2
- package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
- package/dist/cjs/utils/crypto.js +228 -0
- package/dist/esm/modules/archive/compression/crc32.js +1 -1
- package/dist/esm/modules/archive/crypto/aes.js +1 -20
- package/dist/esm/modules/archive/crypto/index.js +3 -1
- package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/esm/modules/excel/utils/encryptor.js +4 -7
- package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
- package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
- package/dist/esm/modules/pdf/core/encryption.js +8 -7
- package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
- package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/esm/modules/pdf/core/pdfa.js +261 -0
- package/dist/esm/modules/pdf/index.js +9 -0
- package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
- package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
- package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/esm/modules/pdf/render/style-converter.js +1 -1
- package/dist/esm/modules/stream/browser/readable.js +8 -2
- package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/esm/utils/crypto.js +209 -0
- package/dist/iife/excelts.iife.js +1248 -1074
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +53 -54
- package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/types/modules/archive/crypto/index.d.ts +2 -1
- package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/types/modules/pdf/index.d.ts +11 -0
- package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/types/modules/pdf/types.d.ts +14 -1
- package/dist/types/utils/crypto.browser.d.ts +64 -0
- package/dist/types/utils/crypto.d.ts +97 -0
- package/package.json +110 -111
- package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
- package/dist/types/modules/pdf/core/crypto.d.ts +0 -65
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF document builder — high-level API for creating PDFs with free-form content.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the table-oriented `pdf()` function, this builder gives direct control
|
|
5
|
+
* over text positioning, vector drawing, images, and page management.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { PdfDocumentBuilder } from "@cj-tech-master/excelts/pdf";
|
|
10
|
+
*
|
|
11
|
+
* const doc = new PdfDocumentBuilder();
|
|
12
|
+
* const page = doc.addPage({ width: 595, height: 842 }); // A4
|
|
13
|
+
*
|
|
14
|
+
* page.drawText("Hello, World!", { x: 72, y: 750, fontSize: 24 });
|
|
15
|
+
* page.drawRect({ x: 72, y: 700, width: 200, height: 30 });
|
|
16
|
+
* page.drawCircle({ cx: 300, cy: 400, r: 50, fill: { r: 1, g: 0, b: 0 } });
|
|
17
|
+
*
|
|
18
|
+
* const bytes = await doc.build();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { PdfContentStream } from "../core/pdf-stream.js";
|
|
22
|
+
import { PdfWriter } from "../core/pdf-writer.js";
|
|
23
|
+
import { PdfDict, pdfRef, pdfString, pdfNumber } from "../core/pdf-object.js";
|
|
24
|
+
import { writePdfAMetadata, writePdfAOutputIntent } from "../core/pdfa.js";
|
|
25
|
+
import { FontManager } from "../font/font-manager.js";
|
|
26
|
+
import { parseTtf } from "../font/ttf-parser.js";
|
|
27
|
+
import { wrapTextLines } from "../render/page-renderer.js";
|
|
28
|
+
import { initEncryption } from "../core/encryption.js";
|
|
29
|
+
import { writeImageXObject } from "./image-utils.js";
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Constants
|
|
32
|
+
// =============================================================================
|
|
33
|
+
const DEFAULT_PAGE_WIDTH = 595.28; // A4
|
|
34
|
+
const DEFAULT_PAGE_HEIGHT = 841.89; // A4
|
|
35
|
+
const DEFAULT_FONT_SIZE = 12;
|
|
36
|
+
const DEFAULT_LINE_HEIGHT = 1.2;
|
|
37
|
+
const BLACK = { r: 0, g: 0, b: 0 };
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// PdfPageBuilder
|
|
40
|
+
// =============================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Builder for a single PDF page.
|
|
43
|
+
*
|
|
44
|
+
* Provides methods for drawing text, shapes, and images at arbitrary positions.
|
|
45
|
+
* All coordinates use PDF's coordinate system: origin at bottom-left, Y increases upward.
|
|
46
|
+
*/
|
|
47
|
+
export class PdfPageBuilder {
|
|
48
|
+
/** @internal */
|
|
49
|
+
constructor(width, height, fontManager) {
|
|
50
|
+
/** @internal */
|
|
51
|
+
this._stream = new PdfContentStream();
|
|
52
|
+
/** @internal */
|
|
53
|
+
this._images = [];
|
|
54
|
+
/** @internal */
|
|
55
|
+
this._annotations = [];
|
|
56
|
+
/** @internal */
|
|
57
|
+
this._builderAnnotations = [];
|
|
58
|
+
/** @internal */
|
|
59
|
+
this._formFields = [];
|
|
60
|
+
this._width = width;
|
|
61
|
+
this._height = height;
|
|
62
|
+
this._fontManager = fontManager;
|
|
63
|
+
}
|
|
64
|
+
/** Page width in points. */
|
|
65
|
+
get width() {
|
|
66
|
+
return this._width;
|
|
67
|
+
}
|
|
68
|
+
/** Page height in points. */
|
|
69
|
+
get height() {
|
|
70
|
+
return this._height;
|
|
71
|
+
}
|
|
72
|
+
// ===========================================================================
|
|
73
|
+
// Text
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
/**
|
|
76
|
+
* Draw text at a specific position.
|
|
77
|
+
*
|
|
78
|
+
* @param text - The text string to draw
|
|
79
|
+
* @param options - Position, font, color, etc.
|
|
80
|
+
*/
|
|
81
|
+
drawText(text, options) {
|
|
82
|
+
const fontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
|
|
83
|
+
const color = options.color ?? BLACK;
|
|
84
|
+
const lineHeightFactor = options.lineHeight ?? DEFAULT_LINE_HEIGHT;
|
|
85
|
+
const bold = options.bold ?? false;
|
|
86
|
+
const italic = options.italic ?? false;
|
|
87
|
+
const fontFamily = options.fontFamily ?? "Helvetica";
|
|
88
|
+
// Resolve font
|
|
89
|
+
const resourceName = this._fontManager.resolveFont(fontFamily, bold, italic);
|
|
90
|
+
const encodedText = this._fontManager.encodeText(text, resourceName);
|
|
91
|
+
this._fontManager.trackText(text);
|
|
92
|
+
if (options.maxWidth) {
|
|
93
|
+
// Word-wrap (reuses the shared wrapTextLines from page-renderer)
|
|
94
|
+
const measure = (s) => this._fontManager.measureText(s, resourceName, fontSize);
|
|
95
|
+
const lines = wrapTextLines(text, measure, options.maxWidth);
|
|
96
|
+
const leading = fontSize * lineHeightFactor;
|
|
97
|
+
this._stream.save();
|
|
98
|
+
this._stream.setFillColor(color);
|
|
99
|
+
this._stream.beginText();
|
|
100
|
+
this._stream.setFont(resourceName, fontSize);
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const lineY = options.y - i * leading;
|
|
103
|
+
this._stream.setTextMatrix(1, 0, 0, 1, options.x, lineY);
|
|
104
|
+
const lineEncoded = this._fontManager.encodeText(lines[i], resourceName);
|
|
105
|
+
if (lineEncoded) {
|
|
106
|
+
this._stream.showTextHex(lineEncoded);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this._stream.showText(lines[i]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this._stream.endText();
|
|
113
|
+
this._stream.restore();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Single line
|
|
117
|
+
this._stream.save();
|
|
118
|
+
this._stream.setFillColor(color);
|
|
119
|
+
this._stream.beginText();
|
|
120
|
+
this._stream.setFont(resourceName, fontSize);
|
|
121
|
+
this._stream.setTextMatrix(1, 0, 0, 1, options.x, options.y);
|
|
122
|
+
if (encodedText) {
|
|
123
|
+
this._stream.showTextHex(encodedText);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this._stream.showText(text);
|
|
127
|
+
}
|
|
128
|
+
this._stream.endText();
|
|
129
|
+
this._stream.restore();
|
|
130
|
+
}
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Measure text width in points.
|
|
135
|
+
*/
|
|
136
|
+
measureText(text, options) {
|
|
137
|
+
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
138
|
+
const fontFamily = options?.fontFamily ?? "Helvetica";
|
|
139
|
+
const bold = options?.bold ?? false;
|
|
140
|
+
const italic = options?.italic ?? false;
|
|
141
|
+
const resourceName = this._fontManager.resolveFont(fontFamily, bold, italic);
|
|
142
|
+
return this._fontManager.measureText(text, resourceName, fontSize);
|
|
143
|
+
}
|
|
144
|
+
// ===========================================================================
|
|
145
|
+
// Shapes
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Draw a rectangle (filled and/or stroked).
|
|
149
|
+
*/
|
|
150
|
+
drawRect(options) {
|
|
151
|
+
this._stream.save();
|
|
152
|
+
if (options.borderRadius && options.borderRadius > 0) {
|
|
153
|
+
this._stream.roundedRect(options.x, options.y, options.width, options.height, options.borderRadius);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this._stream.rect(options.x, options.y, options.width, options.height);
|
|
157
|
+
}
|
|
158
|
+
this._paintPath(options.fill, options.stroke, options.lineWidth);
|
|
159
|
+
this._stream.restore();
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Draw a circle (filled and/or stroked).
|
|
164
|
+
*/
|
|
165
|
+
drawCircle(options) {
|
|
166
|
+
this._stream.save();
|
|
167
|
+
this._stream.circle(options.cx, options.cy, options.r);
|
|
168
|
+
this._paintPath(options.fill, options.stroke, options.lineWidth);
|
|
169
|
+
this._stream.restore();
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Draw an ellipse (filled and/or stroked).
|
|
174
|
+
*/
|
|
175
|
+
drawEllipse(options) {
|
|
176
|
+
this._stream.save();
|
|
177
|
+
this._stream.ellipse(options.cx, options.cy, options.rx, options.ry);
|
|
178
|
+
this._paintPath(options.fill, options.stroke, options.lineWidth);
|
|
179
|
+
this._stream.restore();
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Draw a straight line.
|
|
184
|
+
*/
|
|
185
|
+
drawLine(options) {
|
|
186
|
+
const color = options.color ?? BLACK;
|
|
187
|
+
const lineWidth = options.lineWidth ?? 1;
|
|
188
|
+
this._stream.save();
|
|
189
|
+
this._stream.setStrokeColor(color);
|
|
190
|
+
this._stream.setLineWidth(lineWidth);
|
|
191
|
+
if (options.dashPattern && options.dashPattern.length > 0) {
|
|
192
|
+
this._stream.setDashPattern(options.dashPattern);
|
|
193
|
+
}
|
|
194
|
+
this._stream.moveTo(options.x1, options.y1);
|
|
195
|
+
this._stream.lineTo(options.x2, options.y2);
|
|
196
|
+
this._stream.stroke();
|
|
197
|
+
this._stream.restore();
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Draw a complex path from a list of path operations.
|
|
202
|
+
*/
|
|
203
|
+
drawPath(ops, options) {
|
|
204
|
+
this._stream.save();
|
|
205
|
+
for (const op of ops) {
|
|
206
|
+
switch (op.op) {
|
|
207
|
+
case "move":
|
|
208
|
+
this._stream.moveTo(op.x, op.y);
|
|
209
|
+
break;
|
|
210
|
+
case "line":
|
|
211
|
+
this._stream.lineTo(op.x, op.y);
|
|
212
|
+
break;
|
|
213
|
+
case "curve":
|
|
214
|
+
this._stream.curveTo(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3);
|
|
215
|
+
break;
|
|
216
|
+
case "close":
|
|
217
|
+
this._stream.closePath();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (options?.closePath) {
|
|
222
|
+
this._stream.closePath();
|
|
223
|
+
}
|
|
224
|
+
this._paintPath(options?.fill, options?.stroke, options?.lineWidth);
|
|
225
|
+
this._stream.restore();
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
// ===========================================================================
|
|
229
|
+
// Images
|
|
230
|
+
// ===========================================================================
|
|
231
|
+
/**
|
|
232
|
+
* Draw an image at a specific position.
|
|
233
|
+
*/
|
|
234
|
+
drawImage(options) {
|
|
235
|
+
this._images.push(options);
|
|
236
|
+
// Image drawing is deferred to build time (needs object allocation)
|
|
237
|
+
// We record a placeholder name based on index
|
|
238
|
+
const imgName = `Im${this._images.length}`;
|
|
239
|
+
this._stream.drawImage(imgName, options.x, options.y, options.width, options.height);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
// Annotations
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
/**
|
|
246
|
+
* Add an annotation to this page.
|
|
247
|
+
*
|
|
248
|
+
* Supports: Highlight, Underline, StrikeOut, Squiggly, Text (sticky note),
|
|
249
|
+
* FreeText (inline text), and Stamp.
|
|
250
|
+
*/
|
|
251
|
+
addAnnotation(options) {
|
|
252
|
+
const entries = [];
|
|
253
|
+
switch (options.type) {
|
|
254
|
+
case "Highlight":
|
|
255
|
+
case "Underline":
|
|
256
|
+
case "StrikeOut":
|
|
257
|
+
case "Squiggly": {
|
|
258
|
+
const color = options.color ??
|
|
259
|
+
(options.type === "Highlight" ? { r: 1, g: 1, b: 0 } : { r: 1, g: 0, b: 0 });
|
|
260
|
+
entries.push(["C", `[${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)}]`]);
|
|
261
|
+
if (options.contents) {
|
|
262
|
+
entries.push(["Contents", pdfString(options.contents)]);
|
|
263
|
+
}
|
|
264
|
+
if (options.author) {
|
|
265
|
+
entries.push(["T", pdfString(options.author)]);
|
|
266
|
+
}
|
|
267
|
+
// QuadPoints
|
|
268
|
+
const qp = options.quadPoints ?? [
|
|
269
|
+
options.rect[0],
|
|
270
|
+
options.rect[1],
|
|
271
|
+
options.rect[2],
|
|
272
|
+
options.rect[1],
|
|
273
|
+
options.rect[0],
|
|
274
|
+
options.rect[3],
|
|
275
|
+
options.rect[2],
|
|
276
|
+
options.rect[3]
|
|
277
|
+
];
|
|
278
|
+
entries.push(["QuadPoints", `[${qp.map(v => pdfNumber(v)).join(" ")}]`]);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "Text": {
|
|
282
|
+
const color = options.color ?? { r: 1, g: 1, b: 0 };
|
|
283
|
+
entries.push(["C", `[${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)}]`]);
|
|
284
|
+
if (options.contents) {
|
|
285
|
+
entries.push(["Contents", pdfString(options.contents)]);
|
|
286
|
+
}
|
|
287
|
+
if (options.author) {
|
|
288
|
+
entries.push(["T", pdfString(options.author)]);
|
|
289
|
+
}
|
|
290
|
+
entries.push(["Name", `/${options.iconName ?? "Note"}`]);
|
|
291
|
+
if (options.open) {
|
|
292
|
+
entries.push(["Open", "true"]);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "FreeText": {
|
|
297
|
+
const fontSize = options.fontSize ?? 12;
|
|
298
|
+
const color = options.color ?? BLACK;
|
|
299
|
+
entries.push(["Contents", pdfString(options.contents)]);
|
|
300
|
+
entries.push([
|
|
301
|
+
"DA",
|
|
302
|
+
pdfString(`/Helv ${pdfNumber(fontSize)} Tf ${pdfNumber(color.r)} ${pdfNumber(color.g)} ${pdfNumber(color.b)} rg`)
|
|
303
|
+
]);
|
|
304
|
+
if (options.borderColor) {
|
|
305
|
+
const bc = options.borderColor;
|
|
306
|
+
entries.push(["C", `[${pdfNumber(bc.r)} ${pdfNumber(bc.g)} ${pdfNumber(bc.b)}]`]);
|
|
307
|
+
}
|
|
308
|
+
if (options.author) {
|
|
309
|
+
entries.push(["T", pdfString(options.author)]);
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case "Stamp": {
|
|
314
|
+
entries.push(["Name", `/${options.stampName ?? "Draft"}`]);
|
|
315
|
+
if (options.color) {
|
|
316
|
+
const c = options.color;
|
|
317
|
+
entries.push(["C", `[${pdfNumber(c.r)} ${pdfNumber(c.g)} ${pdfNumber(c.b)}]`]);
|
|
318
|
+
}
|
|
319
|
+
if (options.contents) {
|
|
320
|
+
entries.push(["Contents", pdfString(options.contents)]);
|
|
321
|
+
}
|
|
322
|
+
if (options.author) {
|
|
323
|
+
entries.push(["T", pdfString(options.author)]);
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this._builderAnnotations.push({
|
|
329
|
+
subtype: options.type,
|
|
330
|
+
rect: options.rect,
|
|
331
|
+
entries
|
|
332
|
+
});
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
// ===========================================================================
|
|
336
|
+
// Form Fields
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Add a form field to this page.
|
|
340
|
+
*
|
|
341
|
+
* Supports: text input, checkbox, dropdown (combo box), and radio button groups.
|
|
342
|
+
*/
|
|
343
|
+
addFormField(options) {
|
|
344
|
+
this._formFields.push({ options });
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
// ===========================================================================
|
|
348
|
+
// SVG Path
|
|
349
|
+
// ===========================================================================
|
|
350
|
+
/**
|
|
351
|
+
* Draw an SVG path from a `d` attribute string.
|
|
352
|
+
*
|
|
353
|
+
* Supports all SVG path commands: M, L, H, V, C, S, Q, T, A, Z
|
|
354
|
+
* (both absolute and relative).
|
|
355
|
+
*
|
|
356
|
+
* @param d - The SVG path data string (e.g., "M10 10 L90 90 Z")
|
|
357
|
+
* @param options - Fill/stroke options
|
|
358
|
+
*/
|
|
359
|
+
drawSvgPath(d, options) {
|
|
360
|
+
const ops = parseSvgPath(d);
|
|
361
|
+
return this.drawPath(ops, options);
|
|
362
|
+
}
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
// Raw content stream access
|
|
365
|
+
// ===========================================================================
|
|
366
|
+
/**
|
|
367
|
+
* Get the raw content stream for advanced operations.
|
|
368
|
+
* Use this when the high-level API doesn't cover your use case.
|
|
369
|
+
*/
|
|
370
|
+
getContentStream() {
|
|
371
|
+
return this._stream;
|
|
372
|
+
}
|
|
373
|
+
// ===========================================================================
|
|
374
|
+
// Internal Helpers
|
|
375
|
+
// ===========================================================================
|
|
376
|
+
/** @internal */
|
|
377
|
+
_paintPath(fill, stroke, lineWidth) {
|
|
378
|
+
const hasFill = fill !== undefined;
|
|
379
|
+
const hasStroke = stroke !== undefined;
|
|
380
|
+
if (hasFill) {
|
|
381
|
+
this._stream.setFillColor(fill);
|
|
382
|
+
}
|
|
383
|
+
if (hasStroke) {
|
|
384
|
+
this._stream.setStrokeColor(stroke);
|
|
385
|
+
this._stream.setLineWidth(lineWidth ?? 1);
|
|
386
|
+
}
|
|
387
|
+
if (hasFill && hasStroke) {
|
|
388
|
+
this._stream.fillAndStroke();
|
|
389
|
+
}
|
|
390
|
+
else if (hasFill) {
|
|
391
|
+
this._stream.fill();
|
|
392
|
+
}
|
|
393
|
+
else if (hasStroke) {
|
|
394
|
+
this._stream.stroke();
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Default: stroke with black, 1pt
|
|
398
|
+
this._stream.setStrokeColor(BLACK);
|
|
399
|
+
this._stream.setLineWidth(1);
|
|
400
|
+
this._stream.stroke();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// =============================================================================
|
|
405
|
+
// PdfDocumentBuilder
|
|
406
|
+
// =============================================================================
|
|
407
|
+
/**
|
|
408
|
+
* Builder for constructing multi-page PDF documents with free-form content.
|
|
409
|
+
*
|
|
410
|
+
* Provides fine-grained control over text positioning, vector graphics,
|
|
411
|
+
* and page management — complementing the table-oriented `pdf()` function.
|
|
412
|
+
*/
|
|
413
|
+
export class PdfDocumentBuilder {
|
|
414
|
+
constructor() {
|
|
415
|
+
this._pages = [];
|
|
416
|
+
this._bookmarks = [];
|
|
417
|
+
this._fontManager = new FontManager();
|
|
418
|
+
this._metadata = {};
|
|
419
|
+
this._embeddedFont = null;
|
|
420
|
+
this._pdfA = false;
|
|
421
|
+
this._signatureOptions = null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Add a new blank page to the document.
|
|
425
|
+
*
|
|
426
|
+
* @param options - Page dimensions. Default: A4 (595.28 x 841.89 points).
|
|
427
|
+
* @returns A PdfPageBuilder for the new page.
|
|
428
|
+
*/
|
|
429
|
+
addPage(options) {
|
|
430
|
+
const width = options?.width ?? DEFAULT_PAGE_WIDTH;
|
|
431
|
+
const height = options?.height ?? DEFAULT_PAGE_HEIGHT;
|
|
432
|
+
const page = new PdfPageBuilder(width, height, this._fontManager);
|
|
433
|
+
this._pages.push(page);
|
|
434
|
+
return page;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Set document metadata (title, author, etc.).
|
|
438
|
+
*/
|
|
439
|
+
setMetadata(metadata) {
|
|
440
|
+
this._metadata = metadata;
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Set encryption options (AES-256).
|
|
445
|
+
*/
|
|
446
|
+
setEncryption(encryption) {
|
|
447
|
+
this._encryption = encryption;
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Embed a TrueType font for Unicode/CJK support.
|
|
452
|
+
*
|
|
453
|
+
* @param fontBytes - Raw .ttf file bytes
|
|
454
|
+
*/
|
|
455
|
+
embedFont(fontBytes) {
|
|
456
|
+
this._embeddedFont = fontBytes;
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Enable PDF/A compliance output.
|
|
461
|
+
*
|
|
462
|
+
* Currently supports PDF/A-1b (ISO 19005-1, Level B — visual appearance
|
|
463
|
+
* preservation). When enabled, `build()` will:
|
|
464
|
+
*
|
|
465
|
+
* - Set PDF version to 1.4
|
|
466
|
+
* - Write XMP metadata with `pdfaid:part=1` and `pdfaid:conformance=B`
|
|
467
|
+
* - Write OutputIntents with an embedded sRGB ICC profile
|
|
468
|
+
* - Add `/MarkInfo << /Marked true >>` to the catalog
|
|
469
|
+
*
|
|
470
|
+
* **Limitation:** Type1 base fonts (Helvetica, Times-Roman, Courier, etc.)
|
|
471
|
+
* are not embedded. For strict PDF/A-1b font compliance, use `embedFont()`
|
|
472
|
+
* to embed a TrueType font.
|
|
473
|
+
*
|
|
474
|
+
* @param _level - Conformance level. Currently only "1b" is supported.
|
|
475
|
+
*/
|
|
476
|
+
setPdfACompliance(_level) {
|
|
477
|
+
this._pdfA = true;
|
|
478
|
+
return this;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Digitally sign the PDF during `build()`.
|
|
482
|
+
*
|
|
483
|
+
* When set, `build()` will:
|
|
484
|
+
* 1. Embed a signature dictionary with placeholder in the PDF
|
|
485
|
+
* 2. Compute the byte ranges and sign with RSA PKCS#1 v1.5 + SHA-256
|
|
486
|
+
* 3. Return the fully signed PDF bytes
|
|
487
|
+
*
|
|
488
|
+
* @param options - Certificate, private key, and optional signer metadata
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* doc.sign({
|
|
493
|
+
* certificate: certDerBytes,
|
|
494
|
+
* privateKey: pkcs8DerBytes,
|
|
495
|
+
* name: "John Doe",
|
|
496
|
+
* reason: "Document approval"
|
|
497
|
+
* });
|
|
498
|
+
* const signedPdf = await doc.build();
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
sign(options) {
|
|
502
|
+
this._signatureOptions = options;
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
// ===========================================================================
|
|
506
|
+
// Bookmarks & Table of Contents
|
|
507
|
+
// ===========================================================================
|
|
508
|
+
/**
|
|
509
|
+
* Add a bookmark (PDF outline entry) pointing to a specific page.
|
|
510
|
+
*
|
|
511
|
+
* @param title - Bookmark display title
|
|
512
|
+
* @param pageIndex - Zero-based page index
|
|
513
|
+
* @param parent - Index of a previously added top-level bookmark to nest under (zero-based in insertion order). Omit for top-level.
|
|
514
|
+
* @returns this for chaining
|
|
515
|
+
*/
|
|
516
|
+
addBookmark(title, pageIndex, parent) {
|
|
517
|
+
const node = { title, pageIndex, children: [] };
|
|
518
|
+
if (parent !== undefined) {
|
|
519
|
+
if (parent < 0 || parent >= this._bookmarks.length) {
|
|
520
|
+
throw new RangeError(`Bookmark parent index ${parent} is out of range (0..${this._bookmarks.length - 1})`);
|
|
521
|
+
}
|
|
522
|
+
this._bookmarks[parent].children.push(node);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
this._bookmarks.push(node);
|
|
526
|
+
}
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Generate a table of contents page with clickable entries.
|
|
531
|
+
*
|
|
532
|
+
* Each entry displays the bookmark title and a right-aligned page number,
|
|
533
|
+
* connected by a dotted leader. Entries link to their target pages.
|
|
534
|
+
*
|
|
535
|
+
* @param options - TOC formatting options
|
|
536
|
+
* @returns The created PdfPageBuilder for further customization
|
|
537
|
+
*/
|
|
538
|
+
generateTableOfContents(options) {
|
|
539
|
+
const tocTitle = options?.title ?? "Table of Contents";
|
|
540
|
+
const fontSize = options?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
541
|
+
const indent = options?.indent ?? 20;
|
|
542
|
+
let page = this.addPage();
|
|
543
|
+
const firstPage = page;
|
|
544
|
+
const titleFontSize = fontSize + 6;
|
|
545
|
+
const marginLeft = 72;
|
|
546
|
+
const marginRight = 72;
|
|
547
|
+
const marginBottom = 72;
|
|
548
|
+
const usableWidth = page._width - marginLeft - marginRight;
|
|
549
|
+
let cursorY = page._height - 72;
|
|
550
|
+
// Draw TOC title
|
|
551
|
+
page.drawText(tocTitle, {
|
|
552
|
+
x: marginLeft,
|
|
553
|
+
y: cursorY,
|
|
554
|
+
fontSize: titleFontSize,
|
|
555
|
+
bold: true
|
|
556
|
+
});
|
|
557
|
+
cursorY -= titleFontSize * 1.8;
|
|
558
|
+
// Draw a separator line under the title
|
|
559
|
+
page.drawLine({
|
|
560
|
+
x1: marginLeft,
|
|
561
|
+
y1: cursorY + fontSize * 0.4,
|
|
562
|
+
x2: page._width - marginRight,
|
|
563
|
+
y2: cursorY + fontSize * 0.4,
|
|
564
|
+
color: { r: 0.6, g: 0.6, b: 0.6 },
|
|
565
|
+
lineWidth: 0.5
|
|
566
|
+
});
|
|
567
|
+
cursorY -= fontSize * 0.6;
|
|
568
|
+
// Flatten bookmarks with depth info for rendering
|
|
569
|
+
const entries = [];
|
|
570
|
+
const flattenBookmarks = (nodes, depth) => {
|
|
571
|
+
for (const node of nodes) {
|
|
572
|
+
entries.push({ title: node.title, pageIndex: node.pageIndex, depth });
|
|
573
|
+
flattenBookmarks(node.children, depth + 1);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
flattenBookmarks(this._bookmarks, 0);
|
|
577
|
+
const lineHeight = fontSize * 1.6;
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (cursorY < marginBottom) {
|
|
580
|
+
// Overflow — create a continuation page
|
|
581
|
+
page = this.addPage();
|
|
582
|
+
cursorY = page._height - 72;
|
|
583
|
+
}
|
|
584
|
+
const entryX = marginLeft + entry.depth * indent;
|
|
585
|
+
// Measure title and page number
|
|
586
|
+
const pageNumStr = String(entry.pageIndex + 1);
|
|
587
|
+
const titleWidth = page.measureText(entry.title, { fontSize });
|
|
588
|
+
const pageNumWidth = page.measureText(pageNumStr, { fontSize });
|
|
589
|
+
const dotWidth = page.measureText(".", { fontSize });
|
|
590
|
+
// Draw title text
|
|
591
|
+
page.drawText(entry.title, {
|
|
592
|
+
x: entryX,
|
|
593
|
+
y: cursorY,
|
|
594
|
+
fontSize
|
|
595
|
+
});
|
|
596
|
+
// Draw page number (right-aligned)
|
|
597
|
+
const pageNumX = marginLeft + usableWidth - pageNumWidth;
|
|
598
|
+
page.drawText(pageNumStr, {
|
|
599
|
+
x: pageNumX,
|
|
600
|
+
y: cursorY,
|
|
601
|
+
fontSize
|
|
602
|
+
});
|
|
603
|
+
// Draw dot leaders between title and page number
|
|
604
|
+
const dotsStartX = entryX + titleWidth + dotWidth;
|
|
605
|
+
const dotsEndX = pageNumX - dotWidth;
|
|
606
|
+
if (dotsEndX > dotsStartX && dotWidth > 0) {
|
|
607
|
+
const dotSpacing = dotWidth * 2;
|
|
608
|
+
let dotX = dotsStartX;
|
|
609
|
+
const dots = [];
|
|
610
|
+
while (dotX + dotWidth <= dotsEndX) {
|
|
611
|
+
dots.push(".");
|
|
612
|
+
dotX += dotSpacing;
|
|
613
|
+
}
|
|
614
|
+
if (dots.length > 0) {
|
|
615
|
+
page.drawText(dots.join(" "), {
|
|
616
|
+
x: dotsStartX,
|
|
617
|
+
y: cursorY,
|
|
618
|
+
fontSize,
|
|
619
|
+
color: { r: 0.6, g: 0.6, b: 0.6 }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Record a link annotation for this entry
|
|
624
|
+
const annotY = cursorY - fontSize * 0.3;
|
|
625
|
+
page._annotations.push({
|
|
626
|
+
rect: [entryX, annotY, marginLeft + usableWidth, annotY + fontSize * 1.2],
|
|
627
|
+
destPageIndex: entry.pageIndex
|
|
628
|
+
});
|
|
629
|
+
cursorY -= lineHeight;
|
|
630
|
+
}
|
|
631
|
+
return firstPage;
|
|
632
|
+
}
|
|
633
|
+
/** Get all pages. */
|
|
634
|
+
get pages() {
|
|
635
|
+
return this._pages;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Build the final PDF document.
|
|
639
|
+
*
|
|
640
|
+
* @returns The PDF file as Uint8Array.
|
|
641
|
+
*/
|
|
642
|
+
async build() {
|
|
643
|
+
const writer = new PdfWriter();
|
|
644
|
+
// PDF/A-1b requires PDF 1.4
|
|
645
|
+
if (this._pdfA) {
|
|
646
|
+
writer.setVersion("1.4");
|
|
647
|
+
}
|
|
648
|
+
// Register embedded font if provided
|
|
649
|
+
if (this._embeddedFont) {
|
|
650
|
+
const ttfFont = parseTtf(this._embeddedFont);
|
|
651
|
+
this._fontManager.registerEmbeddedFont(ttfFont);
|
|
652
|
+
}
|
|
653
|
+
// Write font resources
|
|
654
|
+
const fontObjectMap = this._fontManager.writeFontResources(writer);
|
|
655
|
+
const fontDictStr = this._fontManager.buildFontDictString(fontObjectMap);
|
|
656
|
+
// Build each page
|
|
657
|
+
const pageObjNums = [];
|
|
658
|
+
const pagesTreeObjNum = writer.allocObject();
|
|
659
|
+
// Pre-allocate page object numbers so annotations can reference them
|
|
660
|
+
for (let i = 0; i < this._pages.length; i++) {
|
|
661
|
+
pageObjNums.push(writer.allocObject());
|
|
662
|
+
}
|
|
663
|
+
// Track content and resource refs per page for page dict construction
|
|
664
|
+
const pageContentRefs = [];
|
|
665
|
+
const pageResourceRefs = [];
|
|
666
|
+
const allFormFieldRefs = [];
|
|
667
|
+
// Write pages with their content, resources, and annotations
|
|
668
|
+
for (let i = 0; i < this._pages.length; i++) {
|
|
669
|
+
const page = this._pages[i];
|
|
670
|
+
// Write image XObjects for this page
|
|
671
|
+
const imageXObjectMap = new Map();
|
|
672
|
+
for (let j = 0; j < page._images.length; j++) {
|
|
673
|
+
const img = page._images[j];
|
|
674
|
+
const imgName = `Im${j + 1}`;
|
|
675
|
+
const imgObjNum = this._writeImageXObject(writer, img);
|
|
676
|
+
imageXObjectMap.set(imgName, imgObjNum);
|
|
677
|
+
}
|
|
678
|
+
// Build XObject dict string
|
|
679
|
+
let xobjDictStr = "";
|
|
680
|
+
if (imageXObjectMap.size > 0) {
|
|
681
|
+
const entries = [...imageXObjectMap.entries()]
|
|
682
|
+
.map(([name, objNum]) => `/${name} ${pdfRef(objNum)}`)
|
|
683
|
+
.join(" ");
|
|
684
|
+
xobjDictStr = `<< ${entries} >>`;
|
|
685
|
+
}
|
|
686
|
+
// Write content stream
|
|
687
|
+
const contentObjNum = writer.allocObject();
|
|
688
|
+
const contentDict = new PdfDict();
|
|
689
|
+
writer.addStreamObject(contentObjNum, contentDict, page._stream);
|
|
690
|
+
pageContentRefs.push(contentObjNum);
|
|
691
|
+
// Write resources
|
|
692
|
+
const resourcesObjNum = writer.allocObject();
|
|
693
|
+
let resourcesStr = "<< ";
|
|
694
|
+
if (fontDictStr) {
|
|
695
|
+
resourcesStr += `/Font ${fontDictStr} `;
|
|
696
|
+
}
|
|
697
|
+
if (xobjDictStr) {
|
|
698
|
+
resourcesStr += `/XObject ${xobjDictStr} `;
|
|
699
|
+
}
|
|
700
|
+
resourcesStr += ">>";
|
|
701
|
+
writer.addObject(resourcesObjNum, resourcesStr);
|
|
702
|
+
pageResourceRefs.push(resourcesObjNum);
|
|
703
|
+
// Write link annotations
|
|
704
|
+
const annotRefs = [];
|
|
705
|
+
for (const annot of page._annotations) {
|
|
706
|
+
const destPageObj = pageObjNums[annot.destPageIndex];
|
|
707
|
+
if (destPageObj === undefined) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const annotObjNum = writer.allocObject();
|
|
711
|
+
const rect = `[${pdfNumber(annot.rect[0])} ${pdfNumber(annot.rect[1])} ${pdfNumber(annot.rect[2])} ${pdfNumber(annot.rect[3])}]`;
|
|
712
|
+
const annotDict = new PdfDict()
|
|
713
|
+
.set("Type", "/Annot")
|
|
714
|
+
.set("Subtype", "/Link")
|
|
715
|
+
.set("Rect", rect)
|
|
716
|
+
.set("Border", "[0 0 0]")
|
|
717
|
+
.set("Dest", `[${pdfRef(destPageObj)} /Fit]`);
|
|
718
|
+
writer.addObject(annotObjNum, annotDict);
|
|
719
|
+
annotRefs.push(annotObjNum);
|
|
720
|
+
}
|
|
721
|
+
// Write builder-created annotations (Highlight, Text, FreeText, Stamp, etc.)
|
|
722
|
+
for (const annot of page._builderAnnotations) {
|
|
723
|
+
const annotObjNum = writer.allocObject();
|
|
724
|
+
const rect = `[${pdfNumber(annot.rect[0])} ${pdfNumber(annot.rect[1])} ${pdfNumber(annot.rect[2])} ${pdfNumber(annot.rect[3])}]`;
|
|
725
|
+
const annotDict = new PdfDict()
|
|
726
|
+
.set("Type", "/Annot")
|
|
727
|
+
.set("Subtype", `/${annot.subtype}`)
|
|
728
|
+
.set("Rect", rect)
|
|
729
|
+
.set("F", "4"); // Print flag — annotation is printable
|
|
730
|
+
for (const [key, value] of annot.entries) {
|
|
731
|
+
annotDict.set(key, value);
|
|
732
|
+
}
|
|
733
|
+
writer.addObject(annotObjNum, annotDict);
|
|
734
|
+
annotRefs.push(annotObjNum);
|
|
735
|
+
}
|
|
736
|
+
// Write form field widget annotations
|
|
737
|
+
for (const field of page._formFields) {
|
|
738
|
+
const { fieldRefs, annotRefs: fieldAnnotRefs } = this._writeFormFieldAnnotation(writer, field.options, pageObjNums[i]);
|
|
739
|
+
annotRefs.push(...fieldAnnotRefs);
|
|
740
|
+
allFormFieldRefs.push(...fieldRefs);
|
|
741
|
+
}
|
|
742
|
+
// Write page object (using pre-allocated obj num)
|
|
743
|
+
const pageObjNum = pageObjNums[i];
|
|
744
|
+
const mediaBox = `[0 0 ${pdfNumber(page._width)} ${pdfNumber(page._height)}]`;
|
|
745
|
+
const pageDict = new PdfDict()
|
|
746
|
+
.set("Type", "/Page")
|
|
747
|
+
.set("Parent", pdfRef(pagesTreeObjNum))
|
|
748
|
+
.set("MediaBox", mediaBox)
|
|
749
|
+
.set("Contents", pdfRef(contentObjNum))
|
|
750
|
+
.set("Resources", pdfRef(resourcesObjNum));
|
|
751
|
+
if (annotRefs.length > 0) {
|
|
752
|
+
pageDict.set("Annots", "[" + annotRefs.map(r => pdfRef(r)).join(" ") + "]");
|
|
753
|
+
}
|
|
754
|
+
writer.addObject(pageObjNum, pageDict);
|
|
755
|
+
}
|
|
756
|
+
// Ensure at least one page
|
|
757
|
+
if (pageObjNums.length === 0) {
|
|
758
|
+
const emptyContentObjNum = writer.allocObject();
|
|
759
|
+
writer.addStreamObject(emptyContentObjNum, new PdfDict(), new Uint8Array(0));
|
|
760
|
+
const emptyResourcesObjNum = writer.allocObject();
|
|
761
|
+
writer.addObject(emptyResourcesObjNum, "<< >>");
|
|
762
|
+
const pageObjNum = writer.allocObject();
|
|
763
|
+
const emptyPageDict = new PdfDict()
|
|
764
|
+
.set("Type", "/Page")
|
|
765
|
+
.set("Parent", pdfRef(pagesTreeObjNum))
|
|
766
|
+
.set("MediaBox", `[0 0 ${pdfNumber(DEFAULT_PAGE_WIDTH)} ${pdfNumber(DEFAULT_PAGE_HEIGHT)}]`)
|
|
767
|
+
.set("Contents", pdfRef(emptyContentObjNum))
|
|
768
|
+
.set("Resources", pdfRef(emptyResourcesObjNum));
|
|
769
|
+
writer.addObject(pageObjNum, emptyPageDict);
|
|
770
|
+
pageObjNums.push(pageObjNum);
|
|
771
|
+
}
|
|
772
|
+
// Pages tree
|
|
773
|
+
const kidsStr = pageObjNums.map(n => pdfRef(n)).join(" ");
|
|
774
|
+
writer.addObject(pagesTreeObjNum, new PdfDict()
|
|
775
|
+
.set("Type", "/Pages")
|
|
776
|
+
.set("Kids", `[${kidsStr}]`)
|
|
777
|
+
.set("Count", String(pageObjNums.length)));
|
|
778
|
+
// Build outline tree from bookmarks
|
|
779
|
+
let outlinesRef;
|
|
780
|
+
if (this._bookmarks.length > 0) {
|
|
781
|
+
outlinesRef = this._buildOutlines(writer, pageObjNums);
|
|
782
|
+
}
|
|
783
|
+
// Catalog — with optional PDF/A entries
|
|
784
|
+
const catalogExtras = [];
|
|
785
|
+
if (this._pdfA) {
|
|
786
|
+
// Write XMP metadata stream
|
|
787
|
+
const xmpObjNum = writePdfAMetadata(writer, this._metadata);
|
|
788
|
+
catalogExtras.push(["Metadata", pdfRef(xmpObjNum)]);
|
|
789
|
+
// Write OutputIntents with sRGB ICC profile
|
|
790
|
+
const intentObjNum = writePdfAOutputIntent(writer);
|
|
791
|
+
catalogExtras.push(["OutputIntents", `[${pdfRef(intentObjNum)}]`]);
|
|
792
|
+
// Mark as tagged (minimal structural compliance)
|
|
793
|
+
catalogExtras.push(["MarkInfo", "<< /Marked true >>"]);
|
|
794
|
+
}
|
|
795
|
+
// Build catalog — handle three cases:
|
|
796
|
+
// 1. Simple: no form fields, no signing → addCatalog()
|
|
797
|
+
// 2. Form fields only → rebuild catalog with AcroForm
|
|
798
|
+
// 3. Signing (with or without form fields) → signing path builds the catalog
|
|
799
|
+
const needsCustomCatalog = allFormFieldRefs.length > 0 || this._signatureOptions;
|
|
800
|
+
if (!needsCustomCatalog) {
|
|
801
|
+
writer.addCatalog(pagesTreeObjNum, {
|
|
802
|
+
outlinesRef,
|
|
803
|
+
extraEntries: catalogExtras.length > 0 ? catalogExtras : undefined
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
// AcroForm — if any pages have form fields (and not signing — signing path builds its own catalog)
|
|
807
|
+
if (allFormFieldRefs.length > 0 && !this._signatureOptions) {
|
|
808
|
+
const catalogObjNum = writer.allocObject();
|
|
809
|
+
const catalogDict = new PdfDict()
|
|
810
|
+
.set("Type", "/Catalog")
|
|
811
|
+
.set("Pages", pdfRef(pagesTreeObjNum));
|
|
812
|
+
if (outlinesRef) {
|
|
813
|
+
catalogDict.set("Outlines", pdfRef(outlinesRef));
|
|
814
|
+
catalogDict.set("PageMode", "/UseOutlines");
|
|
815
|
+
}
|
|
816
|
+
for (const [key, value] of catalogExtras) {
|
|
817
|
+
catalogDict.set(key, value);
|
|
818
|
+
}
|
|
819
|
+
const fieldsStr = allFormFieldRefs.map(r => pdfRef(r)).join(" ");
|
|
820
|
+
const acroFormStr = `<< /Fields [${fieldsStr}] /NeedAppearances true /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >> /DA (/Helv 0 Tf 0 g) >>`;
|
|
821
|
+
catalogDict.set("AcroForm", acroFormStr);
|
|
822
|
+
writer.addObject(catalogObjNum, catalogDict);
|
|
823
|
+
writer.setCatalog(catalogObjNum);
|
|
824
|
+
}
|
|
825
|
+
// Info dict
|
|
826
|
+
if (this._metadata.title ||
|
|
827
|
+
this._metadata.author ||
|
|
828
|
+
this._metadata.subject ||
|
|
829
|
+
this._metadata.creator) {
|
|
830
|
+
writer.addInfoDict(this._metadata);
|
|
831
|
+
}
|
|
832
|
+
// Encryption
|
|
833
|
+
if (this._encryption) {
|
|
834
|
+
const encState = initEncryption(this._encryption);
|
|
835
|
+
writer.setEncryption(encState);
|
|
836
|
+
}
|
|
837
|
+
// If signing is requested, we need to:
|
|
838
|
+
// 1. Add the signature dict placeholder + widget to the PDF
|
|
839
|
+
// 2. Build the PDF bytes
|
|
840
|
+
// 3. Call signPdf() to fill in the real signature
|
|
841
|
+
if (this._signatureOptions) {
|
|
842
|
+
const { buildSignatureDictPlaceholder, signPdf } = await import("../core/digital-signature.js");
|
|
843
|
+
const { dictString } = buildSignatureDictPlaceholder({
|
|
844
|
+
name: this._signatureOptions.name,
|
|
845
|
+
reason: this._signatureOptions.reason,
|
|
846
|
+
location: this._signatureOptions.location,
|
|
847
|
+
contactInfo: this._signatureOptions.contactInfo
|
|
848
|
+
});
|
|
849
|
+
// Write signature dict as indirect object
|
|
850
|
+
const sigDictObjNum = writer.allocObject();
|
|
851
|
+
writer.addObject(sigDictObjNum, dictString);
|
|
852
|
+
// Write signature widget annotation
|
|
853
|
+
const sigWidgetObjNum = writer.allocObject();
|
|
854
|
+
const sigWidgetDict = new PdfDict()
|
|
855
|
+
.set("Type", "/Annot")
|
|
856
|
+
.set("Subtype", "/Widget")
|
|
857
|
+
.set("FT", "/Sig")
|
|
858
|
+
.set("Rect", "[0 0 0 0]")
|
|
859
|
+
.set("T", pdfString("Signature1"))
|
|
860
|
+
.set("V", pdfRef(sigDictObjNum))
|
|
861
|
+
.set("F", "4");
|
|
862
|
+
writer.addObject(sigWidgetObjNum, sigWidgetDict);
|
|
863
|
+
// Patch catalog to include AcroForm with SigFlags
|
|
864
|
+
// We need to rebuild the catalog with AcroForm
|
|
865
|
+
const sigCatalogObjNum = writer.allocObject();
|
|
866
|
+
const sigCatalogDict = new PdfDict()
|
|
867
|
+
.set("Type", "/Catalog")
|
|
868
|
+
.set("Pages", pdfRef(pagesTreeObjNum));
|
|
869
|
+
if (outlinesRef) {
|
|
870
|
+
sigCatalogDict.set("Outlines", pdfRef(outlinesRef));
|
|
871
|
+
sigCatalogDict.set("PageMode", "/UseOutlines");
|
|
872
|
+
}
|
|
873
|
+
for (const [key, value] of catalogExtras) {
|
|
874
|
+
sigCatalogDict.set(key, value);
|
|
875
|
+
}
|
|
876
|
+
// Merge existing form field refs with signature widget
|
|
877
|
+
const allFields = [...allFormFieldRefs, sigWidgetObjNum];
|
|
878
|
+
const fieldsStr = allFields.map(r => pdfRef(r)).join(" ");
|
|
879
|
+
// Include form field resources (NeedAppearances, DR, DA) when form fields exist
|
|
880
|
+
const hasFormFields = allFormFieldRefs.length > 0;
|
|
881
|
+
const acroFormEntries = [`/Fields [${fieldsStr}]`, "/SigFlags 3"];
|
|
882
|
+
if (hasFormFields) {
|
|
883
|
+
acroFormEntries.push("/NeedAppearances true");
|
|
884
|
+
acroFormEntries.push("/DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >>");
|
|
885
|
+
acroFormEntries.push("/DA (/Helv 0 Tf 0 g)");
|
|
886
|
+
}
|
|
887
|
+
sigCatalogDict.set("AcroForm", `<< ${acroFormEntries.join(" ")} >>`);
|
|
888
|
+
// Add signature widget to first page's annotations
|
|
889
|
+
// (We need to patch the first page dict to include the widget in /Annots)
|
|
890
|
+
// For simplicity, add it as a document-level field (already in AcroForm /Fields)
|
|
891
|
+
writer.addObject(sigCatalogObjNum, sigCatalogDict);
|
|
892
|
+
writer.setCatalog(sigCatalogObjNum);
|
|
893
|
+
const pdfWithPlaceholder = writer.build();
|
|
894
|
+
// Sign the PDF
|
|
895
|
+
return signPdf(pdfWithPlaceholder, this._signatureOptions.certificate, this._signatureOptions.privateKey);
|
|
896
|
+
}
|
|
897
|
+
return writer.build();
|
|
898
|
+
}
|
|
899
|
+
// ===========================================================================
|
|
900
|
+
// Internal Helpers
|
|
901
|
+
// ===========================================================================
|
|
902
|
+
/** @internal */
|
|
903
|
+
_writeImageXObject(writer, img) {
|
|
904
|
+
return writeImageXObject(writer, img.data, img.format);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Build a nested PDF outline (bookmark) tree.
|
|
908
|
+
* @internal
|
|
909
|
+
*/
|
|
910
|
+
_buildOutlines(writer, pageObjNums) {
|
|
911
|
+
const outlinesObjNum = writer.allocObject();
|
|
912
|
+
// Allocate object numbers for all nodes (pre-order traversal)
|
|
913
|
+
const allNodes = [];
|
|
914
|
+
const allocNodes = (nodes, parentObjNum, depth) => {
|
|
915
|
+
for (const node of nodes) {
|
|
916
|
+
const objNum = writer.allocObject();
|
|
917
|
+
allNodes.push({ node, objNum, parentObjNum, depth });
|
|
918
|
+
allocNodes(node.children, objNum, depth + 1);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
allocNodes(this._bookmarks, outlinesObjNum, 0);
|
|
922
|
+
// Group children by parent for sibling linkage
|
|
923
|
+
const childrenByParent = new Map();
|
|
924
|
+
for (const entry of allNodes) {
|
|
925
|
+
const siblings = childrenByParent.get(entry.parentObjNum);
|
|
926
|
+
if (siblings) {
|
|
927
|
+
siblings.push(entry);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
childrenByParent.set(entry.parentObjNum, [entry]);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Count all descendants (including self) for each node
|
|
934
|
+
const countDescendants = (node) => {
|
|
935
|
+
let count = 1;
|
|
936
|
+
for (const child of node.children) {
|
|
937
|
+
count += countDescendants(child);
|
|
938
|
+
}
|
|
939
|
+
return count;
|
|
940
|
+
};
|
|
941
|
+
// Write each outline item
|
|
942
|
+
for (const entry of allNodes) {
|
|
943
|
+
const { node, objNum, parentObjNum } = entry;
|
|
944
|
+
const pageObjNum = pageObjNums[node.pageIndex];
|
|
945
|
+
if (pageObjNum === undefined) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const dict = new PdfDict()
|
|
949
|
+
.set("Title", pdfString(node.title))
|
|
950
|
+
.set("Parent", pdfRef(parentObjNum))
|
|
951
|
+
.set("Dest", `[${pdfRef(pageObjNum)} /Fit]`);
|
|
952
|
+
// Sibling linkage
|
|
953
|
+
const siblings = childrenByParent.get(parentObjNum) ?? [];
|
|
954
|
+
const idx = siblings.indexOf(entry);
|
|
955
|
+
if (idx > 0) {
|
|
956
|
+
dict.set("Prev", pdfRef(siblings[idx - 1].objNum));
|
|
957
|
+
}
|
|
958
|
+
if (idx < siblings.length - 1) {
|
|
959
|
+
dict.set("Next", pdfRef(siblings[idx + 1].objNum));
|
|
960
|
+
}
|
|
961
|
+
// Children linkage
|
|
962
|
+
const children = childrenByParent.get(objNum);
|
|
963
|
+
if (children && children.length > 0) {
|
|
964
|
+
dict.set("First", pdfRef(children[0].objNum));
|
|
965
|
+
dict.set("Last", pdfRef(children[children.length - 1].objNum));
|
|
966
|
+
// Negative count = initially closed, positive = initially open
|
|
967
|
+
const totalChildren = node.children.reduce((sum, c) => sum + countDescendants(c), 0);
|
|
968
|
+
dict.set("Count", String(-totalChildren));
|
|
969
|
+
}
|
|
970
|
+
writer.addObject(objNum, dict);
|
|
971
|
+
}
|
|
972
|
+
// Write outlines root
|
|
973
|
+
const topLevel = childrenByParent.get(outlinesObjNum) ?? [];
|
|
974
|
+
const totalCount = this._bookmarks.length;
|
|
975
|
+
const outlinesDict = new PdfDict().set("Type", "/Outlines").set("Count", String(totalCount));
|
|
976
|
+
if (topLevel.length > 0) {
|
|
977
|
+
outlinesDict.set("First", pdfRef(topLevel[0].objNum));
|
|
978
|
+
outlinesDict.set("Last", pdfRef(topLevel[topLevel.length - 1].objNum));
|
|
979
|
+
}
|
|
980
|
+
writer.addObject(outlinesObjNum, outlinesDict);
|
|
981
|
+
return outlinesObjNum;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Write form field annotation(s) as indirect objects.
|
|
985
|
+
* @internal
|
|
986
|
+
*/
|
|
987
|
+
_writeFormFieldAnnotation(writer, options, pageObjNum) {
|
|
988
|
+
const fieldRefs = [];
|
|
989
|
+
const annotRefs = [];
|
|
990
|
+
if (options.type === "radio") {
|
|
991
|
+
// Radio group: one parent field + one widget per button
|
|
992
|
+
const parentObjNum = writer.allocObject();
|
|
993
|
+
const childRefs = [];
|
|
994
|
+
let ff = 1 << 15; // /Ff bit 16 = Radio
|
|
995
|
+
ff |= 1 << 14; // /Ff bit 15 = NoToggleToOff
|
|
996
|
+
if (options.readOnly) {
|
|
997
|
+
ff |= 1;
|
|
998
|
+
}
|
|
999
|
+
if (options.required) {
|
|
1000
|
+
ff |= 1 << 1;
|
|
1001
|
+
}
|
|
1002
|
+
for (const btn of options.buttons) {
|
|
1003
|
+
const childObjNum = writer.allocObject();
|
|
1004
|
+
const rect = `[${btn.rect.map(v => pdfNumber(v)).join(" ")}]`;
|
|
1005
|
+
const isSelected = options.selected === btn.value;
|
|
1006
|
+
const apState = isSelected ? `/${btn.value}` : "/Off";
|
|
1007
|
+
const childDict = new PdfDict()
|
|
1008
|
+
.set("Type", "/Annot")
|
|
1009
|
+
.set("Subtype", "/Widget")
|
|
1010
|
+
.set("Rect", rect)
|
|
1011
|
+
.set("Parent", pdfRef(parentObjNum))
|
|
1012
|
+
.set("AS", apState)
|
|
1013
|
+
.set("AP", `<< /N << /${btn.value} null /Off null >> >>`);
|
|
1014
|
+
writer.addObject(childObjNum, childDict);
|
|
1015
|
+
childRefs.push(childObjNum);
|
|
1016
|
+
}
|
|
1017
|
+
const parentDict = new PdfDict()
|
|
1018
|
+
.set("FT", "/Btn")
|
|
1019
|
+
.set("T", pdfString(options.name))
|
|
1020
|
+
.set("Ff", String(ff))
|
|
1021
|
+
.set("Kids", `[${childRefs.map(r => pdfRef(r)).join(" ")}]`);
|
|
1022
|
+
if (options.selected) {
|
|
1023
|
+
parentDict.set("V", `/${options.selected}`);
|
|
1024
|
+
}
|
|
1025
|
+
writer.addObject(parentObjNum, parentDict);
|
|
1026
|
+
// Parent goes into AcroForm /Fields; children go into page /Annots
|
|
1027
|
+
fieldRefs.push(parentObjNum);
|
|
1028
|
+
annotRefs.push(...childRefs);
|
|
1029
|
+
return { fieldRefs, annotRefs };
|
|
1030
|
+
}
|
|
1031
|
+
// Single-widget fields: text, checkbox, dropdown
|
|
1032
|
+
const objNum = writer.allocObject();
|
|
1033
|
+
const r = options.rect;
|
|
1034
|
+
const rect = `[${pdfNumber(r[0])} ${pdfNumber(r[1])} ${pdfNumber(r[2])} ${pdfNumber(r[3])}]`;
|
|
1035
|
+
const dict = new PdfDict()
|
|
1036
|
+
.set("Type", "/Annot")
|
|
1037
|
+
.set("Subtype", "/Widget")
|
|
1038
|
+
.set("Rect", rect)
|
|
1039
|
+
.set("T", pdfString(options.name))
|
|
1040
|
+
.set("P", pdfRef(pageObjNum));
|
|
1041
|
+
let ff = 0;
|
|
1042
|
+
if (options.readOnly) {
|
|
1043
|
+
ff |= 1;
|
|
1044
|
+
}
|
|
1045
|
+
if (options.required) {
|
|
1046
|
+
ff |= 1 << 1;
|
|
1047
|
+
}
|
|
1048
|
+
switch (options.type) {
|
|
1049
|
+
case "text": {
|
|
1050
|
+
dict.set("FT", "/Tx");
|
|
1051
|
+
if (options.multiline) {
|
|
1052
|
+
ff |= 1 << 12;
|
|
1053
|
+
}
|
|
1054
|
+
if (options.password) {
|
|
1055
|
+
ff |= 1 << 13;
|
|
1056
|
+
}
|
|
1057
|
+
if (options.maxLength !== undefined) {
|
|
1058
|
+
dict.set("MaxLen", String(options.maxLength));
|
|
1059
|
+
}
|
|
1060
|
+
if (options.value) {
|
|
1061
|
+
dict.set("V", pdfString(options.value));
|
|
1062
|
+
}
|
|
1063
|
+
// Default appearance
|
|
1064
|
+
dict.set("DA", pdfString("/Helv 12 Tf 0 g"));
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case "checkbox": {
|
|
1068
|
+
dict.set("FT", "/Btn");
|
|
1069
|
+
const checked = options.checked ?? false;
|
|
1070
|
+
dict.set("V", checked ? "/Yes" : "/Off");
|
|
1071
|
+
dict.set("AS", checked ? "/Yes" : "/Off");
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "dropdown": {
|
|
1075
|
+
dict.set("FT", "/Ch");
|
|
1076
|
+
ff |= 1 << 17; // Combo flag
|
|
1077
|
+
if (options.editable) {
|
|
1078
|
+
ff |= 1 << 18;
|
|
1079
|
+
}
|
|
1080
|
+
const optStr = options.options.map(o => pdfString(o)).join(" ");
|
|
1081
|
+
dict.set("Opt", `[${optStr}]`);
|
|
1082
|
+
if (options.value) {
|
|
1083
|
+
dict.set("V", pdfString(options.value));
|
|
1084
|
+
}
|
|
1085
|
+
dict.set("DA", pdfString("/Helv 12 Tf 0 g"));
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (ff !== 0) {
|
|
1090
|
+
dict.set("Ff", String(ff));
|
|
1091
|
+
}
|
|
1092
|
+
writer.addObject(objNum, dict);
|
|
1093
|
+
// Single-widget fields go into both /Annots and /Fields
|
|
1094
|
+
fieldRefs.push(objNum);
|
|
1095
|
+
annotRefs.push(objNum);
|
|
1096
|
+
return { fieldRefs, annotRefs };
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// =============================================================================
|
|
1100
|
+
// SVG Path Parser
|
|
1101
|
+
// =============================================================================
|
|
1102
|
+
/**
|
|
1103
|
+
* Parse an SVG path `d` attribute into PathOp array.
|
|
1104
|
+
*
|
|
1105
|
+
* Supports all SVG path commands:
|
|
1106
|
+
* - M/m (moveTo), L/l (lineTo), H/h (horizontal), V/v (vertical)
|
|
1107
|
+
* - C/c (cubic Bézier), S/s (smooth cubic)
|
|
1108
|
+
* - Q/q (quadratic Bézier), T/t (smooth quadratic)
|
|
1109
|
+
* - A/a (elliptical arc), Z/z (close)
|
|
1110
|
+
*
|
|
1111
|
+
* Arc commands are approximated with cubic Bézier curves.
|
|
1112
|
+
*/
|
|
1113
|
+
export function parseSvgPath(d) {
|
|
1114
|
+
const ops = [];
|
|
1115
|
+
// Tokenize: split into commands + numbers
|
|
1116
|
+
const tokens = d.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g);
|
|
1117
|
+
if (!tokens) {
|
|
1118
|
+
return ops;
|
|
1119
|
+
}
|
|
1120
|
+
let i = 0;
|
|
1121
|
+
let cx = 0; // current x
|
|
1122
|
+
let cy = 0; // current y
|
|
1123
|
+
let sx = 0; // subpath start x
|
|
1124
|
+
let sy = 0; // subpath start y
|
|
1125
|
+
let lastCmd = "";
|
|
1126
|
+
// For smooth curves: last control point
|
|
1127
|
+
let lastCpX = 0;
|
|
1128
|
+
let lastCpY = 0;
|
|
1129
|
+
const num = () => {
|
|
1130
|
+
if (i >= tokens.length) {
|
|
1131
|
+
return 0;
|
|
1132
|
+
}
|
|
1133
|
+
return parseFloat(tokens[i++]);
|
|
1134
|
+
};
|
|
1135
|
+
const isNum = () => {
|
|
1136
|
+
if (i >= tokens.length) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const c = tokens[i].charCodeAt(0);
|
|
1140
|
+
return c === 0x2d || c === 0x2b || c === 0x2e || (c >= 0x30 && c <= 0x39);
|
|
1141
|
+
};
|
|
1142
|
+
while (i < tokens.length) {
|
|
1143
|
+
let cmd = tokens[i];
|
|
1144
|
+
if (/[a-zA-Z]/.test(cmd)) {
|
|
1145
|
+
i++;
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
// Implicit repeat of last command (except M becomes L, m becomes l)
|
|
1149
|
+
cmd = lastCmd === "M" ? "L" : lastCmd === "m" ? "l" : lastCmd;
|
|
1150
|
+
}
|
|
1151
|
+
switch (cmd) {
|
|
1152
|
+
case "M":
|
|
1153
|
+
cx = num();
|
|
1154
|
+
cy = num();
|
|
1155
|
+
ops.push({ op: "move", x: cx, y: cy });
|
|
1156
|
+
sx = cx;
|
|
1157
|
+
sy = cy;
|
|
1158
|
+
lastCmd = "M";
|
|
1159
|
+
while (isNum()) {
|
|
1160
|
+
cx = num();
|
|
1161
|
+
cy = num();
|
|
1162
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1163
|
+
}
|
|
1164
|
+
break;
|
|
1165
|
+
case "m":
|
|
1166
|
+
cx += num();
|
|
1167
|
+
cy += num();
|
|
1168
|
+
ops.push({ op: "move", x: cx, y: cy });
|
|
1169
|
+
sx = cx;
|
|
1170
|
+
sy = cy;
|
|
1171
|
+
lastCmd = "m";
|
|
1172
|
+
while (isNum()) {
|
|
1173
|
+
cx += num();
|
|
1174
|
+
cy += num();
|
|
1175
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
case "L":
|
|
1179
|
+
do {
|
|
1180
|
+
cx = num();
|
|
1181
|
+
cy = num();
|
|
1182
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1183
|
+
} while (isNum());
|
|
1184
|
+
lastCmd = "L";
|
|
1185
|
+
break;
|
|
1186
|
+
case "l":
|
|
1187
|
+
do {
|
|
1188
|
+
const dx = num();
|
|
1189
|
+
const dy = num();
|
|
1190
|
+
cx += dx;
|
|
1191
|
+
cy += dy;
|
|
1192
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1193
|
+
} while (isNum());
|
|
1194
|
+
lastCmd = "l";
|
|
1195
|
+
break;
|
|
1196
|
+
case "H":
|
|
1197
|
+
do {
|
|
1198
|
+
cx = num();
|
|
1199
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1200
|
+
} while (isNum());
|
|
1201
|
+
lastCmd = "H";
|
|
1202
|
+
break;
|
|
1203
|
+
case "h":
|
|
1204
|
+
do {
|
|
1205
|
+
cx += num();
|
|
1206
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1207
|
+
} while (isNum());
|
|
1208
|
+
lastCmd = "h";
|
|
1209
|
+
break;
|
|
1210
|
+
case "V":
|
|
1211
|
+
do {
|
|
1212
|
+
cy = num();
|
|
1213
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1214
|
+
} while (isNum());
|
|
1215
|
+
lastCmd = "V";
|
|
1216
|
+
break;
|
|
1217
|
+
case "v":
|
|
1218
|
+
do {
|
|
1219
|
+
cy += num();
|
|
1220
|
+
ops.push({ op: "line", x: cx, y: cy });
|
|
1221
|
+
} while (isNum());
|
|
1222
|
+
lastCmd = "v";
|
|
1223
|
+
break;
|
|
1224
|
+
case "C":
|
|
1225
|
+
do {
|
|
1226
|
+
const x1 = num(), y1 = num(), x2 = num(), y2 = num(), x = num(), y = num();
|
|
1227
|
+
ops.push({ op: "curve", x1, y1, x2, y2, x3: x, y3: y });
|
|
1228
|
+
lastCpX = x2;
|
|
1229
|
+
lastCpY = y2;
|
|
1230
|
+
cx = x;
|
|
1231
|
+
cy = y;
|
|
1232
|
+
} while (isNum());
|
|
1233
|
+
lastCmd = "C";
|
|
1234
|
+
break;
|
|
1235
|
+
case "c":
|
|
1236
|
+
do {
|
|
1237
|
+
const x1 = cx + num(), y1 = cy + num(), x2 = cx + num(), y2 = cy + num();
|
|
1238
|
+
const x = cx + num(), y = cy + num();
|
|
1239
|
+
ops.push({ op: "curve", x1, y1, x2, y2, x3: x, y3: y });
|
|
1240
|
+
lastCpX = x2;
|
|
1241
|
+
lastCpY = y2;
|
|
1242
|
+
cx = x;
|
|
1243
|
+
cy = y;
|
|
1244
|
+
} while (isNum());
|
|
1245
|
+
lastCmd = "c";
|
|
1246
|
+
break;
|
|
1247
|
+
case "S":
|
|
1248
|
+
do {
|
|
1249
|
+
const rx = lastCmd === "S" || lastCmd === "s" || lastCmd === "C" || lastCmd === "c"
|
|
1250
|
+
? 2 * cx - lastCpX
|
|
1251
|
+
: cx;
|
|
1252
|
+
const ry = lastCmd === "S" || lastCmd === "s" || lastCmd === "C" || lastCmd === "c"
|
|
1253
|
+
? 2 * cy - lastCpY
|
|
1254
|
+
: cy;
|
|
1255
|
+
const x2 = num(), y2 = num(), x = num(), y = num();
|
|
1256
|
+
ops.push({ op: "curve", x1: rx, y1: ry, x2, y2, x3: x, y3: y });
|
|
1257
|
+
lastCpX = x2;
|
|
1258
|
+
lastCpY = y2;
|
|
1259
|
+
cx = x;
|
|
1260
|
+
cy = y;
|
|
1261
|
+
lastCmd = "S";
|
|
1262
|
+
} while (isNum());
|
|
1263
|
+
break;
|
|
1264
|
+
case "s":
|
|
1265
|
+
do {
|
|
1266
|
+
const rx = lastCmd === "S" || lastCmd === "s" || lastCmd === "C" || lastCmd === "c"
|
|
1267
|
+
? 2 * cx - lastCpX
|
|
1268
|
+
: cx;
|
|
1269
|
+
const ry = lastCmd === "S" || lastCmd === "s" || lastCmd === "C" || lastCmd === "c"
|
|
1270
|
+
? 2 * cy - lastCpY
|
|
1271
|
+
: cy;
|
|
1272
|
+
const x2 = cx + num(), y2 = cy + num(), x = cx + num(), y = cy + num();
|
|
1273
|
+
ops.push({ op: "curve", x1: rx, y1: ry, x2, y2, x3: x, y3: y });
|
|
1274
|
+
lastCpX = x2;
|
|
1275
|
+
lastCpY = y2;
|
|
1276
|
+
cx = x;
|
|
1277
|
+
cy = y;
|
|
1278
|
+
lastCmd = "s";
|
|
1279
|
+
} while (isNum());
|
|
1280
|
+
break;
|
|
1281
|
+
case "Q":
|
|
1282
|
+
do {
|
|
1283
|
+
const qx = num(), qy = num(), x = num(), y = num();
|
|
1284
|
+
// Convert quadratic to cubic: CP1 = P0 + 2/3*(QP-P0), CP2 = P1 + 2/3*(QP-P1)
|
|
1285
|
+
const c1x = cx + (2 / 3) * (qx - cx), c1y = cy + (2 / 3) * (qy - cy);
|
|
1286
|
+
const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
|
|
1287
|
+
ops.push({
|
|
1288
|
+
op: "curve",
|
|
1289
|
+
x1: c1x,
|
|
1290
|
+
y1: c1y,
|
|
1291
|
+
x2: c2x,
|
|
1292
|
+
y2: c2y,
|
|
1293
|
+
x3: x,
|
|
1294
|
+
y3: y
|
|
1295
|
+
});
|
|
1296
|
+
lastCpX = qx;
|
|
1297
|
+
lastCpY = qy;
|
|
1298
|
+
cx = x;
|
|
1299
|
+
cy = y;
|
|
1300
|
+
} while (isNum());
|
|
1301
|
+
lastCmd = "Q";
|
|
1302
|
+
break;
|
|
1303
|
+
case "q":
|
|
1304
|
+
do {
|
|
1305
|
+
const qx = cx + num(), qy = cy + num(), x = cx + num(), y = cy + num();
|
|
1306
|
+
const c1x = cx + (2 / 3) * (qx - cx), c1y = cy + (2 / 3) * (qy - cy);
|
|
1307
|
+
const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
|
|
1308
|
+
ops.push({
|
|
1309
|
+
op: "curve",
|
|
1310
|
+
x1: c1x,
|
|
1311
|
+
y1: c1y,
|
|
1312
|
+
x2: c2x,
|
|
1313
|
+
y2: c2y,
|
|
1314
|
+
x3: x,
|
|
1315
|
+
y3: y
|
|
1316
|
+
});
|
|
1317
|
+
lastCpX = qx;
|
|
1318
|
+
lastCpY = qy;
|
|
1319
|
+
cx = x;
|
|
1320
|
+
cy = y;
|
|
1321
|
+
} while (isNum());
|
|
1322
|
+
lastCmd = "q";
|
|
1323
|
+
break;
|
|
1324
|
+
case "T":
|
|
1325
|
+
do {
|
|
1326
|
+
const qx = lastCmd === "Q" || lastCmd === "q" || lastCmd === "T" || lastCmd === "t"
|
|
1327
|
+
? 2 * cx - lastCpX
|
|
1328
|
+
: cx;
|
|
1329
|
+
const qy = lastCmd === "Q" || lastCmd === "q" || lastCmd === "T" || lastCmd === "t"
|
|
1330
|
+
? 2 * cy - lastCpY
|
|
1331
|
+
: cy;
|
|
1332
|
+
const x = num(), y = num();
|
|
1333
|
+
const c1x = cx + (2 / 3) * (qx - cx), c1y = cy + (2 / 3) * (qy - cy);
|
|
1334
|
+
const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
|
|
1335
|
+
ops.push({
|
|
1336
|
+
op: "curve",
|
|
1337
|
+
x1: c1x,
|
|
1338
|
+
y1: c1y,
|
|
1339
|
+
x2: c2x,
|
|
1340
|
+
y2: c2y,
|
|
1341
|
+
x3: x,
|
|
1342
|
+
y3: y
|
|
1343
|
+
});
|
|
1344
|
+
lastCpX = qx;
|
|
1345
|
+
lastCpY = qy;
|
|
1346
|
+
cx = x;
|
|
1347
|
+
cy = y;
|
|
1348
|
+
lastCmd = "T";
|
|
1349
|
+
} while (isNum());
|
|
1350
|
+
break;
|
|
1351
|
+
case "t":
|
|
1352
|
+
do {
|
|
1353
|
+
const qx = lastCmd === "Q" || lastCmd === "q" || lastCmd === "T" || lastCmd === "t"
|
|
1354
|
+
? 2 * cx - lastCpX
|
|
1355
|
+
: cx;
|
|
1356
|
+
const qy = lastCmd === "Q" || lastCmd === "q" || lastCmd === "T" || lastCmd === "t"
|
|
1357
|
+
? 2 * cy - lastCpY
|
|
1358
|
+
: cy;
|
|
1359
|
+
const x = cx + num(), y = cy + num();
|
|
1360
|
+
const c1x = cx + (2 / 3) * (qx - cx), c1y = cy + (2 / 3) * (qy - cy);
|
|
1361
|
+
const c2x = x + (2 / 3) * (qx - x), c2y = y + (2 / 3) * (qy - y);
|
|
1362
|
+
ops.push({
|
|
1363
|
+
op: "curve",
|
|
1364
|
+
x1: c1x,
|
|
1365
|
+
y1: c1y,
|
|
1366
|
+
x2: c2x,
|
|
1367
|
+
y2: c2y,
|
|
1368
|
+
x3: x,
|
|
1369
|
+
y3: y
|
|
1370
|
+
});
|
|
1371
|
+
lastCpX = qx;
|
|
1372
|
+
lastCpY = qy;
|
|
1373
|
+
cx = x;
|
|
1374
|
+
cy = y;
|
|
1375
|
+
lastCmd = "t";
|
|
1376
|
+
} while (isNum());
|
|
1377
|
+
break;
|
|
1378
|
+
case "A":
|
|
1379
|
+
case "a": {
|
|
1380
|
+
const isRel = cmd === "a";
|
|
1381
|
+
do {
|
|
1382
|
+
const rx = Math.abs(num()), ry = Math.abs(num());
|
|
1383
|
+
const rotation = (num() * Math.PI) / 180;
|
|
1384
|
+
const largeArc = num() !== 0;
|
|
1385
|
+
const sweep = num() !== 0;
|
|
1386
|
+
const ex = isRel ? cx + num() : num();
|
|
1387
|
+
const ey = isRel ? cy + num() : num();
|
|
1388
|
+
arcToCurves(ops, cx, cy, rx, ry, rotation, largeArc, sweep, ex, ey);
|
|
1389
|
+
cx = ex;
|
|
1390
|
+
cy = ey;
|
|
1391
|
+
} while (isNum());
|
|
1392
|
+
lastCmd = cmd;
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
case "Z":
|
|
1396
|
+
case "z":
|
|
1397
|
+
ops.push({ op: "close" });
|
|
1398
|
+
cx = sx;
|
|
1399
|
+
cy = sy;
|
|
1400
|
+
lastCmd = cmd;
|
|
1401
|
+
break;
|
|
1402
|
+
default:
|
|
1403
|
+
// Unknown command — skip
|
|
1404
|
+
i++;
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return ops;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Convert an SVG elliptical arc to cubic Bézier curves.
|
|
1412
|
+
* Follows the SVG spec's endpoint-to-center arc parameterization.
|
|
1413
|
+
* @internal
|
|
1414
|
+
*/
|
|
1415
|
+
function arcToCurves(ops, x1, y1, rx, ry, phi, largeArc, sweep, x2, y2) {
|
|
1416
|
+
if (rx === 0 || ry === 0) {
|
|
1417
|
+
ops.push({ op: "line", x: x2, y: y2 });
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (x1 === x2 && y1 === y2) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const cosPhi = Math.cos(phi), sinPhi = Math.sin(phi);
|
|
1424
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
1425
|
+
const x1p = cosPhi * dx + sinPhi * dy;
|
|
1426
|
+
const y1p = -sinPhi * dx + cosPhi * dy;
|
|
1427
|
+
// Correct radii
|
|
1428
|
+
let rxSq = rx * rx, rySq = ry * ry;
|
|
1429
|
+
const x1pSq = x1p * x1p, y1pSq = y1p * y1p;
|
|
1430
|
+
const lambda = x1pSq / rxSq + y1pSq / rySq;
|
|
1431
|
+
if (lambda > 1) {
|
|
1432
|
+
const s = Math.sqrt(lambda);
|
|
1433
|
+
rx *= s;
|
|
1434
|
+
ry *= s;
|
|
1435
|
+
rxSq = rx * rx;
|
|
1436
|
+
rySq = ry * ry;
|
|
1437
|
+
}
|
|
1438
|
+
// Center parameterization
|
|
1439
|
+
let sq = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq);
|
|
1440
|
+
if (sq < 0) {
|
|
1441
|
+
sq = 0;
|
|
1442
|
+
}
|
|
1443
|
+
let root = Math.sqrt(sq);
|
|
1444
|
+
if (largeArc === sweep) {
|
|
1445
|
+
root = -root;
|
|
1446
|
+
}
|
|
1447
|
+
const cxp = (root * rx * y1p) / ry;
|
|
1448
|
+
const cyp = (-root * ry * x1p) / rx;
|
|
1449
|
+
const cxr = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
|
|
1450
|
+
const cyr = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
|
|
1451
|
+
const angle = (ux, uy, vx, vy) => {
|
|
1452
|
+
const dot = ux * vx + uy * vy;
|
|
1453
|
+
const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
|
1454
|
+
let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
|
|
1455
|
+
if (ux * vy - uy * vx < 0) {
|
|
1456
|
+
a = -a;
|
|
1457
|
+
}
|
|
1458
|
+
return a;
|
|
1459
|
+
};
|
|
1460
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
1461
|
+
let dTheta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
1462
|
+
if (!sweep && dTheta > 0) {
|
|
1463
|
+
dTheta -= 2 * Math.PI;
|
|
1464
|
+
}
|
|
1465
|
+
if (sweep && dTheta < 0) {
|
|
1466
|
+
dTheta += 2 * Math.PI;
|
|
1467
|
+
}
|
|
1468
|
+
// Split into segments of at most π/2
|
|
1469
|
+
const segments = Math.ceil(Math.abs(dTheta) / (Math.PI / 2));
|
|
1470
|
+
const segAngle = dTheta / segments;
|
|
1471
|
+
for (let s = 0; s < segments; s++) {
|
|
1472
|
+
const t1 = theta1 + s * segAngle;
|
|
1473
|
+
const t2 = theta1 + (s + 1) * segAngle;
|
|
1474
|
+
const alpha = (4 * Math.tan((t2 - t1) / 4)) / 3;
|
|
1475
|
+
const cos1 = Math.cos(t1), sin1 = Math.sin(t1);
|
|
1476
|
+
const cos2 = Math.cos(t2), sin2 = Math.sin(t2);
|
|
1477
|
+
const ep1x = rx * cos1, ep1y = ry * sin1;
|
|
1478
|
+
const ep2x = rx * cos2, ep2y = ry * sin2;
|
|
1479
|
+
const cp1x = ep1x - alpha * rx * sin1;
|
|
1480
|
+
const cp1y = ep1y + alpha * ry * cos1;
|
|
1481
|
+
const cp2x = ep2x + alpha * rx * sin2;
|
|
1482
|
+
const cp2y = ep2y - alpha * ry * cos2;
|
|
1483
|
+
ops.push({
|
|
1484
|
+
op: "curve",
|
|
1485
|
+
x1: cosPhi * cp1x - sinPhi * cp1y + cxr,
|
|
1486
|
+
y1: sinPhi * cp1x + cosPhi * cp1y + cyr,
|
|
1487
|
+
x2: cosPhi * cp2x - sinPhi * cp2y + cxr,
|
|
1488
|
+
y2: sinPhi * cp2x + cosPhi * cp2y + cyr,
|
|
1489
|
+
x3: cosPhi * ep2x - sinPhi * ep2y + cxr,
|
|
1490
|
+
y3: sinPhi * ep2x + cosPhi * ep2y + cyr
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|