@cj-tech-master/excelts 9.1.0 → 9.2.0

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