wand-kit 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitkeep +0 -0
- package/PROMPT.md +255 -0
- package/SKILL.md +117 -0
- package/START-HERE.md +16 -0
- package/brands/current/assets/bg-cover-dark.png +0 -0
- package/brands/current/assets/bg-overlay-dark.png +0 -0
- package/brands/current/assets/bg-statement-dark.png +0 -0
- package/brands/current/assets/closing-art.png +0 -0
- package/brands/current/assets/cover-art.png +0 -0
- package/brands/current/assets/divider-art.png +0 -0
- package/brands/current/assets/glyphs/README.md +29 -0
- package/brands/current/assets/glyphs/activity.svg +15 -0
- package/brands/current/assets/glyphs/atom.svg +17 -0
- package/brands/current/assets/glyphs/badge-check.svg +16 -0
- package/brands/current/assets/glyphs/bell.svg +16 -0
- package/brands/current/assets/glyphs/blocks.svg +16 -0
- package/brands/current/assets/glyphs/book-open.svg +16 -0
- package/brands/current/assets/glyphs/bot.svg +20 -0
- package/brands/current/assets/glyphs/boxes.svg +26 -0
- package/brands/current/assets/glyphs/brain.svg +23 -0
- package/brands/current/assets/glyphs/briefcase.svg +16 -0
- package/brands/current/assets/glyphs/building-2.svg +21 -0
- package/brands/current/assets/glyphs/calendar.svg +18 -0
- package/brands/current/assets/glyphs/chart-column.svg +18 -0
- package/brands/current/assets/glyphs/chart-line.svg +16 -0
- package/brands/current/assets/glyphs/chart-pie.svg +16 -0
- package/brands/current/assets/glyphs/circle-check.svg +16 -0
- package/brands/current/assets/glyphs/clipboard-check.svg +17 -0
- package/brands/current/assets/glyphs/clock.svg +16 -0
- package/brands/current/assets/glyphs/cloud.svg +15 -0
- package/brands/current/assets/glyphs/code.svg +15 -0
- package/brands/current/assets/glyphs/coins.svg +18 -0
- package/brands/current/assets/glyphs/compass.svg +16 -0
- package/brands/current/assets/glyphs/cpu.svg +24 -0
- package/brands/current/assets/glyphs/database.svg +17 -0
- package/brands/current/assets/glyphs/eye.svg +16 -0
- package/brands/current/assets/glyphs/file-check.svg +17 -0
- package/brands/current/assets/glyphs/filter.svg +15 -0
- package/brands/current/assets/glyphs/fingerprint.svg +23 -0
- package/brands/current/assets/glyphs/flag.svg +16 -0
- package/brands/current/assets/glyphs/gauge.svg +16 -0
- package/brands/current/assets/glyphs/gavel.svg +19 -0
- package/brands/current/assets/glyphs/git-branch.svg +18 -0
- package/brands/current/assets/glyphs/globe.svg +17 -0
- package/brands/current/assets/glyphs/graduation-cap.svg +17 -0
- package/brands/current/assets/glyphs/handshake.svg +19 -0
- package/brands/current/assets/glyphs/hexagon.svg +15 -0
- package/brands/current/assets/glyphs/index.json +177 -0
- package/brands/current/assets/glyphs/infinity.svg +15 -0
- package/brands/current/assets/glyphs/key.svg +17 -0
- package/brands/current/assets/glyphs/landmark.svg +20 -0
- package/brands/current/assets/glyphs/layers.svg +17 -0
- package/brands/current/assets/glyphs/lightbulb.svg +17 -0
- package/brands/current/assets/glyphs/link.svg +16 -0
- package/brands/current/assets/glyphs/list-checks.svg +19 -0
- package/brands/current/assets/glyphs/lock.svg +16 -0
- package/brands/current/assets/glyphs/map.svg +17 -0
- package/brands/current/assets/glyphs/megaphone.svg +16 -0
- package/brands/current/assets/glyphs/message-square.svg +15 -0
- package/brands/current/assets/glyphs/milestone.svg +17 -0
- package/brands/current/assets/glyphs/network.svg +19 -0
- package/brands/current/assets/glyphs/orbit.svg +19 -0
- package/brands/current/assets/glyphs/package.svg +18 -0
- package/brands/current/assets/glyphs/plug.svg +18 -0
- package/brands/current/assets/glyphs/puzzle.svg +15 -0
- package/brands/current/assets/glyphs/radar.svg +22 -0
- package/brands/current/assets/glyphs/receipt.svg +17 -0
- package/brands/current/assets/glyphs/refresh-cw.svg +18 -0
- package/brands/current/assets/glyphs/rocket.svg +18 -0
- package/brands/current/assets/glyphs/route.svg +17 -0
- package/brands/current/assets/glyphs/scale.svg +19 -0
- package/brands/current/assets/glyphs/search.svg +16 -0
- package/brands/current/assets/glyphs/server.svg +18 -0
- package/brands/current/assets/glyphs/settings.svg +16 -0
- package/brands/current/assets/glyphs/share-2.svg +19 -0
- package/brands/current/assets/glyphs/shield-check.svg +16 -0
- package/brands/current/assets/glyphs/shuffle.svg +19 -0
- package/brands/current/assets/glyphs/sparkles.svg +19 -0
- package/brands/current/assets/glyphs/square-stack.svg +17 -0
- package/brands/current/assets/glyphs/target.svg +17 -0
- package/brands/current/assets/glyphs/trending-up.svg +16 -0
- package/brands/current/assets/glyphs/triangle-alert.svg +17 -0
- package/brands/current/assets/glyphs/trophy.svg +20 -0
- package/brands/current/assets/glyphs/users.svg +18 -0
- package/brands/current/assets/glyphs/wallet.svg +16 -0
- package/brands/current/assets/glyphs/wand-sparkles.svg +22 -0
- package/brands/current/assets/glyphs/workflow.svg +17 -0
- package/brands/current/assets/glyphs/zap.svg +15 -0
- package/brands/current/assets/mark-dark.png +0 -0
- package/brands/current/assets/mark-white.png +0 -0
- package/brands/current/assets/orb-cover.png +0 -0
- package/brands/current/assets/orb-glow.png +0 -0
- package/brands/current/assets/wordmark-dark.png +0 -0
- package/brands/current/assets/wordmark-white.png +0 -0
- package/brands/current/fonts/Geist-Bold.ttf +0 -0
- package/brands/current/fonts/Geist-BoldItalic.ttf +0 -0
- package/brands/current/fonts/Geist-ExtraBold.ttf +0 -0
- package/brands/current/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Italic.ttf +0 -0
- package/brands/current/fonts/Geist-Light.ttf +0 -0
- package/brands/current/fonts/Geist-LightItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Medium.ttf +0 -0
- package/brands/current/fonts/Geist-MediumItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Regular.ttf +0 -0
- package/brands/current/fonts/Geist-SemiBold.ttf +0 -0
- package/brands/current/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/brands/current/lib/glyphs.js +87 -0
- package/brands/current/lib/measure.js +115 -0
- package/brands/current/lib/pdf.js +603 -0
- package/brands/current/lib/ttf.py +167 -0
- package/brands/current/lib/wandxml.py +37 -0
- package/brands/current/tokens.json +206 -0
- package/brands/next/assets/closing-art.png +0 -0
- package/brands/next/assets/cover-art.png +0 -0
- package/brands/next/assets/divider-art.png +0 -0
- package/brands/next/assets/glyphs/README.md +29 -0
- package/brands/next/assets/glyphs/activity.svg +15 -0
- package/brands/next/assets/glyphs/atom.svg +17 -0
- package/brands/next/assets/glyphs/badge-check.svg +16 -0
- package/brands/next/assets/glyphs/bell.svg +16 -0
- package/brands/next/assets/glyphs/blocks.svg +16 -0
- package/brands/next/assets/glyphs/book-open.svg +16 -0
- package/brands/next/assets/glyphs/bot.svg +20 -0
- package/brands/next/assets/glyphs/boxes.svg +26 -0
- package/brands/next/assets/glyphs/brain.svg +23 -0
- package/brands/next/assets/glyphs/briefcase.svg +16 -0
- package/brands/next/assets/glyphs/building-2.svg +21 -0
- package/brands/next/assets/glyphs/calendar.svg +18 -0
- package/brands/next/assets/glyphs/chart-column.svg +18 -0
- package/brands/next/assets/glyphs/chart-line.svg +16 -0
- package/brands/next/assets/glyphs/chart-pie.svg +16 -0
- package/brands/next/assets/glyphs/circle-check.svg +16 -0
- package/brands/next/assets/glyphs/clipboard-check.svg +17 -0
- package/brands/next/assets/glyphs/clock.svg +16 -0
- package/brands/next/assets/glyphs/cloud.svg +15 -0
- package/brands/next/assets/glyphs/code.svg +15 -0
- package/brands/next/assets/glyphs/coins.svg +18 -0
- package/brands/next/assets/glyphs/compass.svg +16 -0
- package/brands/next/assets/glyphs/cpu.svg +24 -0
- package/brands/next/assets/glyphs/database.svg +17 -0
- package/brands/next/assets/glyphs/eye.svg +16 -0
- package/brands/next/assets/glyphs/file-check.svg +17 -0
- package/brands/next/assets/glyphs/filter.svg +15 -0
- package/brands/next/assets/glyphs/fingerprint.svg +23 -0
- package/brands/next/assets/glyphs/flag.svg +16 -0
- package/brands/next/assets/glyphs/gauge.svg +16 -0
- package/brands/next/assets/glyphs/gavel.svg +19 -0
- package/brands/next/assets/glyphs/git-branch.svg +18 -0
- package/brands/next/assets/glyphs/globe.svg +17 -0
- package/brands/next/assets/glyphs/graduation-cap.svg +17 -0
- package/brands/next/assets/glyphs/handshake.svg +19 -0
- package/brands/next/assets/glyphs/hexagon.svg +15 -0
- package/brands/next/assets/glyphs/index.json +177 -0
- package/brands/next/assets/glyphs/infinity.svg +15 -0
- package/brands/next/assets/glyphs/key.svg +17 -0
- package/brands/next/assets/glyphs/landmark.svg +20 -0
- package/brands/next/assets/glyphs/layers.svg +17 -0
- package/brands/next/assets/glyphs/lightbulb.svg +17 -0
- package/brands/next/assets/glyphs/link.svg +16 -0
- package/brands/next/assets/glyphs/list-checks.svg +19 -0
- package/brands/next/assets/glyphs/lock.svg +16 -0
- package/brands/next/assets/glyphs/map.svg +17 -0
- package/brands/next/assets/glyphs/megaphone.svg +16 -0
- package/brands/next/assets/glyphs/message-square.svg +15 -0
- package/brands/next/assets/glyphs/milestone.svg +17 -0
- package/brands/next/assets/glyphs/network.svg +19 -0
- package/brands/next/assets/glyphs/orbit.svg +19 -0
- package/brands/next/assets/glyphs/package.svg +18 -0
- package/brands/next/assets/glyphs/plug.svg +18 -0
- package/brands/next/assets/glyphs/puzzle.svg +15 -0
- package/brands/next/assets/glyphs/radar.svg +22 -0
- package/brands/next/assets/glyphs/receipt.svg +17 -0
- package/brands/next/assets/glyphs/refresh-cw.svg +18 -0
- package/brands/next/assets/glyphs/rocket.svg +18 -0
- package/brands/next/assets/glyphs/route.svg +17 -0
- package/brands/next/assets/glyphs/scale.svg +19 -0
- package/brands/next/assets/glyphs/search.svg +16 -0
- package/brands/next/assets/glyphs/server.svg +18 -0
- package/brands/next/assets/glyphs/settings.svg +16 -0
- package/brands/next/assets/glyphs/share-2.svg +19 -0
- package/brands/next/assets/glyphs/shield-check.svg +16 -0
- package/brands/next/assets/glyphs/shuffle.svg +19 -0
- package/brands/next/assets/glyphs/sparkles.svg +19 -0
- package/brands/next/assets/glyphs/square-stack.svg +17 -0
- package/brands/next/assets/glyphs/target.svg +17 -0
- package/brands/next/assets/glyphs/trending-up.svg +16 -0
- package/brands/next/assets/glyphs/triangle-alert.svg +17 -0
- package/brands/next/assets/glyphs/trophy.svg +20 -0
- package/brands/next/assets/glyphs/users.svg +18 -0
- package/brands/next/assets/glyphs/wallet.svg +16 -0
- package/brands/next/assets/glyphs/wand-sparkles.svg +22 -0
- package/brands/next/assets/glyphs/workflow.svg +17 -0
- package/brands/next/assets/glyphs/zap.svg +15 -0
- package/brands/next/assets/layers-dark.png +0 -0
- package/brands/next/assets/layers-light.png +0 -0
- package/brands/next/assets/ornaments/burst-dark.png +0 -0
- package/brands/next/assets/ornaments/burst-light.png +0 -0
- package/brands/next/assets/ornaments/chain-dark.png +0 -0
- package/brands/next/assets/ornaments/chain-light.png +0 -0
- package/brands/next/assets/ornaments/coil-dark.png +0 -0
- package/brands/next/assets/ornaments/coil-light.png +0 -0
- package/brands/next/assets/ornaments/cone-dark.png +0 -0
- package/brands/next/assets/ornaments/cone-light.png +0 -0
- package/brands/next/assets/ornaments/diamond-dark.png +0 -0
- package/brands/next/assets/ornaments/diamond-light.png +0 -0
- package/brands/next/assets/ornaments/fan-dark.png +0 -0
- package/brands/next/assets/ornaments/fan-light.png +0 -0
- package/brands/next/assets/ornaments/globe-dark.png +0 -0
- package/brands/next/assets/ornaments/globe-light.png +0 -0
- package/brands/next/assets/ornaments/orbit-dark.png +0 -0
- package/brands/next/assets/ornaments/orbit-light.png +0 -0
- package/brands/next/assets/ornaments/rings-dark.png +0 -0
- package/brands/next/assets/ornaments/rings-light.png +0 -0
- package/brands/next/assets/ornaments/sphere-dark.png +0 -0
- package/brands/next/assets/ornaments/sphere-light.png +0 -0
- package/brands/next/assets/ornaments/sun-dark.png +0 -0
- package/brands/next/assets/ornaments/sun-light.png +0 -0
- package/brands/next/assets/photos/atrium.jpg +0 -0
- package/brands/next/assets/photos/cantilever.jpg +0 -0
- package/brands/next/assets/photos/corridor.jpg +0 -0
- package/brands/next/assets/photos/dome.jpg +0 -0
- package/brands/next/assets/photos/facade.jpg +0 -0
- package/brands/next/assets/photos/glass.jpg +0 -0
- package/brands/next/assets/photos/stairs.jpg +0 -0
- package/brands/next/assets/photos/tower.jpg +0 -0
- package/brands/next/assets/photos/walk.jpg +0 -0
- package/brands/next/assets/spark-gold.png +0 -0
- package/brands/next/assets/statement-art.png +0 -0
- package/brands/next/assets/wordmark-dark.png +0 -0
- package/brands/next/assets/wordmark-light.png +0 -0
- package/brands/next/assets/wordmark.svg +7 -0
- package/brands/next/fonts/FKRomanStandard-Light.ttf +0 -0
- package/brands/next/fonts/OFL-OverusedGrotesk.txt +93 -0
- package/brands/next/fonts/OverusedGrotesk-Italic.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-Medium.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-MediumItalic.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-Roman.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-SemiBoldItalic.ttf +0 -0
- package/brands/next/fonts/faces.json +27 -0
- package/brands/next/lib/glyphs.js +89 -0
- package/brands/next/lib/measure.js +121 -0
- package/brands/next/lib/pdf.js +597 -0
- package/brands/next/lib/ttf.py +169 -0
- package/brands/next/lib/wandxml.py +37 -0
- package/brands/next/tokens.json +174 -0
- package/examples/gallery/job.mjs +83 -0
- package/examples/overview/job.mjs +99 -0
- package/lib/brand.js +135 -0
- package/lib/components.js +313 -0
- package/lib/content.js +66 -0
- package/lib/engine.js +280 -0
- package/lib/html_export.js +71 -0
- package/lib/pptx.js +446 -0
- package/lib/surfaces.js +62 -0
- package/package.json +17 -4
- package/templates/card.mjs +30 -0
- package/templates/page.mjs +52 -0
- package/templates/slide.mjs +46 -0
- package/tools/embed_fonts.py +154 -0
- package/wand.js +179 -0
- package/README.md +0 -3
|
@@ -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 };
|