wand-decks-kit 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROMPT.md +461 -0
- package/README.md +137 -0
- package/START-HERE.md +98 -0
- package/assets/README.md +40 -0
- package/assets/_icon-contact-sheet.png +0 -0
- package/assets/bg-cover-dark.png +0 -0
- package/assets/bg-overlay-dark.png +0 -0
- package/assets/bg-statement-dark.png +0 -0
- package/assets/glyphs/README.md +29 -0
- package/assets/glyphs/activity.svg +15 -0
- package/assets/glyphs/atom.svg +17 -0
- package/assets/glyphs/badge-check.svg +16 -0
- package/assets/glyphs/bell.svg +16 -0
- package/assets/glyphs/blocks.svg +16 -0
- package/assets/glyphs/book-open.svg +16 -0
- package/assets/glyphs/bot.svg +20 -0
- package/assets/glyphs/boxes.svg +26 -0
- package/assets/glyphs/brain.svg +23 -0
- package/assets/glyphs/briefcase.svg +16 -0
- package/assets/glyphs/building-2.svg +21 -0
- package/assets/glyphs/calendar.svg +18 -0
- package/assets/glyphs/chart-column.svg +18 -0
- package/assets/glyphs/chart-line.svg +16 -0
- package/assets/glyphs/chart-pie.svg +16 -0
- package/assets/glyphs/circle-check.svg +16 -0
- package/assets/glyphs/clipboard-check.svg +17 -0
- package/assets/glyphs/clock.svg +16 -0
- package/assets/glyphs/cloud.svg +15 -0
- package/assets/glyphs/coins.svg +18 -0
- package/assets/glyphs/compass.svg +16 -0
- package/assets/glyphs/cpu.svg +24 -0
- package/assets/glyphs/database.svg +17 -0
- package/assets/glyphs/eye.svg +16 -0
- package/assets/glyphs/file-check.svg +17 -0
- package/assets/glyphs/filter.svg +15 -0
- package/assets/glyphs/fingerprint.svg +23 -0
- package/assets/glyphs/flag.svg +16 -0
- package/assets/glyphs/gauge.svg +16 -0
- package/assets/glyphs/gavel.svg +19 -0
- package/assets/glyphs/git-branch.svg +18 -0
- package/assets/glyphs/globe.svg +17 -0
- package/assets/glyphs/graduation-cap.svg +17 -0
- package/assets/glyphs/handshake.svg +19 -0
- package/assets/glyphs/hexagon.svg +15 -0
- package/assets/glyphs/index.json +175 -0
- package/assets/glyphs/infinity.svg +15 -0
- package/assets/glyphs/key.svg +17 -0
- package/assets/glyphs/landmark.svg +20 -0
- package/assets/glyphs/layers.svg +17 -0
- package/assets/glyphs/lightbulb.svg +17 -0
- package/assets/glyphs/link.svg +16 -0
- package/assets/glyphs/list-checks.svg +19 -0
- package/assets/glyphs/lock.svg +16 -0
- package/assets/glyphs/map.svg +17 -0
- package/assets/glyphs/megaphone.svg +16 -0
- package/assets/glyphs/message-square.svg +15 -0
- package/assets/glyphs/milestone.svg +17 -0
- package/assets/glyphs/network.svg +19 -0
- package/assets/glyphs/orbit.svg +19 -0
- package/assets/glyphs/package.svg +18 -0
- package/assets/glyphs/plug.svg +18 -0
- package/assets/glyphs/puzzle.svg +15 -0
- package/assets/glyphs/radar.svg +22 -0
- package/assets/glyphs/receipt.svg +17 -0
- package/assets/glyphs/refresh-cw.svg +18 -0
- package/assets/glyphs/rocket.svg +18 -0
- package/assets/glyphs/route.svg +17 -0
- package/assets/glyphs/scale.svg +19 -0
- package/assets/glyphs/search.svg +16 -0
- package/assets/glyphs/server.svg +18 -0
- package/assets/glyphs/settings.svg +16 -0
- package/assets/glyphs/share-2.svg +19 -0
- package/assets/glyphs/shield-check.svg +16 -0
- package/assets/glyphs/shuffle.svg +19 -0
- package/assets/glyphs/sparkles.svg +19 -0
- package/assets/glyphs/square-stack.svg +17 -0
- package/assets/glyphs/target.svg +17 -0
- package/assets/glyphs/trending-up.svg +16 -0
- package/assets/glyphs/triangle-alert.svg +17 -0
- package/assets/glyphs/trophy.svg +20 -0
- package/assets/glyphs/users.svg +18 -0
- package/assets/glyphs/wallet.svg +16 -0
- package/assets/glyphs/wand-sparkles.svg +22 -0
- package/assets/glyphs/workflow.svg +17 -0
- package/assets/glyphs/zap.svg +15 -0
- package/assets/icons/icon-11.png +0 -0
- package/assets/icons/icon-12.png +0 -0
- package/assets/icons/icon-13.png +0 -0
- package/assets/icons/icon-14.png +0 -0
- package/assets/icons/icon-15.png +0 -0
- package/assets/icons/icon-16.png +0 -0
- package/assets/icons/icon-17.png +0 -0
- package/assets/icons/icon-18.png +0 -0
- package/assets/icons/icon-20.png +0 -0
- package/assets/icons/icon-23.png +0 -0
- package/assets/icons/icon-24.png +0 -0
- package/assets/icons/icon-25.png +0 -0
- package/assets/icons/icon-27.png +0 -0
- package/assets/icons/icon-28.png +0 -0
- package/assets/icons/icon-30.png +0 -0
- package/assets/icons/icon-31.png +0 -0
- package/assets/icons/icon-32.png +0 -0
- package/assets/icons/icon-33.png +0 -0
- package/assets/icons/icon-34.png +0 -0
- package/assets/icons/icon-35.png +0 -0
- package/assets/icons/icon-36.png +0 -0
- package/assets/icons/icon-37.png +0 -0
- package/assets/icons/icon-39.png +0 -0
- package/assets/icons/icon-40.png +0 -0
- package/assets/icons/icon-41.png +0 -0
- package/assets/icons/icon-43.png +0 -0
- package/assets/icons/icon-44.png +0 -0
- package/assets/icons/icon-45.png +0 -0
- package/assets/icons/icon-46.png +0 -0
- package/assets/icons/icon-48.png +0 -0
- package/assets/icons/icon-49.png +0 -0
- package/assets/icons/icon-50.png +0 -0
- package/assets/icons/icon-51.png +0 -0
- package/assets/icons/icon-52.png +0 -0
- package/assets/icons/icon-53.png +0 -0
- package/assets/icons/icon-9.png +0 -0
- package/assets/mark-dark.png +0 -0
- package/assets/mark-white.png +0 -0
- package/assets/orb-cover.png +0 -0
- package/assets/wordmark-dark.png +0 -0
- package/assets/wordmark-white.png +0 -0
- package/build.js +192 -0
- package/build_example.js +91 -0
- package/build_infinite.js +283 -0
- package/docs/LAYOUTS.md +86 -0
- package/docs/PIPELINE.md +116 -0
- package/docs/SPEC.md +89 -0
- package/embed_fonts.py +193 -0
- package/examples/template.json +164 -0
- package/fonts/Geist-Bold.ttf +0 -0
- package/fonts/Geist-BoldItalic.ttf +0 -0
- package/fonts/Geist-ExtraBold.ttf +0 -0
- package/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/fonts/Geist-Italic.ttf +0 -0
- package/fonts/Geist-Light.ttf +0 -0
- package/fonts/Geist-LightItalic.ttf +0 -0
- package/fonts/Geist-Medium.ttf +0 -0
- package/fonts/Geist-MediumItalic.ttf +0 -0
- package/fonts/Geist-Regular.ttf +0 -0
- package/fonts/Geist-SemiBold.ttf +0 -0
- package/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/lib/README.md +23 -0
- package/lib/brand.js +51 -0
- package/lib/glyphs.js +87 -0
- package/lib/layouts_client.js +212 -0
- package/lib/layouts_consult.js +301 -0
- package/lib/layouts_dense.js +361 -0
- package/lib/measure.js +109 -0
- package/lib/pdf.js +517 -0
- package/lib/plan.js +305 -0
- package/lib/render_pdf.js +203 -0
- package/lib/schema.js +399 -0
- package/lib/shared.js +15 -0
- package/lib/trace.js +88 -0
- package/lib/ttf.py +167 -0
- package/lib/wandxml.py +37 -0
- package/package.json +17 -0
- package/preview/preview.css +112 -0
- package/preview/preview.html +24 -0
- package/preview/preview.js +169 -0
- package/qa_coverage.py +120 -0
- package/requirements.txt +9 -0
- package/tools/extract.py +199 -0
- package/wand.js +397 -0
- package/wand_kit.js +1016 -0
package/lib/pdf.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
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
|
+
line({ x1, y1, x2, y2, color = "000000", width = 0.75 }) {
|
|
116
|
+
this.page.ops.push(`${rgb(color)} RG`, `${num(width)} w`,
|
|
117
|
+
`${num(x1 * PT)} ${num(this._y(y1))} m ${num(x2 * PT)} ${num(this._y(y2))} l S`);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ---------------------------------------------------------------- text */
|
|
122
|
+
|
|
123
|
+
_font(stem) {
|
|
124
|
+
if (!this.fonts.has(stem)) {
|
|
125
|
+
const file = path.join(this.fontsDir, stem + ".ttf");
|
|
126
|
+
if (!fs.existsSync(file)) throw new Error(`no font ${stem}.ttf in ${this.fontsDir}`);
|
|
127
|
+
this.fonts.set(stem, { stem, font: this.fontkit.openSync(file), file, used: new Map(), uni: new Map() });
|
|
128
|
+
}
|
|
129
|
+
return this.fonts.get(stem);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* One line of text at (x, y) where y is the BASELINE, from the page top.
|
|
134
|
+
* Returns the advance width in inches.
|
|
135
|
+
*/
|
|
136
|
+
text(str, { x, y, stem, size, color = "000000", tracking = 0 }) {
|
|
137
|
+
if (str == null || str === "") return 0;
|
|
138
|
+
const F = this._font(stem);
|
|
139
|
+
const run = F.font.layout(String(str));
|
|
140
|
+
const upm = F.font.unitsPerEm;
|
|
141
|
+
const hex = [];
|
|
142
|
+
let adv = 0;
|
|
143
|
+
for (const g of run.glyphs) {
|
|
144
|
+
F.used.set(g.id, g.advanceWidth);
|
|
145
|
+
if (g.codePoints && g.codePoints.length) F.uni.set(g.id, g.codePoints);
|
|
146
|
+
hex.push(g.id.toString(16).padStart(4, "0"));
|
|
147
|
+
adv += g.advanceWidth;
|
|
148
|
+
}
|
|
149
|
+
this.page.fonts.add(stem);
|
|
150
|
+
const o = this.page.ops;
|
|
151
|
+
o.push("BT", `${rgb(color)} rg`, `/F_${stem.replace(/[^A-Za-z0-9]/g, "")} ${num(size)} Tf`);
|
|
152
|
+
if (tracking) o.push(`${num(tracking)} Tc`);
|
|
153
|
+
o.push(`${num(x * PT)} ${num(this._y(y))} Td`, `<${hex.join("")}> Tj`);
|
|
154
|
+
if (tracking) o.push("0 Tc");
|
|
155
|
+
o.push("ET");
|
|
156
|
+
return (adv / upm) * (size / PT) + (tracking / PT) * run.glyphs.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ---------------------------------------------------------------- svg */
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Draw a Lucide-style stroked SVG at (x, y) in a `size` box.
|
|
163
|
+
*
|
|
164
|
+
* These are vector paths, so they go into the content stream as path
|
|
165
|
+
* operators rather than as an image — which is why an icon in a Wand document
|
|
166
|
+
* is crisp at any zoom and takes about 300 bytes. 58 of the 75 glyphs use
|
|
167
|
+
* elliptical arcs, so arc support is not optional; `arcTo` converts them to
|
|
168
|
+
* the beziers PDF understands.
|
|
169
|
+
*/
|
|
170
|
+
svg(svgText, { x, y, size, color = "000000", strokeWidth = 2 }) {
|
|
171
|
+
const o = this.page.ops;
|
|
172
|
+
const s = (size * PT) / 24; // the viewBox is always 0 0 24 24
|
|
173
|
+
o.push("q", `${rgb(color)} RG`, `${num(strokeWidth)} w`, "1 J", "1 j",
|
|
174
|
+
`${num(s)} 0 0 ${num(-s)} ${num(x * PT)} ${num(this._y(y))} cm`);
|
|
175
|
+
for (const d of svgToPaths(svgText)) emitPath(o, d);
|
|
176
|
+
o.push("S", "Q");
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* -------------------------------------------------------------- images */
|
|
181
|
+
|
|
182
|
+
image(file, { x, y, w, h }) {
|
|
183
|
+
const abs = path.resolve(file);
|
|
184
|
+
if (!this.images.has(abs)) this.images.set(abs, decodeImage(abs));
|
|
185
|
+
const im = this.images.get(abs);
|
|
186
|
+
const name = "Im" + [...this.images.keys()].indexOf(abs);
|
|
187
|
+
im.name = name;
|
|
188
|
+
this.page.images.add(abs);
|
|
189
|
+
this.page.ops.push("q", `${num(w * PT)} 0 0 ${num(h * PT)} ${num(x * PT)} ${num(this._y(y + h))} cm`,
|
|
190
|
+
`/${name} Do`, "Q");
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* --------------------------------------------------------------- write */
|
|
195
|
+
|
|
196
|
+
toBuffer() {
|
|
197
|
+
const objs = []; // 1-indexed on output
|
|
198
|
+
const ref = (n) => `${n} 0 R`;
|
|
199
|
+
const add = (body) => { objs.push(body); return objs.length; };
|
|
200
|
+
|
|
201
|
+
// fonts
|
|
202
|
+
const fontRefs = new Map();
|
|
203
|
+
for (const F of this.fonts.values()) {
|
|
204
|
+
const raw = fs.readFileSync(F.file);
|
|
205
|
+
const upm = F.font.unitsPerEm;
|
|
206
|
+
const sc = 1000 / upm;
|
|
207
|
+
const fileRef = add({ stream: zlib.deflateSync(raw),
|
|
208
|
+
dict: `/Filter /FlateDecode /Length1 ${raw.length}` });
|
|
209
|
+
const flags = 4 | (F.font.italicAngle ? 64 : 32);
|
|
210
|
+
const bb = F.font.bbox;
|
|
211
|
+
const desc = add(`<< /Type /FontDescriptor /FontName /${psName(F)} /Flags ${flags}` +
|
|
212
|
+
` /FontBBox [${Math.round(bb.minX * sc)} ${Math.round(bb.minY * sc)} ${Math.round(bb.maxX * sc)} ${Math.round(bb.maxY * sc)}]` +
|
|
213
|
+
` /ItalicAngle ${Math.round(F.font.italicAngle || 0)} /Ascent ${Math.round(F.font.ascent * sc)}` +
|
|
214
|
+
` /Descent ${Math.round(F.font.descent * sc)} /CapHeight ${Math.round((F.font.capHeight || 700) * sc)}` +
|
|
215
|
+
` /StemV 80 /FontFile2 ${ref(fileRef)} >>`);
|
|
216
|
+
const gids = [...F.used.keys()].sort((a, b) => a - b);
|
|
217
|
+
const W = gids.map((g) => `${g} [${Math.round(F.used.get(g) * sc)}]`).join(" ");
|
|
218
|
+
const cid = add(`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${psName(F)}` +
|
|
219
|
+
` /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>` +
|
|
220
|
+
` /FontDescriptor ${ref(desc)} /DW 1000 /W [${W}] /CIDToGIDMap /Identity >>`);
|
|
221
|
+
const tu = add({ stream: zlib.deflateSync(Buffer.from(toUnicode(F), "latin1")),
|
|
222
|
+
dict: "/Filter /FlateDecode" });
|
|
223
|
+
const f0 = add(`<< /Type /Font /Subtype /Type0 /BaseFont /${psName(F)}` +
|
|
224
|
+
` /Encoding /Identity-H /DescendantFonts [${ref(cid)}] /ToUnicode ${ref(tu)} >>`);
|
|
225
|
+
fontRefs.set(F.stem, f0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// images
|
|
229
|
+
const imageRefs = new Map();
|
|
230
|
+
for (const [abs, im] of this.images) {
|
|
231
|
+
let smask = null;
|
|
232
|
+
if (im.alpha) {
|
|
233
|
+
smask = add({ stream: zlib.deflateSync(im.alpha),
|
|
234
|
+
dict: `/Type /XObject /Subtype /Image /Width ${im.w} /Height ${im.h}` +
|
|
235
|
+
` /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode` });
|
|
236
|
+
}
|
|
237
|
+
const r = add({ stream: im.filter === "DCTDecode" ? im.data : zlib.deflateSync(im.data),
|
|
238
|
+
dict: `/Type /XObject /Subtype /Image /Width ${im.w} /Height ${im.h}` +
|
|
239
|
+
` /ColorSpace /DeviceRGB /BitsPerComponent 8` +
|
|
240
|
+
` /Filter /${im.filter === "DCTDecode" ? "DCTDecode" : "FlateDecode"}` +
|
|
241
|
+
(smask ? ` /SMask ${ref(smask)}` : "") });
|
|
242
|
+
imageRefs.set(abs, r);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// pages
|
|
246
|
+
const pagesRef = objs.length + 1 + this.pages.length * 2 + 1; // filled below
|
|
247
|
+
const kids = [];
|
|
248
|
+
for (const pg of this.pages) {
|
|
249
|
+
const content = add({ stream: zlib.deflateSync(Buffer.from(pg.ops.join("\n"), "latin1")),
|
|
250
|
+
dict: "/Filter /FlateDecode" });
|
|
251
|
+
const fRes = [...pg.fonts].map((s) => `/F_${s.replace(/[^A-Za-z0-9]/g, "")} ${ref(fontRefs.get(s))}`).join(" ");
|
|
252
|
+
const xRes = [...pg.images].map((a) => `/${this.images.get(a).name} ${ref(imageRefs.get(a))}`).join(" ");
|
|
253
|
+
const gRes = [...pg.alphas].map((a) => `/${this.alphas.get(a)} << /ca ${a} /CA 1 >>`).join(" ");
|
|
254
|
+
const p = add(`<< /Type /Page /Parent __PAGES__ /MediaBox [0 0 ${num(this.W * PT)} ${num(this.H * PT)}]` +
|
|
255
|
+
` /Resources << /Font << ${fRes} >> /XObject << ${xRes} >>` +
|
|
256
|
+
(gRes ? ` /ExtGState << ${gRes} >>` : "") + ` >> /Contents ${ref(content)} >>`);
|
|
257
|
+
kids.push(p);
|
|
258
|
+
}
|
|
259
|
+
const pagesObj = add(`<< /Type /Pages /Kids [${kids.map(ref).join(" ")}] /Count ${kids.length} >>`);
|
|
260
|
+
const catalog = add(`<< /Type /Catalog /Pages ${ref(pagesObj)} >>`);
|
|
261
|
+
const info = add(`<< /Producer (wand-docs) /Creator (wand-docs) >>`);
|
|
262
|
+
for (let i = 0; i < objs.length; i++) {
|
|
263
|
+
if (typeof objs[i] === "string") objs[i] = objs[i].replace("__PAGES__", ref(pagesObj));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const chunks = [Buffer.from("%PDF-1.7\n%\xE2\xE3\xCF\xD3\n", "latin1")];
|
|
267
|
+
let pos = chunks[0].length;
|
|
268
|
+
const offsets = [0];
|
|
269
|
+
objs.forEach((o, i) => {
|
|
270
|
+
const head = Buffer.from(`${i + 1} 0 obj\n`, "latin1");
|
|
271
|
+
const body = typeof o === "string"
|
|
272
|
+
? Buffer.from(o + "\n", "latin1")
|
|
273
|
+
: Buffer.concat([Buffer.from(`<< ${o.dict} /Length ${o.stream.length} >>\nstream\n`, "latin1"),
|
|
274
|
+
o.stream, Buffer.from("\nendstream\n", "latin1")]);
|
|
275
|
+
const tail = Buffer.from("endobj\n", "latin1");
|
|
276
|
+
offsets.push(pos);
|
|
277
|
+
chunks.push(head, body, tail);
|
|
278
|
+
pos += head.length + body.length + tail.length;
|
|
279
|
+
});
|
|
280
|
+
const xref = [`xref\n0 ${objs.length + 1}\n0000000000 65535 f \n`];
|
|
281
|
+
for (let i = 1; i <= objs.length; i++) xref.push(String(offsets[i]).padStart(10, "0") + " 00000 n \n");
|
|
282
|
+
chunks.push(Buffer.from(xref.join(""), "latin1"));
|
|
283
|
+
chunks.push(Buffer.from(`trailer\n<< /Size ${objs.length + 1} /Root ${ref(catalog)} /Info ${ref(info)} >>\n` +
|
|
284
|
+
`startxref\n${pos}\n%%EOF\n`, "latin1"));
|
|
285
|
+
return Buffer.concat(chunks);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const psName = (F) => F.stem.replace(/[^A-Za-z0-9]/g, "");
|
|
290
|
+
|
|
291
|
+
/** Without this, the PDF renders correctly and `pdftotext` returns mojibake. */
|
|
292
|
+
function toUnicode(F) {
|
|
293
|
+
const rev = new Map(F.uni); // recorded during layout
|
|
294
|
+
for (const cp of F.font.characterSet || []) { // anything layout never saw
|
|
295
|
+
try {
|
|
296
|
+
const g = F.font.glyphForCodePoint(cp);
|
|
297
|
+
if (g && F.used.has(g.id) && !rev.has(g.id)) rev.set(g.id, [cp]);
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
const utf16be = (cps) => cps.map((cp) => {
|
|
301
|
+
if (cp <= 0xffff) return cp.toString(16).padStart(4, "0").toUpperCase();
|
|
302
|
+
const v = cp - 0x10000;
|
|
303
|
+
return ((0xd800 | (v >> 10)).toString(16) + (0xdc00 | (v & 0x3ff)).toString(16)).toUpperCase();
|
|
304
|
+
}).join("");
|
|
305
|
+
const lines = [...rev.entries()].map(([g, cps]) =>
|
|
306
|
+
`<${g.toString(16).padStart(4, "0")}> <${utf16be(cps)}>`);
|
|
307
|
+
const chunks = [];
|
|
308
|
+
for (let i = 0; i < lines.length; i += 100) {
|
|
309
|
+
const part = lines.slice(i, i + 100);
|
|
310
|
+
chunks.push(`${part.length} beginbfchar\n${part.join("\n")}\nendbfchar`);
|
|
311
|
+
}
|
|
312
|
+
return `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
|
|
313
|
+
`/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n` +
|
|
314
|
+
`/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n` +
|
|
315
|
+
`${chunks.join("\n")}\nendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* ------------------------------------------------------------------ images */
|
|
319
|
+
|
|
320
|
+
function decodeImage(file) {
|
|
321
|
+
const buf = fs.readFileSync(file);
|
|
322
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
323
|
+
let i = 2, w = 0, h = 0;
|
|
324
|
+
while (i < buf.length) {
|
|
325
|
+
if (buf[i] !== 0xff) { i++; continue; }
|
|
326
|
+
const m = buf[i + 1];
|
|
327
|
+
if (m >= 0xc0 && m <= 0xcf && m !== 0xc4 && m !== 0xc8 && m !== 0xcc) {
|
|
328
|
+
h = buf.readUInt16BE(i + 5); w = buf.readUInt16BE(i + 7); break;
|
|
329
|
+
}
|
|
330
|
+
i += 2 + buf.readUInt16BE(i + 2);
|
|
331
|
+
}
|
|
332
|
+
return { w, h, data: buf, filter: "DCTDecode", alpha: null };
|
|
333
|
+
}
|
|
334
|
+
return decodePNG(buf, file);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function decodePNG(buf, file) {
|
|
338
|
+
let pos = 8, idat = [], w = 0, h = 0, bd = 0, ct = 0, plte = null, trns = null;
|
|
339
|
+
while (pos < buf.length) {
|
|
340
|
+
const len = buf.readUInt32BE(pos), type = buf.toString("latin1", pos + 4, pos + 8);
|
|
341
|
+
const data = buf.subarray(pos + 8, pos + 8 + len);
|
|
342
|
+
if (type === "IHDR") { w = data.readUInt32BE(0); h = data.readUInt32BE(4); bd = data[8]; ct = data[9]; }
|
|
343
|
+
else if (type === "IDAT") idat.push(data);
|
|
344
|
+
else if (type === "PLTE") plte = data;
|
|
345
|
+
else if (type === "tRNS") trns = data;
|
|
346
|
+
pos += 12 + len;
|
|
347
|
+
}
|
|
348
|
+
if (bd !== 8) throw new Error(`${path.basename(file)}: only 8-bit PNG is supported`);
|
|
349
|
+
const chan = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[ct];
|
|
350
|
+
if (!chan) throw new Error(`${path.basename(file)}: unsupported PNG colour type ${ct}`);
|
|
351
|
+
const raw = zlib.inflateSync(Buffer.concat(idat));
|
|
352
|
+
const stride = w * chan;
|
|
353
|
+
const out = Buffer.alloc(h * stride);
|
|
354
|
+
let p = 0;
|
|
355
|
+
for (let y = 0; y < h; y++) {
|
|
356
|
+
const f = raw[p++];
|
|
357
|
+
const row = raw.subarray(p, p + stride); p += stride;
|
|
358
|
+
const cur = out.subarray(y * stride, (y + 1) * stride);
|
|
359
|
+
const prev = y ? out.subarray((y - 1) * stride, y * stride) : Buffer.alloc(stride);
|
|
360
|
+
for (let i = 0; i < stride; i++) {
|
|
361
|
+
const a = i >= chan ? cur[i - chan] : 0, b = prev[i], c = i >= chan ? prev[i - chan] : 0;
|
|
362
|
+
let v = row[i];
|
|
363
|
+
if (f === 1) v += a; else if (f === 2) v += b; else if (f === 3) v += (a + b) >> 1;
|
|
364
|
+
else if (f === 4) {
|
|
365
|
+
const pp = a + b - c, pa = Math.abs(pp - a), pb = Math.abs(pp - b), pc = Math.abs(pp - c);
|
|
366
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
367
|
+
}
|
|
368
|
+
cur[i] = v & 255;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const rgbBuf = Buffer.alloc(w * h * 3);
|
|
372
|
+
let alpha = null;
|
|
373
|
+
const needAlpha = ct === 4 || ct === 6 || (ct === 3 && trns);
|
|
374
|
+
if (needAlpha) alpha = Buffer.alloc(w * h);
|
|
375
|
+
for (let i = 0, n = w * h; i < n; i++) {
|
|
376
|
+
let r, g, b, a = 255;
|
|
377
|
+
if (ct === 6) { r = out[i * 4]; g = out[i * 4 + 1]; b = out[i * 4 + 2]; a = out[i * 4 + 3]; }
|
|
378
|
+
else if (ct === 2) { r = out[i * 3]; g = out[i * 3 + 1]; b = out[i * 3 + 2]; }
|
|
379
|
+
else if (ct === 0) { r = g = b = out[i]; }
|
|
380
|
+
else if (ct === 4) { r = g = b = out[i * 2]; a = out[i * 2 + 1]; }
|
|
381
|
+
else { const ix = out[i]; r = plte[ix * 3]; g = plte[ix * 3 + 1]; b = plte[ix * 3 + 2];
|
|
382
|
+
if (trns && ix < trns.length) a = trns[ix]; }
|
|
383
|
+
rgbBuf[i * 3] = r; rgbBuf[i * 3 + 1] = g; rgbBuf[i * 3 + 2] = b;
|
|
384
|
+
if (alpha) alpha[i] = a;
|
|
385
|
+
}
|
|
386
|
+
return { w, h, data: rgbBuf, filter: "Flate", alpha };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* --------------------------------------------------------------------- svg */
|
|
390
|
+
|
|
391
|
+
/** Every drawable in the file, normalised to path data. */
|
|
392
|
+
function svgToPaths(svg) {
|
|
393
|
+
const out = [];
|
|
394
|
+
for (const m of svg.matchAll(/<path[^>]*\sd="([^"]+)"/g)) out.push(m[1]);
|
|
395
|
+
for (const m of svg.matchAll(/<circle[^>]*>/g)) {
|
|
396
|
+
const a = attrs(m[0]);
|
|
397
|
+
const [cx, cy, r] = [+a.cx || 0, +a.cy || 0, +a.r || 0];
|
|
398
|
+
out.push(`M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0`);
|
|
399
|
+
}
|
|
400
|
+
for (const m of svg.matchAll(/<ellipse[^>]*>/g)) {
|
|
401
|
+
const a = attrs(m[0]);
|
|
402
|
+
const [cx, cy, rx, ry] = [+a.cx || 0, +a.cy || 0, +a.rx || 0, +a.ry || 0];
|
|
403
|
+
out.push(`M ${cx - rx} ${cy} a ${rx} ${ry} 0 1 0 ${rx * 2} 0 a ${rx} ${ry} 0 1 0 ${-rx * 2} 0`);
|
|
404
|
+
}
|
|
405
|
+
for (const m of svg.matchAll(/<rect[^>]*>/g)) {
|
|
406
|
+
const a = attrs(m[0]);
|
|
407
|
+
const [X, Y, W, H, r] = [+a.x || 0, +a.y || 0, +a.width || 0, +a.height || 0, +a.rx || 0];
|
|
408
|
+
out.push(r
|
|
409
|
+
? `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} ` +
|
|
410
|
+
`H ${X + r} a ${r} ${r} 0 0 1 ${-r} ${-r} V ${Y + r} a ${r} ${r} 0 0 1 ${r} ${-r} Z`
|
|
411
|
+
: `M ${X} ${Y} H ${X + W} V ${Y + H} H ${X} Z`);
|
|
412
|
+
}
|
|
413
|
+
for (const m of svg.matchAll(/<line[^>]*>/g)) {
|
|
414
|
+
const a = attrs(m[0]);
|
|
415
|
+
out.push(`M ${+a.x1 || 0} ${+a.y1 || 0} L ${+a.x2 || 0} ${+a.y2 || 0}`);
|
|
416
|
+
}
|
|
417
|
+
for (const m of svg.matchAll(/<(polyline|polygon)[^>]*>/g)) {
|
|
418
|
+
const a = attrs(m[0]);
|
|
419
|
+
const pts = (a.points || "").trim().split(/[\s,]+/).map(Number);
|
|
420
|
+
if (pts.length < 4) continue;
|
|
421
|
+
let d = `M ${pts[0]} ${pts[1]}`;
|
|
422
|
+
for (let i = 2; i < pts.length; i += 2) d += ` L ${pts[i]} ${pts[i + 1]}`;
|
|
423
|
+
out.push(d + (m[1] === "polygon" ? " Z" : ""));
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const attrs = (tag) => Object.fromEntries([...tag.matchAll(/([a-zA-Z-]+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
|
|
429
|
+
|
|
430
|
+
function emitPath(o, d) {
|
|
431
|
+
const tok = d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/gi) || [];
|
|
432
|
+
let i = 0, cmd = "", x = 0, y = 0, sx = 0, sy = 0, px = 0, py = 0;
|
|
433
|
+
const nx = () => parseFloat(tok[i++]);
|
|
434
|
+
while (i < tok.length) {
|
|
435
|
+
if (/[a-zA-Z]/.test(tok[i])) cmd = tok[i++];
|
|
436
|
+
const rel = cmd === cmd.toLowerCase();
|
|
437
|
+
const C = cmd.toUpperCase();
|
|
438
|
+
if (C === "M") {
|
|
439
|
+
const a = nx(), b = nx();
|
|
440
|
+
x = rel ? x + a : a; y = rel ? y + b : b; sx = x; sy = y; px = x; py = y;
|
|
441
|
+
o.push(`${num(x)} ${num(y)} m`);
|
|
442
|
+
cmd = rel ? "l" : "L";
|
|
443
|
+
} else if (C === "L") {
|
|
444
|
+
const a = nx(), b = nx();
|
|
445
|
+
x = rel ? x + a : a; y = rel ? y + b : b; px = x; py = y;
|
|
446
|
+
o.push(`${num(x)} ${num(y)} l`);
|
|
447
|
+
} else if (C === "H") { const a = nx(); x = rel ? x + a : a; px = x; py = y; o.push(`${num(x)} ${num(y)} l`); }
|
|
448
|
+
else if (C === "V") { const a = nx(); y = rel ? y + a : a; px = x; py = y; o.push(`${num(x)} ${num(y)} l`); }
|
|
449
|
+
else if (C === "C" || C === "S") {
|
|
450
|
+
let x1, y1;
|
|
451
|
+
if (C === "C") { const a = nx(), b = nx(); x1 = rel ? x + a : a; y1 = rel ? y + b : b; }
|
|
452
|
+
else { x1 = 2 * x - px; y1 = 2 * y - py; }
|
|
453
|
+
const c = nx(), e = nx(), f = nx(), g = nx();
|
|
454
|
+
const x2 = rel ? x + c : c, y2 = rel ? y + e : e;
|
|
455
|
+
const X = rel ? x + f : f, Y = rel ? y + g : g;
|
|
456
|
+
o.push(`${num(x1)} ${num(y1)} ${num(x2)} ${num(y2)} ${num(X)} ${num(Y)} c`);
|
|
457
|
+
px = x2; py = y2; x = X; y = Y;
|
|
458
|
+
} else if (C === "Q" || C === "T") {
|
|
459
|
+
let qx, qy;
|
|
460
|
+
if (C === "Q") { const a = nx(), b = nx(); qx = rel ? x + a : a; qy = rel ? y + b : b; }
|
|
461
|
+
else { qx = 2 * x - px; qy = 2 * y - py; }
|
|
462
|
+
const c = nx(), e = nx();
|
|
463
|
+
const X = rel ? x + c : c, Y = rel ? y + e : e;
|
|
464
|
+
o.push(`${num(x + (2 / 3) * (qx - x))} ${num(y + (2 / 3) * (qy - y))} ` +
|
|
465
|
+
`${num(X + (2 / 3) * (qx - X))} ${num(Y + (2 / 3) * (qy - Y))} ${num(X)} ${num(Y)} c`);
|
|
466
|
+
px = qx; py = qy; x = X; y = Y;
|
|
467
|
+
} else if (C === "A") {
|
|
468
|
+
const rx = nx(), ry = nx(), rot = nx(), large = nx(), sweep = nx();
|
|
469
|
+
const a = nx(), b = nx();
|
|
470
|
+
const X = rel ? x + a : a, Y = rel ? y + b : b;
|
|
471
|
+
arcTo(o, x, y, rx, ry, rot, large, sweep, X, Y);
|
|
472
|
+
px = X; py = Y; x = X; y = Y;
|
|
473
|
+
} else if (C === "Z") { o.push("h"); x = sx; y = sy; px = x; py = y; }
|
|
474
|
+
else { i++; }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** SVG elliptical arc -> up to four cubic beziers (endpoint parameterisation). */
|
|
479
|
+
function arcTo(o, x1, y1, rx, ry, rotDeg, large, sweep, x2, y2) {
|
|
480
|
+
if (!rx || !ry) { o.push(`${num(x2)} ${num(y2)} l`); return; }
|
|
481
|
+
rx = Math.abs(rx); ry = Math.abs(ry);
|
|
482
|
+
const phi = (rotDeg * Math.PI) / 180, cos = Math.cos(phi), sin = Math.sin(phi);
|
|
483
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
484
|
+
const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
|
|
485
|
+
let lam = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
486
|
+
if (lam > 1) { const s = Math.sqrt(lam); rx *= s; ry *= s; }
|
|
487
|
+
const sq = Math.max(0, (rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p) /
|
|
488
|
+
(rx * rx * y1p * y1p + ry * ry * x1p * x1p));
|
|
489
|
+
const co = (large !== sweep ? 1 : -1) * Math.sqrt(sq);
|
|
490
|
+
const cxp = co * (rx * y1p) / ry, cyp = co * -(ry * x1p) / rx;
|
|
491
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2;
|
|
492
|
+
const cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
493
|
+
const ang = (ux, uy, vx, vy) => {
|
|
494
|
+
const d = (ux * vx + uy * vy) / (Math.hypot(ux, uy) * Math.hypot(vx, vy));
|
|
495
|
+
const a = Math.acos(Math.min(1, Math.max(-1, d)));
|
|
496
|
+
return ux * vy - uy * vx < 0 ? -a : a;
|
|
497
|
+
};
|
|
498
|
+
let th1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
499
|
+
let dth = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
500
|
+
if (!sweep && dth > 0) dth -= 2 * Math.PI;
|
|
501
|
+
if (sweep && dth < 0) dth += 2 * Math.PI;
|
|
502
|
+
const segs = Math.ceil(Math.abs(dth) / (Math.PI / 2));
|
|
503
|
+
const delta = dth / segs;
|
|
504
|
+
const t = (4 / 3) * Math.tan(delta / 4);
|
|
505
|
+
for (let s = 0; s < segs; s++) {
|
|
506
|
+
const a0 = th1 + s * delta, a1 = a0 + delta;
|
|
507
|
+
const c0 = Math.cos(a0), s0 = Math.sin(a0), c1 = Math.cos(a1), s1 = Math.sin(a1);
|
|
508
|
+
const p = (ca, sa) => [cos * rx * ca - sin * ry * sa + cx, sin * rx * ca + cos * ry * sa + cy];
|
|
509
|
+
const [, ] = p(c0, s0);
|
|
510
|
+
const [q1x, q1y] = p(c0 - t * s0, s0 + t * c0);
|
|
511
|
+
const [q2x, q2y] = p(c1 + t * s1, s1 - t * c1);
|
|
512
|
+
const [ex, ey] = p(c1, s1);
|
|
513
|
+
o.push(`${num(q1x)} ${num(q1y)} ${num(q2x)} ${num(q2y)} ${num(ex)} ${num(ey)} c`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
module.exports = { Doc, rgb, PT, svgToPaths };
|