@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,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
+ }