pindeck 0.0.0-stage → 2.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/LICENSE +21 -0
- package/README.md +315 -2
- package/cli.mjs +97 -0
- package/lib/claude.mjs +70 -0
- package/lib/diff.mjs +46 -0
- package/lib/eids.mjs +608 -0
- package/lib/layout.mjs +37 -0
- package/lib/omml.mjs +175 -0
- package/lib/pdf-render.jxa.js +54 -0
- package/lib/pdf.mjs +68 -0
- package/lib/pptx-import.mjs +682 -0
- package/lib/pptx-render.mjs +100 -0
- package/lib/pptx.mjs +340 -0
- package/lib/system-prompt.md +97 -0
- package/lib/waveform.mjs +67 -0
- package/lib/xml.mjs +55 -0
- package/lib/zip.mjs +46 -0
- package/package.json +38 -4
- package/public/apple-touch-icon.png +0 -0
- package/public/bridge.js +963 -0
- package/public/editor.css +281 -0
- package/public/editor.js +1089 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +8 -0
- package/public/index.html +165 -0
- package/server.mjs +1163 -0
package/lib/omml.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* Office Math (OMML, as in PowerPoint and Word) to LaTeX for KaTeX.
|
|
2
|
+
Covers what equations in slides use: fractions, scripts, roots, delimiters, sums and integrals,
|
|
3
|
+
functions, limits, accents, bars, braces, matrices, equation arrays, and the Unicode math letters
|
|
4
|
+
(bold, italic, double-struck, ...). Anything unknown falls back to its text. */
|
|
5
|
+
|
|
6
|
+
import { kid, kids, textOf } from "./xml.mjs";
|
|
7
|
+
|
|
8
|
+
const GREEK = { α: "alpha", β: "beta", γ: "gamma", δ: "delta", ε: "varepsilon", ϵ: "epsilon", ζ: "zeta", η: "eta", θ: "theta", ϑ: "vartheta",
|
|
9
|
+
ι: "iota", κ: "kappa", ϰ: "varkappa", ϴ: "Theta", λ: "lambda", μ: "mu", ν: "nu", ξ: "xi", π: "pi", ϖ: "varpi", ρ: "rho", ϱ: "varrho", σ: "sigma", ς: "varsigma",
|
|
10
|
+
τ: "tau", υ: "upsilon", φ: "varphi", ϕ: "phi", χ: "chi", ψ: "psi", ω: "omega", Γ: "Gamma", Δ: "Delta", Θ: "Theta", Λ: "Lambda",
|
|
11
|
+
Ξ: "Xi", Π: "Pi", Σ: "Sigma", Υ: "Upsilon", Φ: "Phi", Ψ: "Psi", Ω: "Omega" };
|
|
12
|
+
const SYMBOLS = { Α: "A", Β: "B", Ε: "E", Ζ: "Z", Η: "H", Ι: "I", Κ: "K", Μ: "M", Ν: "N", Ο: "O", Ρ: "P", Τ: "T", Χ: "X", ο: "o", "≤": "\\le", "≥": "\\ge", "≠": "\\ne", "≈": "\\approx", "≡": "\\equiv", "∼": "\\sim", "≃": "\\simeq", "≅": "\\cong",
|
|
13
|
+
"∝": "\\propto", "∈": "\\in", "∉": "\\notin", "∋": "\\ni", "⊂": "\\subset", "⊆": "\\subseteq", "⊃": "\\supset", "⊇": "\\supseteq",
|
|
14
|
+
"∩": "\\cap", "∪": "\\cup", "∅": "\\emptyset", "∞": "\\infty", "∂": "\\partial", "∇": "\\nabla", "×": "\\times", "÷": "\\div",
|
|
15
|
+
"⋅": "\\cdot", "·": "\\cdot", "∙": "\\cdot", "±": "\\pm", "∓": "\\mp", "→": "\\to", "←": "\\leftarrow", "↔": "\\leftrightarrow",
|
|
16
|
+
"⇒": "\\Rightarrow", "⇐": "\\Leftarrow", "⇔": "\\Leftrightarrow", "↦": "\\mapsto", "∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
|
|
17
|
+
"∧": "\\wedge", "∨": "\\vee", "⊗": "\\otimes", "⊕": "\\oplus", "⊙": "\\odot", "∘": "\\circ", "∗": "\\ast", "⋆": "\\star",
|
|
18
|
+
"…": "\\ldots", "⋯": "\\cdots", "⋮": "\\vdots", "⋱": "\\ddots", "′": "'", "″": "''", "°": "^\\circ", "−": "-", "∣": "\\mid",
|
|
19
|
+
"‖": "\\|", "⊥": "\\perp", "∥": "\\parallel", "∠": "\\angle", "ℓ": "\\ell", "ℏ": "\\hbar", "ℜ": "\\Re", "ℑ": "\\Im",
|
|
20
|
+
"⟨": "\\langle", "⟩": "\\rangle", "⌊": "\\lfloor", "⌋": "\\rfloor", "⌈": "\\lceil", "⌉": "\\rceil", "∑": "\\sum", "∏": "\\prod",
|
|
21
|
+
"∫": "\\int", "∮": "\\oint", "√": "\\surd", "≪": "\\ll", "≫": "\\gg", "≜": "\\triangleq", "≔": ":=", "⊤": "\\top", "†": "\\dagger",
|
|
22
|
+
"ℝ": "\\mathbb{R}", "ℂ": "\\mathbb{C}", "ℕ": "\\mathbb{N}", "ℤ": "\\mathbb{Z}", "ℚ": "\\mathbb{Q}", "ℍ": "\\mathbb{H}", "ℙ": "\\mathbb{P}",
|
|
23
|
+
"ℎ": "h", "ℬ": "\\mathcal{B}", "ℰ": "\\mathcal{E}", "ℱ": "\\mathcal{F}", "ℋ": "\\mathcal{H}", "ℐ": "\\mathcal{I}", "ℒ": "\\mathcal{L}",
|
|
24
|
+
"ℳ": "\\mathcal{M}", "ℛ": "\\mathcal{R}", "{": "\\{", "}": "\\}", "%": "\\%", "#": "\\#", "&": "\\&", "_": "\\_", "$": "\\$",
|
|
25
|
+
"\\": "\\backslash ", "~": "\\sim " };
|
|
26
|
+
|
|
27
|
+
/* Unicode mathematical alphanumerics (U+1D400 ...): style and plain letter */
|
|
28
|
+
const LATIN_STYLES = [[0x1d400, "mathbf"], [0x1d434, ""], [0x1d468, "boldsymbol"], [0x1d49c, "mathcal"], [0x1d4d0, "mathcal"],
|
|
29
|
+
[0x1d504, "mathfrak"], [0x1d538, "mathbb"], [0x1d56c, "mathfrak"], [0x1d5a0, "mathsf"], [0x1d5d4, "mathsf"], [0x1d608, "mathsf"],
|
|
30
|
+
[0x1d63c, "mathsf"], [0x1d670, "mathtt"]];
|
|
31
|
+
// each style has 26 capitals (with ϴ and ∇) and 32 small letters (with ∂ϵϑϰϕϱϖ)
|
|
32
|
+
const GREEK_UPPER = "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡϴΣΤΥΦΧΨΩ∇", GREEK_LOWER = "αβγδεζηθικλμνξοπρςστυφχψω∂ϵϑϰϕϱϖ";
|
|
33
|
+
function mathLetter(cp) {
|
|
34
|
+
if (cp >= 0x1d400 && cp < 0x1d6a4) {
|
|
35
|
+
const i = Math.floor((cp - 0x1d400) / 52), r = (cp - 0x1d400) % 52;
|
|
36
|
+
const [, style] = LATIN_STYLES[i] || [, ""];
|
|
37
|
+
return { style, ch: String.fromCharCode(r < 26 ? 65 + r : 97 + r - 26) };
|
|
38
|
+
}
|
|
39
|
+
if (cp >= 0x1d6a8 && cp < 0x1d7ca) { // Greek: bold, italic, bold italic, sans bold, sans bold italic
|
|
40
|
+
const i = Math.floor((cp - 0x1d6a8) / 58), r = (cp - 0x1d6a8) % 58;
|
|
41
|
+
const ch = r < 26 ? GREEK_UPPER[r] : GREEK_LOWER[r - 26];
|
|
42
|
+
return { style: ["boldsymbol", "", "boldsymbol", "boldsymbol", "boldsymbol"][i], ch };
|
|
43
|
+
}
|
|
44
|
+
if (cp >= 0x1d7ce && cp <= 0x1d7ff) {
|
|
45
|
+
const i = Math.floor((cp - 0x1d7ce) / 10);
|
|
46
|
+
return { style: ["mathbf", "mathbb", "mathsf", "mathsf", "mathtt"][i], ch: String((cp - 0x1d7ce) % 10) };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Text of a math run: letters as they are, symbols as commands, styled letters grouped */
|
|
52
|
+
function textToTex(s, sty) {
|
|
53
|
+
let out = "", group = null, buf = "";
|
|
54
|
+
const flush = () => { if (buf) out += group ? `\\${group}{${buf}}` : buf; buf = ""; };
|
|
55
|
+
for (const c of s) {
|
|
56
|
+
const ml = mathLetter(c.codePointAt(0));
|
|
57
|
+
let style = ml ? ml.style : "", tex = ml ? ml.ch : c;
|
|
58
|
+
if (!ml && sty === "b" && /[A-Za-z0-9]/.test(c)) style = "mathbf";
|
|
59
|
+
if (!ml && sty === "bi" && /[A-Za-z]/.test(c)) style = "boldsymbol";
|
|
60
|
+
if (GREEK[tex]) tex = `\\${GREEK[tex]} `;
|
|
61
|
+
else if (SYMBOLS[tex]) tex = SYMBOLS[tex] + (/[a-z]$/i.test(SYMBOLS[tex]) ? " " : "");
|
|
62
|
+
if (style !== group) { flush(); group = style; }
|
|
63
|
+
buf += tex;
|
|
64
|
+
}
|
|
65
|
+
flush();
|
|
66
|
+
return out.replace(/ +$/, " ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FUNCTIONS = new Set(["sin", "cos", "tan", "cot", "sec", "csc", "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", "log", "ln",
|
|
70
|
+
"lg", "exp", "lim", "max", "min", "sup", "inf", "arg", "det", "dim", "ker", "deg", "gcd", "Pr", "liminf", "limsup", "argmax", "argmin"]);
|
|
71
|
+
const NARY = { "∑": "\\sum", "∏": "\\prod", "∐": "\\coprod", "∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
|
|
72
|
+
"⋃": "\\bigcup", "⋂": "\\bigcap", "⋁": "\\bigvee", "⋀": "\\bigwedge", "⨁": "\\bigoplus", "⨂": "\\bigotimes" };
|
|
73
|
+
const ACCENTS = { "̂": "hat", "̃": "tilde", "̄": "bar", "̅": "overline", "⃗": "vec", "⃑": "vec", "̇": "dot",
|
|
74
|
+
"̈": "ddot", "̌": "check", "́": "acute", "̀": "grave", "̆": "breve", "˜": "tilde", "^": "hat", "~": "tilde" };
|
|
75
|
+
const DELIMS = { "(": "(", ")": ")", "[": "[", "]": "]", "{": "\\{", "}": "\\}", "|": "|", "‖": "\\|", "⟨": "\\langle", "⟩": "\\rangle",
|
|
76
|
+
"〈": "\\langle", "〉": "\\rangle", "⌊": "\\lfloor", "⌋": "\\rfloor", "⌈": "\\lceil", "⌉": "\\rceil", "/": "/", "⟦": "\\llbracket", "⟧": "\\rrbracket", "": "." };
|
|
77
|
+
|
|
78
|
+
/* Plain text inside \text{}: TeX's special characters escaped */
|
|
79
|
+
const TEXT_ESC = { "\\": "\\textbackslash{}", "{": "\\{", "}": "\\}", "%": "\\%", "&": "\\&", "#": "\\#", "_": "\\_", "$": "\\$",
|
|
80
|
+
"^": "\\textasciicircum{}", "~": "\\textasciitilde{}" };
|
|
81
|
+
const textEsc = (t) => t.replace(/[\\{}%&#_$^~]/g, (c) => TEXT_ESC[c]);
|
|
82
|
+
|
|
83
|
+
const val = (pr, name, fallback = null) => kid(pr, name)?.attrs["m:val"] ?? fallback;
|
|
84
|
+
const brace = (s) => (s.length === 1 || /^\\[a-zA-Z]+ ?$/.test(s) ? s.trim() : `{${s}}`);
|
|
85
|
+
|
|
86
|
+
function conv(el) {
|
|
87
|
+
if (!el) return "";
|
|
88
|
+
if (el.text != null) return "";
|
|
89
|
+
const c = (name) => conv(kid(el, name));
|
|
90
|
+
const all = () => (el.children || []).filter((x) => x.name && !x.name.endsWith("Pr")).map(conv).join("");
|
|
91
|
+
switch (el.name) {
|
|
92
|
+
case "m:r": {
|
|
93
|
+
const t = textOf(kid(el, "m:t"));
|
|
94
|
+
const rPr = kid(el, "m:rPr");
|
|
95
|
+
if (rPr && kid(rPr, "m:nor")) return `\\text{${textEsc(t)}}`;
|
|
96
|
+
const sty = val(rPr, "m:sty");
|
|
97
|
+
const tex = textToTex(t, sty);
|
|
98
|
+
if (sty === "p" && /^[A-Za-z]{2,}$/.test(t)) return FUNCTIONS.has(t) ? `\\${t} ` : `\\mathrm{${t}}`;
|
|
99
|
+
return tex;
|
|
100
|
+
}
|
|
101
|
+
case "m:f": {
|
|
102
|
+
const type = val(kid(el, "m:fPr"), "m:type", "bar");
|
|
103
|
+
const num = c("m:num"), den = c("m:den");
|
|
104
|
+
if (type === "lin") return `${brace(num)}/${brace(den)}`;
|
|
105
|
+
if (type === "noBar") return `\\genfrac{}{}{0pt}{}{${num}}{${den}}`;
|
|
106
|
+
return `\\frac{${num}}{${den}}`;
|
|
107
|
+
}
|
|
108
|
+
case "m:sSup": return `${brace(c("m:e"))}^{${c("m:sup")}}`;
|
|
109
|
+
case "m:sSub": return `${brace(c("m:e"))}_{${c("m:sub")}}`;
|
|
110
|
+
case "m:sSubSup": return `${brace(c("m:e"))}_{${c("m:sub")}}^{${c("m:sup")}}`;
|
|
111
|
+
case "m:sPre": return `{}_{${c("m:sub")}}^{${c("m:sup")}}${brace(c("m:e"))}`;
|
|
112
|
+
case "m:rad": {
|
|
113
|
+
const hide = val(kid(el, "m:radPr"), "m:degHide") === "1" || val(kid(el, "m:radPr"), "m:degHide") === "on";
|
|
114
|
+
const deg = c("m:deg");
|
|
115
|
+
return hide || !deg ? `\\sqrt{${c("m:e")}}` : `\\sqrt[${deg}]{${c("m:e")}}`;
|
|
116
|
+
}
|
|
117
|
+
case "m:d": {
|
|
118
|
+
const pr = kid(el, "m:dPr");
|
|
119
|
+
const beg = val(pr, "m:begChr", "("), end = val(pr, "m:endChr", ")"), sep = val(pr, "m:sepChr", "|");
|
|
120
|
+
const parts = kids(el, "m:e").map(conv);
|
|
121
|
+
const d = (ch) => DELIMS[ch] ?? "."; // \left and \right take only real delimiters
|
|
122
|
+
const sepTex = sep === "|" ? "\\mid " : DELIMS[sep] ?? (SYMBOLS[sep] || sep);
|
|
123
|
+
return `\\left${d(beg)} ${parts.join(sepTex === "," ? ", " : ` ${sepTex} `)} \\right${d(end)}`;
|
|
124
|
+
}
|
|
125
|
+
case "m:nary": {
|
|
126
|
+
const pr = kid(el, "m:naryPr");
|
|
127
|
+
const chr = val(pr, "m:chr", "∫");
|
|
128
|
+
const op = NARY[chr] || SYMBOLS[chr] || chr;
|
|
129
|
+
const hidden = (n) => ["1", "on", "true"].includes(val(pr, n));
|
|
130
|
+
const lim = val(pr, "m:limLoc") === "undOvr" && /int/.test(op) ? "\\limits" : "";
|
|
131
|
+
const sub = hidden("m:subHide") ? "" : c("m:sub"), sup = hidden("m:supHide") ? "" : c("m:sup");
|
|
132
|
+
return `${op}${lim}${sub ? `_{${sub}}` : ""}${sup ? `^{${sup}}` : ""} ${c("m:e")}`;
|
|
133
|
+
}
|
|
134
|
+
case "m:func": {
|
|
135
|
+
const name = conv(kid(el, "m:fName")).trim();
|
|
136
|
+
const plain = name.replace(/\\mathrm\{([a-z]+)\}/gi, "$1");
|
|
137
|
+
const head = /^[A-Za-z]+$/.test(plain) ? (FUNCTIONS.has(plain) ? `\\${plain}` : `\\operatorname{${plain}}`) : name;
|
|
138
|
+
return `${head}${/^\\[a-z]+$/i.test(head) ? " " : ""}${brace(c("m:e"))}`;
|
|
139
|
+
}
|
|
140
|
+
case "m:limLow": {
|
|
141
|
+
const e = c("m:e").trim(), lim = c("m:lim");
|
|
142
|
+
if (/^\\(lim|max|min|sup|inf|argmax|argmin|operatorname\{[^}]+\})\s*$/.test(e) || FUNCTIONS.has(e.replace(/^\\/, ""))) return `${e.startsWith("\\") ? e : "\\" + e}_{${lim}}`;
|
|
143
|
+
return `\\underset{${lim}}{${e}}`;
|
|
144
|
+
}
|
|
145
|
+
case "m:limUpp": return `\\overset{${c("m:lim")}}{${c("m:e")}}`;
|
|
146
|
+
case "m:acc": {
|
|
147
|
+
const chr = val(kid(el, "m:accPr"), "m:chr", "̂");
|
|
148
|
+
return `\\${ACCENTS[chr] || "hat"}{${c("m:e")}}`;
|
|
149
|
+
}
|
|
150
|
+
case "m:bar": return val(kid(el, "m:barPr"), "m:pos", "bot") === "top" ? `\\overline{${c("m:e")}}` : `\\underline{${c("m:e")}}`;
|
|
151
|
+
case "m:groupChr": {
|
|
152
|
+
const pr = kid(el, "m:groupChrPr");
|
|
153
|
+
const chr = val(pr, "m:chr", "⏟"), top = val(pr, "m:pos", "bot") === "top";
|
|
154
|
+
if (chr === "⏞" || (top && chr === "⏟")) return `\\overbrace{${c("m:e")}}`;
|
|
155
|
+
if (chr === "⏟") return `\\underbrace{${c("m:e")}}`;
|
|
156
|
+
return top ? `\\overset{${SYMBOLS[chr] || chr}}{${c("m:e")}}` : `\\underset{${SYMBOLS[chr] || chr}}{${c("m:e")}}`;
|
|
157
|
+
}
|
|
158
|
+
case "m:borderBox": return `\\boxed{${c("m:e")}}`;
|
|
159
|
+
// "&" in an equation array marks the alignment point (textToTex made it a literal \&)
|
|
160
|
+
case "m:eqArr": return `\\begin{aligned}${kids(el, "m:e").map((e) => conv(e).replace(/\\&/g, "&")).join(" \\\\ ")}\\end{aligned}`;
|
|
161
|
+
case "m:m": return `\\begin{matrix}${kids(el, "m:mr").map((r) => kids(r, "m:e").map(conv).join(" & ")).join(" \\\\ ")}\\end{matrix}`;
|
|
162
|
+
case "m:oMathPara": return kids(el, "m:oMath").map(conv).join(" \\\\ ");
|
|
163
|
+
case "a14:m": return all();
|
|
164
|
+
default: return all();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* LaTeX of an OMML element (a14:m, m:oMathPara or m:oMath) and whether it is a displayed equation */
|
|
169
|
+
export function ommlToTex(el) {
|
|
170
|
+
const display = el.name === "m:oMathPara" || (el.name === "a14:m" && !!kid(el, "m:oMathPara"));
|
|
171
|
+
const tex = conv(el).replace(/\s+/g, " ").replace(/\s*([{}^_])\s*/g, "$1").replace(/\\([a-zA-Z]+)([{}^_])/g, "\\$1$2").trim()
|
|
172
|
+
// a degree sign or prime right after a superscript (x^{2}°) would be a double superscript
|
|
173
|
+
.replace(/(\^\{(?:[^{}]|\{[^{}]*\})*\})(?=\^|')/g, "$1{}");
|
|
174
|
+
return { tex, display };
|
|
175
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Renders PDF pages, or regions of them, to PNG with macOS PDFKit (built into macOS, no extra tools).
|
|
2
|
+
// Used by the PowerPoint import: each slide was exported as one PDF page.
|
|
3
|
+
// osascript -l JavaScript pdf-render.jxa.js <jobs.json>
|
|
4
|
+
// jobs.json: { "pdf": "/path/deck.pdf", "slideWidth": 1280, "jobs": [
|
|
5
|
+
// { "page": 1, "out": "/path/page-1.png", "width": 640 }, // whole page
|
|
6
|
+
// { "page": 4, "out": "/path/fig.png", "box": [x, y, w, h], "scale": 2 } ] } // region, in slide px
|
|
7
|
+
// Prints the number of pages of the PDF; with "info": true instead { pages, sizes: [[w, h] in points] }.
|
|
8
|
+
ObjC.import("PDFKit");
|
|
9
|
+
ObjC.import("AppKit");
|
|
10
|
+
function run(argv) {
|
|
11
|
+
const spec = JSON.parse(ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(argv[0], $.NSUTF8StringEncoding, null)));
|
|
12
|
+
const doc = $.PDFDocument.alloc.initWithURL($.NSURL.fileURLWithPath(spec.pdf));
|
|
13
|
+
if (!doc || doc.isNil()) throw new Error("Cannot open " + spec.pdf);
|
|
14
|
+
if (spec.info) {
|
|
15
|
+
const sizes = [];
|
|
16
|
+
for (let i = 0; i < doc.pageCount; i++) {
|
|
17
|
+
const b = doc.pageAtIndex(i).boundsForBox($.kPDFDisplayBoxMediaBox);
|
|
18
|
+
sizes.push([Math.round(b.size.width), Math.round(b.size.height)]);
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify({ pages: Number(doc.pageCount), sizes });
|
|
21
|
+
}
|
|
22
|
+
for (const job of spec.jobs || []) {
|
|
23
|
+
const page = doc.pageAtIndex(job.page - 1);
|
|
24
|
+
if (!page || page.isNil()) continue;
|
|
25
|
+
const pb = page.boundsForBox($.kPDFDisplayBoxMediaBox);
|
|
26
|
+
const pw = pb.size.width, ph = pb.size.height;
|
|
27
|
+
const f = pw / (spec.slideWidth || 1280); // PDF points per slide px
|
|
28
|
+
let w, h, scale, tx, ty;
|
|
29
|
+
if (job.box) {
|
|
30
|
+
const [x, y, bw, bh] = job.box;
|
|
31
|
+
scale = (job.scale || 2) / f; // output px per PDF point
|
|
32
|
+
w = Math.max(1, Math.round(bw * (job.scale || 2))); h = Math.max(1, Math.round(bh * (job.scale || 2)));
|
|
33
|
+
tx = -x * f; ty = -(ph - (y + bh) * f); // PDF y runs upwards
|
|
34
|
+
} else {
|
|
35
|
+
scale = (job.width || 1280) / pw;
|
|
36
|
+
w = Math.round(pw * scale); h = Math.round(ph * scale); tx = 0; ty = 0;
|
|
37
|
+
}
|
|
38
|
+
const rep = $.NSBitmapImageRep.alloc.initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBytesPerRowBitsPerPixel(
|
|
39
|
+
null, w, h, 8, 4, true, false, $.NSDeviceRGBColorSpace, 0, 0);
|
|
40
|
+
const ctx = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(rep);
|
|
41
|
+
$.NSGraphicsContext.saveGraphicsState;
|
|
42
|
+
$.NSGraphicsContext.currentContext = ctx;
|
|
43
|
+
$.NSColor.whiteColor.setFill;
|
|
44
|
+
$.NSRectFill($.NSMakeRect(0, 0, w, h));
|
|
45
|
+
const cg = ctx.CGContext;
|
|
46
|
+
$.CGContextScaleCTM(cg, scale, scale);
|
|
47
|
+
$.CGContextTranslateCTM(cg, tx, ty);
|
|
48
|
+
page.drawWithBoxToContext($.kPDFDisplayBoxMediaBox, cg);
|
|
49
|
+
ctx.flushGraphics;
|
|
50
|
+
$.NSGraphicsContext.restoreGraphicsState;
|
|
51
|
+
rep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $({})).writeToFileAtomically(job.out, true);
|
|
52
|
+
}
|
|
53
|
+
return String(doc.pageCount);
|
|
54
|
+
}
|
package/lib/pdf.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* PDF export through headless Chrome, in two steps.
|
|
2
|
+
|
|
3
|
+
1. Layout: open the deck in reveal.js print view (?print-pdf) with --dump-dom and a virtual-time
|
|
4
|
+
budget. Chrome runs the page's timers until reveal.js has built the print layout (one
|
|
5
|
+
.pdf-page per slide, or per build step) and prints the resulting DOM. requestAnimationFrame
|
|
6
|
+
is replaced by a timer for this, since headless Chrome renders no frames here.
|
|
7
|
+
2. Print: the dumped DOM, without its scripts, is served as a static page and printed with
|
|
8
|
+
--print-to-pdf. Nothing is left to wait for, so Chrome's print timing no longer matters.
|
|
9
|
+
|
|
10
|
+
Chrome's own --print-to-pdf on the live deck prints before reveal.js has laid out the pages,
|
|
11
|
+
and DevTools-based printing is not used because managed Chrome installs may forbid remote
|
|
12
|
+
debugging. Only plain command-line flags are needed. */
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
|
|
17
|
+
/* Added to the <head> of the print page: headless Chrome renders no animation frames here, and
|
|
18
|
+
reveal.js waits for one (requestAnimationFrame) before it builds the print layout. */
|
|
19
|
+
export const PDF_HEAD_TAG = `<script>window.requestAnimationFrame = function (cb) {
|
|
20
|
+
return setTimeout(function () { cb(performance.now()); }, 16);
|
|
21
|
+
};</script>
|
|
22
|
+
</head>`;
|
|
23
|
+
|
|
24
|
+
/* Added to the end of the print page: print colours and backgrounds exactly. */
|
|
25
|
+
export const PDF_PAGE_TAG = `<style>@media print { * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } }</style>
|
|
26
|
+
</body>`;
|
|
27
|
+
|
|
28
|
+
const BASE = ["--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check",
|
|
29
|
+
"--disable-extensions", "--mute-audio", "--hide-scrollbars"];
|
|
30
|
+
|
|
31
|
+
/* Runs Chrome with args, stops it once done(stdout, stderr) is true (Chrome often lingers). */
|
|
32
|
+
function chromeRun(chrome, args, profile, done, timeout) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const proc = spawn(chrome, [...BASE, `--user-data-dir=${profile}`, ...args]);
|
|
35
|
+
let out = "", err = "", finished = false;
|
|
36
|
+
const finish = (e, v) => {
|
|
37
|
+
if (finished) return;
|
|
38
|
+
finished = true;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
proc.kill("SIGKILL");
|
|
41
|
+
setTimeout(() => fs.rm(profile, { recursive: true, force: true }, () => {}), 500);
|
|
42
|
+
e ? reject(e) : resolve(v);
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(() => finish(new Error("Chrome timed out.")), timeout);
|
|
45
|
+
const check = () => { if (done(out, err)) setTimeout(() => finish(null, out), 150); };
|
|
46
|
+
proc.stdout.on("data", (d) => { out += d; check(); });
|
|
47
|
+
proc.stderr.on("data", (d) => { err += d; check(); });
|
|
48
|
+
proc.on("error", (e) => finish(e));
|
|
49
|
+
proc.on("exit", () => (done(out, err) ? finish(null, out) : finish(new Error("Chrome exited early."))));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Step 1: the deck's DOM after reveal.js has built the print layout, as static HTML. */
|
|
54
|
+
export async function printLayout(chrome, url, profile) {
|
|
55
|
+
const dom = await chromeRun(chrome, ["--virtual-time-budget=15000", "--dump-dom", url], profile,
|
|
56
|
+
(o) => /<\/html>\s*$/i.test(o), 60000);
|
|
57
|
+
if (!/class="[^"]*pdf-page/.test(dom)) throw new Error("reveal.js did not build the print layout.");
|
|
58
|
+
return "<!doctype html>\n" + dom.replace(/<script\b[\s\S]*?<\/script>/gi, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Step 2: print a (static) page to a PDF file. */
|
|
62
|
+
export async function printPdf(chrome, url, out, profile) {
|
|
63
|
+
try { fs.rmSync(out, { force: true }); } catch {}
|
|
64
|
+
await chromeRun(chrome, ["--no-pdf-header-footer", `--print-to-pdf=${out}`, url], profile,
|
|
65
|
+
(_o, e) => /bytes written to file/.test(e), 90000);
|
|
66
|
+
if (!fs.existsSync(out)) throw new Error("Chrome did not write the PDF.");
|
|
67
|
+
return out;
|
|
68
|
+
}
|