wand-kit 0.3.2 → 0.5.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 (162) hide show
  1. package/PROMPT.md +221 -19
  2. package/SKILL.md +100 -20
  3. package/START-HERE.md +12 -1
  4. package/brands/current/tokens.json +10 -2
  5. package/brands/interim/assets/avatar.svg +1 -0
  6. package/brands/interim/assets/closing-art.png +0 -0
  7. package/brands/interim/assets/cover-art.png +0 -0
  8. package/brands/interim/assets/cover-portrait-art.png +0 -0
  9. package/brands/interim/assets/divider-art.png +0 -0
  10. package/brands/interim/assets/glyphs/README.md +29 -0
  11. package/brands/interim/assets/glyphs/activity.svg +15 -0
  12. package/brands/interim/assets/glyphs/atom.svg +17 -0
  13. package/brands/interim/assets/glyphs/badge-check.svg +16 -0
  14. package/brands/interim/assets/glyphs/bell.svg +16 -0
  15. package/brands/interim/assets/glyphs/blocks.svg +16 -0
  16. package/brands/interim/assets/glyphs/book-open.svg +16 -0
  17. package/brands/interim/assets/glyphs/bot.svg +20 -0
  18. package/brands/interim/assets/glyphs/boxes.svg +26 -0
  19. package/brands/interim/assets/glyphs/brain.svg +23 -0
  20. package/brands/interim/assets/glyphs/briefcase.svg +16 -0
  21. package/brands/interim/assets/glyphs/building-2.svg +21 -0
  22. package/brands/interim/assets/glyphs/calendar.svg +18 -0
  23. package/brands/interim/assets/glyphs/chart-column.svg +18 -0
  24. package/brands/interim/assets/glyphs/chart-line.svg +16 -0
  25. package/brands/interim/assets/glyphs/chart-pie.svg +16 -0
  26. package/brands/interim/assets/glyphs/circle-check.svg +16 -0
  27. package/brands/interim/assets/glyphs/clipboard-check.svg +17 -0
  28. package/brands/interim/assets/glyphs/clock.svg +16 -0
  29. package/brands/interim/assets/glyphs/cloud.svg +15 -0
  30. package/brands/interim/assets/glyphs/code.svg +15 -0
  31. package/brands/interim/assets/glyphs/coins.svg +18 -0
  32. package/brands/interim/assets/glyphs/compass.svg +16 -0
  33. package/brands/interim/assets/glyphs/cpu.svg +24 -0
  34. package/brands/interim/assets/glyphs/database.svg +17 -0
  35. package/brands/interim/assets/glyphs/eye.svg +16 -0
  36. package/brands/interim/assets/glyphs/file-check.svg +17 -0
  37. package/brands/interim/assets/glyphs/filter.svg +15 -0
  38. package/brands/interim/assets/glyphs/fingerprint.svg +23 -0
  39. package/brands/interim/assets/glyphs/flag.svg +16 -0
  40. package/brands/interim/assets/glyphs/gauge.svg +16 -0
  41. package/brands/interim/assets/glyphs/gavel.svg +19 -0
  42. package/brands/interim/assets/glyphs/git-branch.svg +18 -0
  43. package/brands/interim/assets/glyphs/globe.svg +17 -0
  44. package/brands/interim/assets/glyphs/graduation-cap.svg +17 -0
  45. package/brands/interim/assets/glyphs/handshake.svg +19 -0
  46. package/brands/interim/assets/glyphs/hexagon.svg +15 -0
  47. package/brands/interim/assets/glyphs/index.json +177 -0
  48. package/brands/interim/assets/glyphs/infinity.svg +15 -0
  49. package/brands/interim/assets/glyphs/key.svg +17 -0
  50. package/brands/interim/assets/glyphs/landmark.svg +20 -0
  51. package/brands/interim/assets/glyphs/layers.svg +17 -0
  52. package/brands/interim/assets/glyphs/lightbulb.svg +17 -0
  53. package/brands/interim/assets/glyphs/link.svg +16 -0
  54. package/brands/interim/assets/glyphs/list-checks.svg +19 -0
  55. package/brands/interim/assets/glyphs/lock.svg +16 -0
  56. package/brands/interim/assets/glyphs/map.svg +17 -0
  57. package/brands/interim/assets/glyphs/megaphone.svg +16 -0
  58. package/brands/interim/assets/glyphs/message-square.svg +15 -0
  59. package/brands/interim/assets/glyphs/milestone.svg +17 -0
  60. package/brands/interim/assets/glyphs/network.svg +19 -0
  61. package/brands/interim/assets/glyphs/orbit.svg +19 -0
  62. package/brands/interim/assets/glyphs/package.svg +18 -0
  63. package/brands/interim/assets/glyphs/plug.svg +18 -0
  64. package/brands/interim/assets/glyphs/puzzle.svg +15 -0
  65. package/brands/interim/assets/glyphs/radar.svg +22 -0
  66. package/brands/interim/assets/glyphs/receipt.svg +17 -0
  67. package/brands/interim/assets/glyphs/refresh-cw.svg +18 -0
  68. package/brands/interim/assets/glyphs/rocket.svg +18 -0
  69. package/brands/interim/assets/glyphs/route.svg +17 -0
  70. package/brands/interim/assets/glyphs/scale.svg +19 -0
  71. package/brands/interim/assets/glyphs/search.svg +16 -0
  72. package/brands/interim/assets/glyphs/server.svg +18 -0
  73. package/brands/interim/assets/glyphs/settings.svg +16 -0
  74. package/brands/interim/assets/glyphs/share-2.svg +19 -0
  75. package/brands/interim/assets/glyphs/shield-check.svg +16 -0
  76. package/brands/interim/assets/glyphs/shuffle.svg +19 -0
  77. package/brands/interim/assets/glyphs/sparkles.svg +19 -0
  78. package/brands/interim/assets/glyphs/square-stack.svg +17 -0
  79. package/brands/interim/assets/glyphs/target.svg +17 -0
  80. package/brands/interim/assets/glyphs/trending-up.svg +16 -0
  81. package/brands/interim/assets/glyphs/triangle-alert.svg +17 -0
  82. package/brands/interim/assets/glyphs/trophy.svg +20 -0
  83. package/brands/interim/assets/glyphs/users.svg +18 -0
  84. package/brands/interim/assets/glyphs/wallet.svg +16 -0
  85. package/brands/interim/assets/glyphs/wand-sparkles.svg +22 -0
  86. package/brands/interim/assets/glyphs/workflow.svg +17 -0
  87. package/brands/interim/assets/glyphs/zap.svg +15 -0
  88. package/brands/interim/assets/mark.svg +1 -0
  89. package/brands/interim/assets/social/composition.json +6833 -0
  90. package/brands/interim/assets/spark-lavender.png +0 -0
  91. package/brands/interim/assets/spark-violet.png +0 -0
  92. package/brands/interim/assets/statement-art.png +0 -0
  93. package/brands/interim/assets/story/agent-alpha.png +0 -0
  94. package/brands/interim/assets/story/agent.png +0 -0
  95. package/brands/interim/assets/story/economy-alpha.png +0 -0
  96. package/brands/interim/assets/story/economy.png +0 -0
  97. package/brands/interim/assets/story/index.json +214 -0
  98. package/brands/interim/assets/story/learning-alpha.png +0 -0
  99. package/brands/interim/assets/story/learning.png +0 -0
  100. package/brands/interim/assets/story/organization-alpha.png +0 -0
  101. package/brands/interim/assets/story/organization.png +0 -0
  102. package/brands/interim/assets/story/worker-alpha.png +0 -0
  103. package/brands/interim/assets/story/worker-light.png +0 -0
  104. package/brands/interim/assets/story/worker.png +0 -0
  105. package/brands/interim/assets/wordmark-on-dark.png +0 -0
  106. package/brands/interim/assets/wordmark-on-dark.svg +1 -0
  107. package/brands/interim/assets/wordmark-on-light.png +0 -0
  108. package/brands/interim/assets/wordmark-on-light.svg +1 -0
  109. package/brands/interim/fonts/Geist-Book.ttf +0 -0
  110. package/brands/interim/fonts/Geist-Medium.ttf +0 -0
  111. package/brands/interim/fonts/Geist-Regular.ttf +0 -0
  112. package/brands/interim/fonts/Geist-SemiBold.ttf +0 -0
  113. package/brands/interim/fonts/Newsreader-Italic.ttf +0 -0
  114. package/brands/interim/fonts/Newsreader-Medium.ttf +0 -0
  115. package/brands/interim/fonts/Newsreader-MediumItalic.ttf +0 -0
  116. package/brands/interim/fonts/Newsreader-Regular.ttf +0 -0
  117. package/brands/interim/fonts/OFL-Geist.txt +93 -0
  118. package/brands/interim/fonts/OFL-Newsreader.txt +93 -0
  119. package/brands/interim/fonts/faces.json +12 -0
  120. package/brands/interim/lib/composition.js +169 -0
  121. package/brands/interim/lib/composition.mjs +101 -0
  122. package/brands/interim/lib/glyphs.js +89 -0
  123. package/brands/interim/lib/measure.js +121 -0
  124. package/brands/interim/lib/pdf.js +597 -0
  125. package/brands/interim/lib/ttf.py +169 -0
  126. package/brands/interim/lib/wandxml.py +37 -0
  127. package/brands/interim/tokens.json +299 -0
  128. package/brands/next/tokens.json +14 -2
  129. package/examples/social/social.json +23 -0
  130. package/lib/audit.js +142 -0
  131. package/lib/brand.js +12 -7
  132. package/lib/components.js +42 -10
  133. package/lib/engine.js +72 -9
  134. package/lib/library.js +180 -0
  135. package/lib/pptx.js +1 -1
  136. package/lib/social.js +366 -0
  137. package/lib/talk.js +37 -4
  138. package/library/index.json +629 -0
  139. package/library/interim/avatar-800x800.png +0 -0
  140. package/library/interim/icons/apple-touch-icon.png +0 -0
  141. package/library/interim/icons/favicon-16.png +0 -0
  142. package/library/interim/icons/favicon-32.png +0 -0
  143. package/library/interim/icons/favicon-48.png +0 -0
  144. package/library/interim/icons/favicon.ico +0 -0
  145. package/library/interim/icons/favicon.svg +1 -0
  146. package/library/interim/icons/icon-192.png +0 -0
  147. package/library/interim/icons/icon-512.png +0 -0
  148. package/library/interim/icons/icon-tags.html +5 -0
  149. package/library/interim/icons/site.webmanifest +19 -0
  150. package/library/interim/linkedin-banner-1584x396.png +0 -0
  151. package/library/interim/linkedin-company-1128x191.png +0 -0
  152. package/library/interim/open-agent-discovery-share-1200x630.png +0 -0
  153. package/library/interim/open-agent-discovery-share-head-tags.html +17 -0
  154. package/library/interim/wand-ai-share-1200x630.png +0 -0
  155. package/library/interim/wand-ai-share-head-tags.html +17 -0
  156. package/library/interim/x-header-1500x500.png +0 -0
  157. package/package.json +2 -2
  158. package/templates/card.mjs +6 -2
  159. package/templates/page.mjs +1 -1
  160. package/templates/slide.mjs +1 -1
  161. package/templates/storyboard.mjs +1 -1
  162. package/wand.js +65 -6
@@ -0,0 +1,597 @@
1
+ /**
2
+ * lib/pdf.js — a PDF writer with exactly the parts a Wand document needs.
3
+ *
4
+ * Why write one rather than reach for a library: the whole system's promise is
5
+ * that a kit unzips and runs with nothing installed. A PDF renderer that pulls
6
+ * in a native rasteriser breaks that on the first machine without build tools.
7
+ * The subset here — text in embedded TrueType, filled and rounded rectangles,
8
+ * lines, and images — is the whole of these documents.
9
+ *
10
+ * Fonts go in as CIDFontType2 with Identity-H encoding: fontkit gives the glyph
11
+ * ids and advances, so text positions the same way the deck engine measures it.
12
+ * A ToUnicode CMap goes in beside every font, which is not decoration — it is
13
+ * what makes `pdftotext` able to read the output back, and that is the coverage
14
+ * gate.
15
+ *
16
+ * Coordinates are top-left and in inches, matching the deck kit, and are
17
+ * flipped to PDF's bottom-left points on the way out. Layout code should never
18
+ * have to think in two coordinate systems.
19
+ */
20
+
21
+ const zlib = require("zlib");
22
+ const fs = require("fs");
23
+ const path = require("path");
24
+
25
+ const PT = 72;
26
+ const esc = (s) => s.replace(/([\\()])/g, "\\$1");
27
+ const num = (n) => (Math.round(n * 1000) / 1000).toString();
28
+
29
+ /** #RRGGBB -> "r g b" in PDF's 0–1 space. */
30
+ function rgb(hex) {
31
+ const h = String(hex).replace(/^#/, "");
32
+ const n = parseInt(h, 16);
33
+ return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255].map(num).join(" ");
34
+ }
35
+
36
+ class Doc {
37
+ /** @param {{width:number,height:number,fontkit:any,fontsDir:string}} o inches */
38
+ constructor({ width = 8.5, height = 11, fontkit, fontsDir }) {
39
+ this.W = width; this.H = height;
40
+ this.fontkit = fontkit; this.fontsDir = fontsDir;
41
+ this.pages = [];
42
+ this.fonts = new Map(); // stem -> { font, used:Map<gid,width>, ref }
43
+ this.images = new Map(); // absolute path -> { ref, w, h, ... }
44
+ this.alphas = new Map(); // opacity -> ExtGState name
45
+ this.page = null;
46
+ }
47
+
48
+ addPage() {
49
+ this.page = { ops: [], fonts: new Set(), images: new Set(), alphas: new Set() };
50
+ this.pages.push(this.page);
51
+ return this.page;
52
+ }
53
+
54
+ /* ------------------------------------------------------------ geometry */
55
+
56
+ /** y is measured from the top of the page, like every other kit here. */
57
+ _y(y) { return (this.H - y) * PT; }
58
+
59
+ /** A named ExtGState for constant fill opacity, 0–1. */
60
+ _alpha(a) {
61
+ const key = Math.round(a * 100) / 100;
62
+ if (!this.alphas.has(key)) this.alphas.set(key, "GS" + this.alphas.size);
63
+ const name = this.alphas.get(key);
64
+ this.page.alphas.add(key);
65
+ return name;
66
+ }
67
+
68
+ rect({ x, y, w, h, fill, stroke, lineWidth = 0.75, radius = 0, dash, opacity }) {
69
+ const o = this.page.ops;
70
+ const soft = opacity != null && opacity < 1;
71
+ if (soft) o.push("q", `/${this._alpha(opacity)} gs`);
72
+ if (fill) o.push(`${rgb(fill)} rg`);
73
+ if (stroke) o.push(`${rgb(stroke)} RG`, `${num(lineWidth)} w`);
74
+ if (dash) o.push(`[${num(dash)} ${num(dash)}] 0 d`);
75
+ const X = x * PT, Y = this._y(y + h), Wd = w * PT, Ht = h * PT;
76
+ const r = Math.min(radius * PT, Wd / 2, Ht / 2);
77
+ if (r <= 0) {
78
+ o.push(`${num(X)} ${num(Y)} ${num(Wd)} ${num(Ht)} re`);
79
+ } else {
80
+ const k = r * 0.5523;
81
+ o.push(
82
+ `${num(X + r)} ${num(Y)} m`,
83
+ `${num(X + Wd - r)} ${num(Y)} l`,
84
+ `${num(X + Wd - r + k)} ${num(Y)} ${num(X + Wd)} ${num(Y + r - k)} ${num(X + Wd)} ${num(Y + r)} c`,
85
+ `${num(X + Wd)} ${num(Y + Ht - r)} l`,
86
+ `${num(X + Wd)} ${num(Y + Ht - r + k)} ${num(X + Wd - r + k)} ${num(Y + Ht)} ${num(X + Wd - r)} ${num(Y + Ht)} c`,
87
+ `${num(X + r)} ${num(Y + Ht)} l`,
88
+ `${num(X + r - k)} ${num(Y + Ht)} ${num(X)} ${num(Y + Ht - r + k)} ${num(X)} ${num(Y + Ht - r)} c`,
89
+ `${num(X)} ${num(Y + r)} l`,
90
+ `${num(X)} ${num(Y + r - k)} ${num(X + r - k)} ${num(Y)} ${num(X + r)} ${num(Y)} c`,
91
+ "h");
92
+ }
93
+ o.push(fill && stroke ? "B" : stroke ? "S" : "f");
94
+ if (dash) o.push("[] 0 d");
95
+ if (soft) o.push("Q");
96
+ return this;
97
+ }
98
+
99
+ /** An ellipse, drawn as four beziers. The badge circles need it. */
100
+ ellipse({ x, y, w, h, fill, stroke, lineWidth = 0.75 }) {
101
+ const o = this.page.ops;
102
+ if (fill) o.push(`${rgb(fill)} rg`);
103
+ if (stroke) o.push(`${rgb(stroke)} RG`, `${num(lineWidth)} w`);
104
+ const cx = (x + w / 2) * PT, cy = this._y(y + h / 2);
105
+ const rx = (w / 2) * PT, ry = (h / 2) * PT, k = 0.5523;
106
+ o.push(`${num(cx + rx)} ${num(cy)} m`,
107
+ `${num(cx + rx)} ${num(cy + ry * k)} ${num(cx + rx * k)} ${num(cy + ry)} ${num(cx)} ${num(cy + ry)} c`,
108
+ `${num(cx - rx * k)} ${num(cy + ry)} ${num(cx - rx)} ${num(cy + ry * k)} ${num(cx - rx)} ${num(cy)} c`,
109
+ `${num(cx - rx)} ${num(cy - ry * k)} ${num(cx - rx * k)} ${num(cy - ry)} ${num(cx)} ${num(cy - ry)} c`,
110
+ `${num(cx + rx * k)} ${num(cy - ry)} ${num(cx + rx)} ${num(cy - ry * k)} ${num(cx + rx)} ${num(cy)} c`,
111
+ "h", fill && stroke ? "B" : stroke ? "S" : "f");
112
+ return this;
113
+ }
114
+
115
+ /**
116
+ * A filled triangle in its box, pointing up — or right when rotated 90°, the
117
+ * way a flow connector is drawn. Matches pptxgenjs's "triangle" + rotate.
118
+ */
119
+ triangle({ x, y, w, h, fill, rotate = 0 }) {
120
+ const o = this.page.ops;
121
+ const pts = rotate === 90
122
+ ? [[x, y], [x + w, y + h / 2], [x, y + h]]
123
+ : [[x, y + h], [x + w / 2, y], [x + w, y + h]];
124
+ o.push(`${rgb(fill || "000000")} rg`,
125
+ pts.map(([px, py], i) => `${num(px * PT)} ${num(this._y(py))} ${i ? "l" : "m"}`).join(" ") + " h f");
126
+ return this;
127
+ }
128
+
129
+ /**
130
+ * A path of lines and cubic / quadratic curves, as pptxgenjs's custGeom
131
+ * writes it: points in inches relative to (x, y). Stroked, filled or both.
132
+ */
133
+ path({ x, y, points, stroke, width = 1, fill, opacity, dash }) {
134
+ const o = this.page.ops;
135
+ const soft = opacity != null && opacity < 1;
136
+ o.push("q");
137
+ if (soft) o.push(`/${this._alpha(opacity)} gs`);
138
+ if (fill) o.push(`${rgb(fill)} rg`);
139
+ if (stroke) o.push(`${rgb(stroke)} RG`, `${num(width)} w`, "1 J", "1 j");
140
+ if (dash) o.push(`[${num(dash)} ${num(dash * 1.4)}] 0 d`);
141
+ const P = (px, py) => `${num((x + px) * PT)} ${num(this._y(y + py))}`;
142
+ const seg = [];
143
+ points.forEach((p, i) => {
144
+ if (p.close) { seg.push("h"); return; }
145
+ if (p.curve && p.curve.type === "cubic") seg.push(`${P(p.curve.x1, p.curve.y1)} ${P(p.curve.x2, p.curve.y2)} ${P(p.x, p.y)} c`);
146
+ else if (p.curve && p.curve.type === "quadratic") {
147
+ // a quadratic as the cubic PDF draws: control points two thirds of the way
148
+ const prev = points[i - 1] || p;
149
+ const c1x = prev.x + (2 / 3) * (p.curve.x1 - prev.x), c1y = prev.y + (2 / 3) * (p.curve.y1 - prev.y);
150
+ const c2x = p.x + (2 / 3) * (p.curve.x1 - p.x), c2y = p.y + (2 / 3) * (p.curve.y1 - p.y);
151
+ seg.push(`${P(c1x, c1y)} ${P(c2x, c2y)} ${P(p.x, p.y)} c`);
152
+ }
153
+ else seg.push(`${P(p.x, p.y)} ${i === 0 || p.moveTo ? "m" : "l"}`);
154
+ });
155
+ o.push(seg.join(" "), fill && stroke ? "B" : fill ? "f" : "S", "Q");
156
+ return this;
157
+ }
158
+
159
+ /** A filled polygon, optionally translucent — an area chart's fill. */
160
+ poly({ points, fill, opacity }) {
161
+ const o = this.page.ops;
162
+ const soft = opacity != null && opacity < 1;
163
+ if (soft) o.push("q", `/${this._alpha(opacity)} gs`);
164
+ o.push(`${rgb(fill || "000000")} rg`,
165
+ points.map(([px, py], i) => `${num(px * PT)} ${num(this._y(py))} ${i ? "l" : "m"}`).join(" ") + " h f");
166
+ if (soft) o.push("Q");
167
+ return this;
168
+ }
169
+
170
+ /** Width of a string in inches, for centring a label without drawing it. */
171
+ measure(str, { stem, size }) {
172
+ const F = this._font(stem);
173
+ return (F.font.layout(String(str)).advanceWidth / F.font.unitsPerEm) * size / 72;
174
+ }
175
+
176
+ line({ x1, y1, x2, y2, color = "000000", width = 0.75 }) {
177
+ this.page.ops.push(`${rgb(color)} RG`, `${num(width)} w`,
178
+ `${num(x1 * PT)} ${num(this._y(y1))} m ${num(x2 * PT)} ${num(this._y(y2))} l S`);
179
+ return this;
180
+ }
181
+
182
+ /* ---------------------------------------------------------------- text */
183
+
184
+ _font(stem) {
185
+ if (!this.fonts.has(stem)) {
186
+ const file = path.join(this.fontsDir, stem + ".ttf");
187
+ if (!fs.existsSync(file)) throw new Error(`no font ${stem}.ttf in ${this.fontsDir}`);
188
+ this.fonts.set(stem, { stem, font: this.fontkit.openSync(file), file, used: new Map(), uni: new Map() });
189
+ }
190
+ return this.fonts.get(stem);
191
+ }
192
+
193
+ /**
194
+ * One line of text at (x, y) where y is the BASELINE, from the page top.
195
+ * Returns the advance width in inches.
196
+ */
197
+ text(str, { x, y, stem, size, color = "000000", tracking = 0 }) {
198
+ if (str == null || str === "") return 0;
199
+ const F = this._font(stem);
200
+ const run = F.font.layout(String(str));
201
+ const upm = F.font.unitsPerEm;
202
+ // Kerning goes in as TJ adjustments. A bare Tj draws every glyph at its
203
+ // nominal advance, while measure.js (and PowerPoint) kern — so a serif
204
+ // heading drew wider than it measured, and the space before a gold phrase
205
+ // disappeared under the drift.
206
+ const parts = [];
207
+ let hex = "", adv = 0;
208
+ run.glyphs.forEach((g, i) => {
209
+ F.used.set(g.id, g.advanceWidth);
210
+ if (g.codePoints && g.codePoints.length) F.uni.set(g.id, g.codePoints);
211
+ hex += g.id.toString(16).padStart(4, "0");
212
+ const kern = run.positions[i].xAdvance - g.advanceWidth;
213
+ if (kern) { parts.push(`<${hex}>`, num(-kern * 1000 / upm)); hex = ""; }
214
+ adv += run.positions[i].xAdvance;
215
+ });
216
+ if (hex) parts.push(`<${hex}>`);
217
+ this.page.fonts.add(stem);
218
+ const o = this.page.ops;
219
+ o.push("BT", `${rgb(color)} rg`, `/F_${stem.replace(/[^A-Za-z0-9]/g, "")} ${num(size)} Tf`);
220
+ if (tracking) o.push(`${num(tracking)} Tc`);
221
+ o.push(`${num(x * PT)} ${num(this._y(y))} Td`, `[${parts.join(" ")}] TJ`);
222
+ if (tracking) o.push("0 Tc");
223
+ o.push("ET");
224
+ return (adv / upm) * (size / PT) + (tracking / PT) * run.glyphs.length;
225
+ }
226
+
227
+ /* ---------------------------------------------------------------- svg */
228
+
229
+ /**
230
+ * Draw a Lucide-style stroked SVG at (x, y) in a `size` box.
231
+ *
232
+ * These are vector paths, so they go into the content stream as path
233
+ * operators rather than as an image — which is why an icon in a Wand document
234
+ * is crisp at any zoom and takes about 300 bytes. 58 of the 75 glyphs use
235
+ * elliptical arcs, so arc support is not optional; `arcTo` converts them to
236
+ * the beziers PDF understands.
237
+ */
238
+ svg(svgText, { x, y, size, color = "000000", strokeWidth = 2 }) {
239
+ const o = this.page.ops;
240
+ const s = (size * PT) / 24; // the viewBox is always 0 0 24 24
241
+ o.push("q", `${rgb(color)} RG`, `${num(strokeWidth)} w`, "1 J", "1 j",
242
+ `${num(s)} 0 0 ${num(-s)} ${num(x * PT)} ${num(this._y(y))} cm`);
243
+ for (const d of svgToPaths(svgText)) emitPath(o, d);
244
+ o.push("S", "Q");
245
+ return this;
246
+ }
247
+
248
+ /* -------------------------------------------------------------- images */
249
+
250
+ /**
251
+ * `cover` crops the image to fill the box at its own aspect, centred — what
252
+ * pptxgenjs writes as `sizing: { type: "cover" }`. Without it a photograph
253
+ * set into a half-slide panel is squashed here and cropped in PowerPoint.
254
+ */
255
+ image(file, { x, y, w, h, cover = false }) {
256
+ const abs = path.resolve(file);
257
+ if (!this.images.has(abs)) this.images.set(abs, decodeImage(abs));
258
+ const im = this.images.get(abs);
259
+ const name = "Im" + [...this.images.keys()].indexOf(abs);
260
+ im.name = name;
261
+ this.page.images.add(abs);
262
+ let dx = x, dy = y, dw = w, dh = h;
263
+ const o = this.page.ops;
264
+ o.push("q");
265
+ if (cover && im.w && im.h) {
266
+ const s = Math.max(w / im.w, h / im.h);
267
+ dw = im.w * s; dh = im.h * s; dx = x + (w - dw) / 2; dy = y + (h - dh) / 2;
268
+ o.push(`${num(x * PT)} ${num(this._y(y + h))} ${num(w * PT)} ${num(h * PT)} re W n`);
269
+ }
270
+ o.push(`${num(dw * PT)} 0 0 ${num(dh * PT)} ${num(dx * PT)} ${num(this._y(dy + dh))} cm`, `/${name} Do`, "Q");
271
+ return this;
272
+ }
273
+
274
+ /* --------------------------------------------------------------- write */
275
+
276
+ toBuffer() {
277
+ const objs = []; // 1-indexed on output
278
+ const ref = (n) => `${n} 0 R`;
279
+ const add = (body) => { objs.push(body); return objs.length; };
280
+
281
+ // fonts
282
+ const fontRefs = new Map();
283
+ for (const F of this.fonts.values()) {
284
+ const raw = fs.readFileSync(F.file);
285
+ const upm = F.font.unitsPerEm;
286
+ const sc = 1000 / upm;
287
+ const fileRef = add({ stream: zlib.deflateSync(raw),
288
+ dict: `/Filter /FlateDecode /Length1 ${raw.length}` });
289
+ const flags = 4 | (F.font.italicAngle ? 64 : 32);
290
+ const bb = F.font.bbox;
291
+ const desc = add(`<< /Type /FontDescriptor /FontName /${psName(F)} /Flags ${flags}` +
292
+ ` /FontBBox [${Math.round(bb.minX * sc)} ${Math.round(bb.minY * sc)} ${Math.round(bb.maxX * sc)} ${Math.round(bb.maxY * sc)}]` +
293
+ ` /ItalicAngle ${Math.round(F.font.italicAngle || 0)} /Ascent ${Math.round(F.font.ascent * sc)}` +
294
+ ` /Descent ${Math.round(F.font.descent * sc)} /CapHeight ${Math.round((F.font.capHeight || 700) * sc)}` +
295
+ ` /StemV 80 /FontFile2 ${ref(fileRef)} >>`);
296
+ const gids = [...F.used.keys()].sort((a, b) => a - b);
297
+ const W = gids.map((g) => `${g} [${Math.round(F.used.get(g) * sc)}]`).join(" ");
298
+ const cid = add(`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${psName(F)}` +
299
+ ` /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>` +
300
+ ` /FontDescriptor ${ref(desc)} /DW 1000 /W [${W}] /CIDToGIDMap /Identity >>`);
301
+ const tu = add({ stream: zlib.deflateSync(Buffer.from(toUnicode(F), "latin1")),
302
+ dict: "/Filter /FlateDecode" });
303
+ const f0 = add(`<< /Type /Font /Subtype /Type0 /BaseFont /${psName(F)}` +
304
+ ` /Encoding /Identity-H /DescendantFonts [${ref(cid)}] /ToUnicode ${ref(tu)} >>`);
305
+ fontRefs.set(F.stem, f0);
306
+ }
307
+
308
+ // images
309
+ const imageRefs = new Map();
310
+ for (const [abs, im] of this.images) {
311
+ let smask = null;
312
+ if (im.alpha) {
313
+ smask = add({ stream: zlib.deflateSync(im.alpha),
314
+ dict: `/Type /XObject /Subtype /Image /Width ${im.w} /Height ${im.h}` +
315
+ ` /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode` });
316
+ }
317
+ const r = add({ stream: im.filter === "DCTDecode" ? im.data : zlib.deflateSync(im.data),
318
+ dict: `/Type /XObject /Subtype /Image /Width ${im.w} /Height ${im.h}` +
319
+ ` /ColorSpace /DeviceRGB /BitsPerComponent 8` +
320
+ ` /Filter /${im.filter === "DCTDecode" ? "DCTDecode" : "FlateDecode"}` +
321
+ (smask ? ` /SMask ${ref(smask)}` : "") });
322
+ imageRefs.set(abs, r);
323
+ }
324
+
325
+ // pages
326
+ const pagesRef = objs.length + 1 + this.pages.length * 2 + 1; // filled below
327
+ const kids = [];
328
+ for (const pg of this.pages) {
329
+ const content = add({ stream: zlib.deflateSync(Buffer.from(pg.ops.join("\n"), "latin1")),
330
+ dict: "/Filter /FlateDecode" });
331
+ const fRes = [...pg.fonts].map((s) => `/F_${s.replace(/[^A-Za-z0-9]/g, "")} ${ref(fontRefs.get(s))}`).join(" ");
332
+ const xRes = [...pg.images].map((a) => `/${this.images.get(a).name} ${ref(imageRefs.get(a))}`).join(" ");
333
+ const gRes = [...pg.alphas].map((a) => `/${this.alphas.get(a)} << /ca ${a} /CA 1 >>`).join(" ");
334
+ const p = add(`<< /Type /Page /Parent __PAGES__ /MediaBox [0 0 ${num(this.W * PT)} ${num(this.H * PT)}]` +
335
+ ` /Resources << /Font << ${fRes} >> /XObject << ${xRes} >>` +
336
+ (gRes ? ` /ExtGState << ${gRes} >>` : "") + ` >> /Contents ${ref(content)} >>`);
337
+ kids.push(p);
338
+ }
339
+ const pagesObj = add(`<< /Type /Pages /Kids [${kids.map(ref).join(" ")}] /Count ${kids.length} >>`);
340
+ const catalog = add(`<< /Type /Catalog /Pages ${ref(pagesObj)} >>`);
341
+ const info = add(`<< /Producer (wand-docs) /Creator (wand-docs) >>`);
342
+ for (let i = 0; i < objs.length; i++) {
343
+ if (typeof objs[i] === "string") objs[i] = objs[i].replace("__PAGES__", ref(pagesObj));
344
+ }
345
+
346
+ const chunks = [Buffer.from("%PDF-1.7\n%\xE2\xE3\xCF\xD3\n", "latin1")];
347
+ let pos = chunks[0].length;
348
+ const offsets = [0];
349
+ objs.forEach((o, i) => {
350
+ const head = Buffer.from(`${i + 1} 0 obj\n`, "latin1");
351
+ const body = typeof o === "string"
352
+ ? Buffer.from(o + "\n", "latin1")
353
+ : Buffer.concat([Buffer.from(`<< ${o.dict} /Length ${o.stream.length} >>\nstream\n`, "latin1"),
354
+ o.stream, Buffer.from("\nendstream\n", "latin1")]);
355
+ const tail = Buffer.from("endobj\n", "latin1");
356
+ offsets.push(pos);
357
+ chunks.push(head, body, tail);
358
+ pos += head.length + body.length + tail.length;
359
+ });
360
+ const xref = [`xref\n0 ${objs.length + 1}\n0000000000 65535 f \n`];
361
+ for (let i = 1; i <= objs.length; i++) xref.push(String(offsets[i]).padStart(10, "0") + " 00000 n \n");
362
+ chunks.push(Buffer.from(xref.join(""), "latin1"));
363
+ chunks.push(Buffer.from(`trailer\n<< /Size ${objs.length + 1} /Root ${ref(catalog)} /Info ${ref(info)} >>\n` +
364
+ `startxref\n${pos}\n%%EOF\n`, "latin1"));
365
+ return Buffer.concat(chunks);
366
+ }
367
+ }
368
+
369
+ const psName = (F) => F.stem.replace(/[^A-Za-z0-9]/g, "");
370
+
371
+ /** Without this, the PDF renders correctly and `pdftotext` returns mojibake. */
372
+ function toUnicode(F) {
373
+ const rev = new Map(F.uni); // recorded during layout
374
+ for (const cp of F.font.characterSet || []) { // anything layout never saw
375
+ try {
376
+ const g = F.font.glyphForCodePoint(cp);
377
+ if (g && F.used.has(g.id) && !rev.has(g.id)) rev.set(g.id, [cp]);
378
+ } catch {}
379
+ }
380
+ const utf16be = (cps) => cps.map((cp) => {
381
+ if (cp <= 0xffff) return cp.toString(16).padStart(4, "0").toUpperCase();
382
+ const v = cp - 0x10000;
383
+ return ((0xd800 | (v >> 10)).toString(16) + (0xdc00 | (v & 0x3ff)).toString(16)).toUpperCase();
384
+ }).join("");
385
+ const lines = [...rev.entries()].map(([g, cps]) =>
386
+ `<${g.toString(16).padStart(4, "0")}> <${utf16be(cps)}>`);
387
+ const chunks = [];
388
+ for (let i = 0; i < lines.length; i += 100) {
389
+ const part = lines.slice(i, i + 100);
390
+ chunks.push(`${part.length} beginbfchar\n${part.join("\n")}\nendbfchar`);
391
+ }
392
+ return `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
393
+ `/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n` +
394
+ `/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n` +
395
+ `${chunks.join("\n")}\nendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
396
+ }
397
+
398
+ /* ------------------------------------------------------------------ images */
399
+
400
+ function decodeImage(file) {
401
+ const buf = fs.readFileSync(file);
402
+ if (buf[0] === 0xff && buf[1] === 0xd8) {
403
+ let i = 2, w = 0, h = 0;
404
+ while (i < buf.length) {
405
+ if (buf[i] !== 0xff) { i++; continue; }
406
+ const m = buf[i + 1];
407
+ if (m >= 0xc0 && m <= 0xcf && m !== 0xc4 && m !== 0xc8 && m !== 0xcc) {
408
+ h = buf.readUInt16BE(i + 5); w = buf.readUInt16BE(i + 7); break;
409
+ }
410
+ i += 2 + buf.readUInt16BE(i + 2);
411
+ }
412
+ return { w, h, data: buf, filter: "DCTDecode", alpha: null };
413
+ }
414
+ return decodePNG(buf, file);
415
+ }
416
+
417
+ function decodePNG(buf, file) {
418
+ let pos = 8, idat = [], w = 0, h = 0, bd = 0, ct = 0, plte = null, trns = null;
419
+ while (pos < buf.length) {
420
+ const len = buf.readUInt32BE(pos), type = buf.toString("latin1", pos + 4, pos + 8);
421
+ const data = buf.subarray(pos + 8, pos + 8 + len);
422
+ if (type === "IHDR") { w = data.readUInt32BE(0); h = data.readUInt32BE(4); bd = data[8]; ct = data[9]; }
423
+ else if (type === "IDAT") idat.push(data);
424
+ else if (type === "PLTE") plte = data;
425
+ else if (type === "tRNS") trns = data;
426
+ pos += 12 + len;
427
+ }
428
+ if (bd !== 8) throw new Error(`${path.basename(file)}: only 8-bit PNG is supported`);
429
+ const chan = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[ct];
430
+ if (!chan) throw new Error(`${path.basename(file)}: unsupported PNG colour type ${ct}`);
431
+ const raw = zlib.inflateSync(Buffer.concat(idat));
432
+ const stride = w * chan;
433
+ const out = Buffer.alloc(h * stride);
434
+ let p = 0;
435
+ for (let y = 0; y < h; y++) {
436
+ const f = raw[p++];
437
+ const row = raw.subarray(p, p + stride); p += stride;
438
+ const cur = out.subarray(y * stride, (y + 1) * stride);
439
+ const prev = y ? out.subarray((y - 1) * stride, y * stride) : Buffer.alloc(stride);
440
+ for (let i = 0; i < stride; i++) {
441
+ const a = i >= chan ? cur[i - chan] : 0, b = prev[i], c = i >= chan ? prev[i - chan] : 0;
442
+ let v = row[i];
443
+ if (f === 1) v += a; else if (f === 2) v += b; else if (f === 3) v += (a + b) >> 1;
444
+ else if (f === 4) {
445
+ const pp = a + b - c, pa = Math.abs(pp - a), pb = Math.abs(pp - b), pc = Math.abs(pp - c);
446
+ v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
447
+ }
448
+ cur[i] = v & 255;
449
+ }
450
+ }
451
+ const rgbBuf = Buffer.alloc(w * h * 3);
452
+ let alpha = null;
453
+ const needAlpha = ct === 4 || ct === 6 || (ct === 3 && trns);
454
+ if (needAlpha) alpha = Buffer.alloc(w * h);
455
+ for (let i = 0, n = w * h; i < n; i++) {
456
+ let r, g, b, a = 255;
457
+ if (ct === 6) { r = out[i * 4]; g = out[i * 4 + 1]; b = out[i * 4 + 2]; a = out[i * 4 + 3]; }
458
+ else if (ct === 2) { r = out[i * 3]; g = out[i * 3 + 1]; b = out[i * 3 + 2]; }
459
+ else if (ct === 0) { r = g = b = out[i]; }
460
+ else if (ct === 4) { r = g = b = out[i * 2]; a = out[i * 2 + 1]; }
461
+ else { const ix = out[i]; r = plte[ix * 3]; g = plte[ix * 3 + 1]; b = plte[ix * 3 + 2];
462
+ if (trns && ix < trns.length) a = trns[ix]; }
463
+ rgbBuf[i * 3] = r; rgbBuf[i * 3 + 1] = g; rgbBuf[i * 3 + 2] = b;
464
+ if (alpha) alpha[i] = a;
465
+ }
466
+ return { w, h, data: rgbBuf, filter: "Flate", alpha };
467
+ }
468
+
469
+ /* --------------------------------------------------------------------- svg */
470
+
471
+ /** Every drawable in the file, normalised to path data. */
472
+ function svgToPaths(svg) {
473
+ const out = [];
474
+ for (const m of svg.matchAll(/<path[^>]*\sd="([^"]+)"/g)) out.push(m[1]);
475
+ for (const m of svg.matchAll(/<circle[^>]*>/g)) {
476
+ const a = attrs(m[0]);
477
+ const [cx, cy, r] = [+a.cx || 0, +a.cy || 0, +a.r || 0];
478
+ out.push(`M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0`);
479
+ }
480
+ for (const m of svg.matchAll(/<ellipse[^>]*>/g)) {
481
+ const a = attrs(m[0]);
482
+ const [cx, cy, rx, ry] = [+a.cx || 0, +a.cy || 0, +a.rx || 0, +a.ry || 0];
483
+ out.push(`M ${cx - rx} ${cy} a ${rx} ${ry} 0 1 0 ${rx * 2} 0 a ${rx} ${ry} 0 1 0 ${-rx * 2} 0`);
484
+ }
485
+ for (const m of svg.matchAll(/<rect[^>]*>/g)) {
486
+ const a = attrs(m[0]);
487
+ const [X, Y, W, H, r] = [+a.x || 0, +a.y || 0, +a.width || 0, +a.height || 0, +a.rx || 0];
488
+ out.push(r
489
+ ? `M ${X + r} ${Y} H ${X + W - r} a ${r} ${r} 0 0 1 ${r} ${r} V ${Y + H - r} a ${r} ${r} 0 0 1 ${-r} ${r} ` +
490
+ `H ${X + r} a ${r} ${r} 0 0 1 ${-r} ${-r} V ${Y + r} a ${r} ${r} 0 0 1 ${r} ${-r} Z`
491
+ : `M ${X} ${Y} H ${X + W} V ${Y + H} H ${X} Z`);
492
+ }
493
+ for (const m of svg.matchAll(/<line[^>]*>/g)) {
494
+ const a = attrs(m[0]);
495
+ out.push(`M ${+a.x1 || 0} ${+a.y1 || 0} L ${+a.x2 || 0} ${+a.y2 || 0}`);
496
+ }
497
+ for (const m of svg.matchAll(/<(polyline|polygon)[^>]*>/g)) {
498
+ const a = attrs(m[0]);
499
+ const pts = (a.points || "").trim().split(/[\s,]+/).map(Number);
500
+ if (pts.length < 4) continue;
501
+ let d = `M ${pts[0]} ${pts[1]}`;
502
+ for (let i = 2; i < pts.length; i += 2) d += ` L ${pts[i]} ${pts[i + 1]}`;
503
+ out.push(d + (m[1] === "polygon" ? " Z" : ""));
504
+ }
505
+ return out;
506
+ }
507
+
508
+ const attrs = (tag) => Object.fromEntries([...tag.matchAll(/([a-zA-Z-]+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
509
+
510
+ function emitPath(o, d) {
511
+ const tok = d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) || [];
512
+ let i = 0, cmd = "", x = 0, y = 0, sx = 0, sy = 0, px = 0, py = 0;
513
+ const nx = () => parseFloat(tok[i++]);
514
+ while (i < tok.length) {
515
+ if (/[a-zA-Z]/.test(tok[i])) cmd = tok[i++];
516
+ const rel = cmd === cmd.toLowerCase();
517
+ const C = cmd.toUpperCase();
518
+ if (C === "M") {
519
+ const a = nx(), b = nx();
520
+ x = rel ? x + a : a; y = rel ? y + b : b; sx = x; sy = y; px = x; py = y;
521
+ o.push(`${num(x)} ${num(y)} m`);
522
+ cmd = rel ? "l" : "L";
523
+ } else if (C === "L") {
524
+ const a = nx(), b = nx();
525
+ x = rel ? x + a : a; y = rel ? y + b : b; px = x; py = y;
526
+ o.push(`${num(x)} ${num(y)} l`);
527
+ } else if (C === "H") { const a = nx(); x = rel ? x + a : a; px = x; py = y; o.push(`${num(x)} ${num(y)} l`); }
528
+ else if (C === "V") { const a = nx(); y = rel ? y + a : a; px = x; py = y; o.push(`${num(x)} ${num(y)} l`); }
529
+ else if (C === "C" || C === "S") {
530
+ let x1, y1;
531
+ if (C === "C") { const a = nx(), b = nx(); x1 = rel ? x + a : a; y1 = rel ? y + b : b; }
532
+ else { x1 = 2 * x - px; y1 = 2 * y - py; }
533
+ const c = nx(), e = nx(), f = nx(), g = nx();
534
+ const x2 = rel ? x + c : c, y2 = rel ? y + e : e;
535
+ const X = rel ? x + f : f, Y = rel ? y + g : g;
536
+ o.push(`${num(x1)} ${num(y1)} ${num(x2)} ${num(y2)} ${num(X)} ${num(Y)} c`);
537
+ px = x2; py = y2; x = X; y = Y;
538
+ } else if (C === "Q" || C === "T") {
539
+ let qx, qy;
540
+ if (C === "Q") { const a = nx(), b = nx(); qx = rel ? x + a : a; qy = rel ? y + b : b; }
541
+ else { qx = 2 * x - px; qy = 2 * y - py; }
542
+ const c = nx(), e = nx();
543
+ const X = rel ? x + c : c, Y = rel ? y + e : e;
544
+ o.push(`${num(x + (2 / 3) * (qx - x))} ${num(y + (2 / 3) * (qy - y))} ` +
545
+ `${num(X + (2 / 3) * (qx - X))} ${num(Y + (2 / 3) * (qy - Y))} ${num(X)} ${num(Y)} c`);
546
+ px = qx; py = qy; x = X; y = Y;
547
+ } else if (C === "A") {
548
+ const rx = nx(), ry = nx(), rot = nx(), large = nx(), sweep = nx();
549
+ const a = nx(), b = nx();
550
+ const X = rel ? x + a : a, Y = rel ? y + b : b;
551
+ arcTo(o, x, y, rx, ry, rot, large, sweep, X, Y);
552
+ px = X; py = Y; x = X; y = Y;
553
+ } else if (C === "Z") { o.push("h"); x = sx; y = sy; px = x; py = y; }
554
+ else { i++; }
555
+ }
556
+ }
557
+
558
+ /** SVG elliptical arc -> up to four cubic beziers (endpoint parameterisation). */
559
+ function arcTo(o, x1, y1, rx, ry, rotDeg, large, sweep, x2, y2) {
560
+ if (!rx || !ry) { o.push(`${num(x2)} ${num(y2)} l`); return; }
561
+ rx = Math.abs(rx); ry = Math.abs(ry);
562
+ const phi = (rotDeg * Math.PI) / 180, cos = Math.cos(phi), sin = Math.sin(phi);
563
+ const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
564
+ const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
565
+ let lam = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
566
+ if (lam > 1) { const s = Math.sqrt(lam); rx *= s; ry *= s; }
567
+ const sq = Math.max(0, (rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p) /
568
+ (rx * rx * y1p * y1p + ry * ry * x1p * x1p));
569
+ const co = (large !== sweep ? 1 : -1) * Math.sqrt(sq);
570
+ const cxp = co * (rx * y1p) / ry, cyp = co * -(ry * x1p) / rx;
571
+ const cx = cos * cxp - sin * cyp + (x1 + x2) / 2;
572
+ const cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
573
+ const ang = (ux, uy, vx, vy) => {
574
+ const d = (ux * vx + uy * vy) / (Math.hypot(ux, uy) * Math.hypot(vx, vy));
575
+ const a = Math.acos(Math.min(1, Math.max(-1, d)));
576
+ return ux * vy - uy * vx < 0 ? -a : a;
577
+ };
578
+ let th1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
579
+ let dth = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
580
+ if (!sweep && dth > 0) dth -= 2 * Math.PI;
581
+ if (sweep && dth < 0) dth += 2 * Math.PI;
582
+ const segs = Math.ceil(Math.abs(dth) / (Math.PI / 2));
583
+ const delta = dth / segs;
584
+ const t = (4 / 3) * Math.tan(delta / 4);
585
+ for (let s = 0; s < segs; s++) {
586
+ const a0 = th1 + s * delta, a1 = a0 + delta;
587
+ const c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
588
+ const p = (ca, sa) => [cos * rx * ca - sin * ry * sa + cx, sin * rx * ca + cos * ry * sa + cy];
589
+ const [, ] = p(c0, s0);
590
+ const [q1x, q1y] = p(c0 - t * s0, s0 + t * c0);
591
+ const [q2x, q2y] = p(c1 + t * s1, s1 - t * c1);
592
+ const [ex, ey] = p(c1, s1);
593
+ o.push(`${num(q1x)} ${num(q1y)} ${num(q2x)} ${num(q2y)} ${num(ex)} ${num(ey)} c`);
594
+ }
595
+ }
596
+
597
+ module.exports = { Doc, rgb, PT, svgToPaths };