kittyhtml 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kyle Kukshtel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # kittyhtml
2
+
3
+ Render HTML to an image and display it inline in a graphics-capable terminal (Kitty, WezTerm, Ghostty, iTerm2).
4
+
5
+ This is **not** a headless browser. It's a thin CLI that pipes HTML through [DropFlow](https://github.com/chearon/dropflow) (a real CSS layout engine, no JS/no Chromium) to a PNG, then emits the Kitty graphics protocol or iTerm2 inline-image protocol on stdout.
6
+
7
+ Built for AI agents that have something nice to show you — a styled report, a small table, a card — without taking over your screen with a browser.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ # from this directory, until published
13
+ npm install
14
+ npm link # exposes the `kittyhtml` binary on your PATH
15
+ ```
16
+
17
+ Requires Node 20+. Pulls in [`canvas`](https://www.npmjs.com/package/canvas) (native build) and `dropflow`.
18
+
19
+ ## Use
20
+
21
+ ```sh
22
+ kittyhtml --demo # bundled demo page
23
+ echo '<h1>hi</h1>' | kittyhtml --width 400
24
+ kittyhtml report.html --scale 2 -o report.png # write PNG to file
25
+ ```
26
+
27
+ ### Options
28
+
29
+ | flag | default | description |
30
+ |---|---|---|
31
+ | `--width N` | `800` | viewport width in CSS px |
32
+ | `--height N` | *auto-fit* | fixed canvas height |
33
+ | `--scale N` | `1` | pixel ratio (try `2` for retina-sharp text) |
34
+ | `--background CSS` | — | fill canvas before painting, e.g. `#fff` |
35
+ | `--format auto\|kitty\|iterm2` | `auto` | output protocol; auto-detect from `$TERM`/`$TERM_PROGRAM` |
36
+ | `--out, -o PATH` | — | write PNG to file (use `-` for raw PNG on stdout) |
37
+ | `--demo` | — | render the bundled demo page |
38
+
39
+ ### As a library
40
+
41
+ ```js
42
+ import { renderHtml, encode } from 'kittyhtml';
43
+
44
+ const png = await renderHtml('<h1>hello</h1>', { width: 400, scale: 2 });
45
+ process.stdout.write(encode(png, 'kitty'));
46
+ ```
47
+
48
+ ## Releasing
49
+
50
+ Releases publish via GitHub Actions using npm trusted publishing (OIDC, no long-lived token). To cut a release:
51
+
52
+ ```sh
53
+ npm version patch # or minor / major — bumps package.json and tags
54
+ git push --follow-tags
55
+ ```
56
+
57
+ The `Publish to npm` workflow fires on the `v*` tag, exchanges a GitHub OIDC token with npm for a one-shot publish token, and publishes with `--provenance` so each release carries a Sigstore attestation linking it back to the source commit.
58
+
59
+ ## CSS caveats
60
+
61
+ DropFlow implements a serious subset of CSS but isn't a browser. Things to know when writing HTML for it (as of DropFlow 0.6.x):
62
+
63
+ - Use the longhand `background-color`, not the `background` shorthand.
64
+ - `max-width` / `min-width` aren't supported yet — use `width`.
65
+ - `list-style` markers don't render; use `&bull;` or numbers inline.
66
+ - `border-radius`, `box-shadow`, `transform`, and `position: absolute/fixed` aren't supported yet.
67
+ - Body background doesn't propagate to the canvas — set the background on a wrapper element, or pass `--background <css>` to fill the canvas.
68
+
69
+ See the [DropFlow README](https://github.com/chearon/dropflow#supported-css-rules) for the full support matrix.
70
+
71
+ ## Fonts
72
+
73
+ First run fetches Noto fonts from a CDN via DropFlow's bundled `register-noto-fonts.js`. Subsequent renders reuse what was loaded. Bundled offline fonts are on the roadmap.
74
+
75
+ ## Claude Code skill
76
+
77
+ A bundled skill lets Claude Code render output as a styled inline image when you ask for it as "kittyhtml" or "khtml":
78
+
79
+ ```sh
80
+ mkdir -p ~/.claude/skills
81
+ cp -r skill/kittyhtml ~/.claude/skills/kittyhtml
82
+ ```
83
+
84
+ Then in any Claude Code session: *"give me this report as kittyhtml"* — the agent will generate DropFlow-compatible HTML and pipe it through this CLI. The skill is narrow on purpose; it only triggers on those keywords.
85
+
86
+ ## How agents should use it
87
+
88
+ If you're an AI agent on a host with `kittyhtml` installed and the user is on a graphics-capable terminal, pipe your HTML through it instead of dumping markup as text:
89
+
90
+ ```sh
91
+ echo "$HTML" | kittyhtml --width 700 --scale 2
92
+ ```
93
+
94
+ The image is one frame in the scrollback — no popups, no new windows.
Binary file
Binary file
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body style="font-family: 'Noto Sans', sans-serif; margin: 0; padding: 0; background-color: #f4f4f5; color: #18181b;">
4
+ <div style="background-color: #f4f4f5; padding: 32px 0;">
5
+ <div style="width: 720px; margin: 0 auto;">
6
+
7
+ <div style="background-color: #ffffff; border: 1px solid #e4e4e7; padding: 28px 32px;">
8
+ <div style="font-size: 11px; color: #71717a; font-weight: 700;">KITTYHTML &middot; DEMO</div>
9
+ <div style="font-size: 26px; font-weight: 700; padding-top: 6px;">HTML, in your terminal.</div>
10
+ <div style="font-size: 14px; color: #52525b; padding-top: 4px;">Rendered by DropFlow, displayed via the Kitty graphics protocol.</div>
11
+
12
+ <div style="padding-top: 18px; font-size: 14px; line-height: 1.6;">
13
+ No Chromium, no headless browser. Just CSS layout to a PNG, base64-streamed into your terminal as one inline image. Useful when an AI agent has something to show you that reads better as a page than as plain text.
14
+ </div>
15
+
16
+ <div style="font-size: 14px; font-weight: 700; padding-top: 22px; padding-bottom: 6px;">What this is good for</div>
17
+ <div style="font-size: 14px; line-height: 1.7; padding-left: 4px;">
18
+ <div>&bull;&nbsp;&nbsp;Inline reports, tables, and summaries from agents.</div>
19
+ <div>&bull;&nbsp;&nbsp;Quick previews of email or marketing copy.</div>
20
+ <div>&bull;&nbsp;&nbsp;Lightweight diagrams that compose better in HTML than in ASCII.</div>
21
+ </div>
22
+
23
+ <div style="margin-top: 20px; background-color: #18181b; color: #e4e4e7; padding: 14px 16px; font-family: 'Noto Sans Mono', monospace; font-size: 13px;">
24
+ $ echo '&lt;h1&gt;hi&lt;/h1&gt;' | kittyhtml
25
+ </div>
26
+
27
+ <div style="font-size: 12px; color: #71717a; padding-top: 18px;">
28
+ Tip: try <span style="font-family: 'Noto Sans Mono', monospace; color: #18181b;">--scale 2</span> for sharper text on high-DPI displays.
29
+ </div>
30
+ </div>
31
+
32
+ </div>
33
+ </div>
34
+ </body>
35
+ </html>
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "kittyhtml",
3
+ "version": "0.1.0",
4
+ "description": "Render HTML to an image and display it inline in Kitty/iTerm2-capable terminals. No browser — CSS layout via DropFlow.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kittyhtml": "./src/cli.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js",
12
+ "./render": "./src/render.js",
13
+ "./protocols": "./src/protocols.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "assets/fonts",
18
+ "skill",
19
+ "examples/demo.html",
20
+ "README.md"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "scripts": {
26
+ "demo": "node src/cli.js --demo"
27
+ },
28
+ "dependencies": {
29
+ "canvas": "^2.11.2",
30
+ "dropflow": "^0.6.1"
31
+ },
32
+ "keywords": [
33
+ "kitty",
34
+ "iterm2",
35
+ "terminal",
36
+ "graphics",
37
+ "html",
38
+ "css",
39
+ "render",
40
+ "dropflow",
41
+ "image",
42
+ "cli",
43
+ "ai-agent",
44
+ "claude-code"
45
+ ],
46
+ "author": "Kyle Kukshtel <kyle.kukshtel@gmail.com>",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/kkukshtel/kittyhtml.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/kkukshtel/kittyhtml/issues"
54
+ },
55
+ "homepage": "https://github.com/kkukshtel/kittyhtml#readme"
56
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: kittyhtml
3
+ description: Render output as an inline terminal image using the `kittyhtml` CLI. ONLY use when the user explicitly asks for output as "kittyhtml" or "khtml" (e.g. "show me this as kittyhtml", "as khtml please", "render in kittyhtml"). Do NOT use for general HTML, web, or browser-related tasks.
4
+ ---
5
+
6
+ # kittyhtml output format
7
+
8
+ The user has asked for output as **kittyhtml** or **khtml** — they want the result rendered as a styled HTML page and displayed inline in their terminal as an image, via the `kittyhtml` CLI.
9
+
10
+ ## What to do
11
+
12
+ 1. **Verify the tool is installed**: `command -v kittyhtml` (one-time check per session). If missing, tell the user to run `npm install -g kittyhtml` and stop.
13
+
14
+ 2. **Generate HTML** that represents the content the user asked about — a styled page, card, table, summary, or whatever fits the request. Keep it focused; the rendered image should fit on one screen.
15
+
16
+ 3. **Pipe it through the CLI**:
17
+ ```sh
18
+ cat <<'HTML' | kittyhtml --width 700 --scale 2
19
+ <!DOCTYPE html>
20
+ <html><body>...</body></html>
21
+ HTML
22
+ ```
23
+
24
+ Use `--scale 2` for crisp text on retina/HiDPI. Use `--width 600`–`800` for content that should feel "page-sized." Use a smaller width (`--width 400`) for compact card-like output.
25
+
26
+ 4. After piping, write a one-line confirmation (e.g. "Rendered above."). The image is the deliverable; don't restate its contents in text.
27
+
28
+ ## CSS rules — DropFlow subset
29
+
30
+ DropFlow is a real CSS layout engine but it's not a browser. Stick to this subset:
31
+
32
+ - **Use `background-color`, NOT the `background` shorthand.** The shorthand is silently dropped.
33
+ - **Use `width: Npx`, NOT `max-width`.** `max-width` / `min-width` aren't implemented.
34
+ - **No `list-style` markers.** Don't use `<ul><li>` and expect bullets. Use `<div>&bull;&nbsp;&nbsp;item</div>` or numbered prefixes.
35
+ - **No `border-radius`, `box-shadow`, `transform`, `position: absolute/fixed`.** Square corners only. Use `border: 1px solid #color;` for definition.
36
+ - **`<body>` background does NOT propagate to the canvas.** Wrap content in an outer `<div style="background-color: #fff; padding: ...">` to fill the image.
37
+ - **Only inline `style` attributes work** — no `<style>` blocks, no classes.
38
+ - **Fonts available**: `Noto Sans` (default), `Noto Sans Mono` for code.
39
+
40
+ ## Template that's known to render well
41
+
42
+ ```html
43
+ <!DOCTYPE html>
44
+ <html>
45
+ <body style="font-family: 'Noto Sans', sans-serif; margin: 0; padding: 0; color: #18181b;">
46
+ <div style="background-color: #f4f4f5; padding: 24px 0;">
47
+ <div style="width: 640px; margin: 0 auto;">
48
+ <div style="background-color: #ffffff; border: 1px solid #e4e4e7; padding: 24px 28px;">
49
+ <!-- content here -->
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </body>
54
+ </html>
55
+ ```
56
+
57
+ ## When NOT to use this skill
58
+
59
+ - The user wants real HTML to save to a file or open in a browser → write HTML normally.
60
+ - The user is asking how `kittyhtml` works → answer the question directly.
61
+ - The user wants a screenshot of a real web page → this isn't a browser; suggest a headless-Chrome tool instead.
62
+ - The user hasn't said "kittyhtml" or "khtml" → do not invoke; produce regular output.
package/src/cli.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { renderHtml } from './render.js';
6
+ import { encode, detectTerminal } from './protocols.js';
7
+
8
+ const HELP = `kittyhtml — render HTML to an image and display it inline in the terminal.
9
+
10
+ USAGE
11
+ kittyhtml [FILE] [options]
12
+ cat page.html | kittyhtml [options]
13
+ kittyhtml --demo
14
+
15
+ OPTIONS
16
+ --width N Viewport width in CSS px (default 800).
17
+ --height N Fixed canvas height; omit to auto-fit content.
18
+ --scale N Pixel ratio for crisper output (default 1; try 2).
19
+ --background CSS Fill canvas background before painting (e.g. "#fff").
20
+ --format FMT Output protocol: auto | kitty | iterm2 (default auto).
21
+ --out, -o PATH Write PNG to PATH instead of stdout. ("-" = stdout PNG)
22
+ --demo Render a bundled demo page.
23
+ --help, -h Show this help.
24
+
25
+ EXAMPLES
26
+ kittyhtml --demo
27
+ echo '<h1>hi</h1>' | kittyhtml --width 400
28
+ kittyhtml report.html --scale 2 -o report.png
29
+ `;
30
+
31
+ function parseArgs(argv) {
32
+ const opts = {
33
+ width: 800,
34
+ height: null,
35
+ scale: 1,
36
+ background: null,
37
+ format: 'auto',
38
+ out: null,
39
+ demo: false,
40
+ file: null,
41
+ };
42
+ const positional = [];
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ const next = () => {
46
+ const v = argv[++i];
47
+ if (v === undefined) {
48
+ process.stderr.write(`kittyhtml: missing value for ${a}\n`);
49
+ process.exit(2);
50
+ }
51
+ return v;
52
+ };
53
+ switch (a) {
54
+ case '--demo': opts.demo = true; break;
55
+ case '--width': opts.width = Number(next()); break;
56
+ case '--height': opts.height = Number(next()); break;
57
+ case '--scale': opts.scale = Number(next()); break;
58
+ case '--background': case '--bg': opts.background = next(); break;
59
+ case '--format': opts.format = next(); break;
60
+ case '--out': case '-o': opts.out = next(); break;
61
+ case '--help': case '-h': process.stdout.write(HELP); process.exit(0);
62
+ default:
63
+ if (a.startsWith('-')) {
64
+ process.stderr.write(`kittyhtml: unknown option ${a}\n`);
65
+ process.exit(2);
66
+ }
67
+ positional.push(a);
68
+ }
69
+ }
70
+ if (positional.length > 1) {
71
+ process.stderr.write(`kittyhtml: too many positional arguments\n`);
72
+ process.exit(2);
73
+ }
74
+ if (positional[0]) opts.file = positional[0];
75
+ return opts;
76
+ }
77
+
78
+ function loadDemoHtml() {
79
+ const here = dirname(fileURLToPath(import.meta.url));
80
+ return readFileSync(join(here, '..', 'examples', 'demo.html'), 'utf8');
81
+ }
82
+
83
+ async function main() {
84
+ const opts = parseArgs(process.argv.slice(2));
85
+
86
+ let html;
87
+ if (opts.demo) {
88
+ html = loadDemoHtml();
89
+ } else if (opts.file) {
90
+ html = readFileSync(opts.file, 'utf8');
91
+ } else if (!process.stdin.isTTY) {
92
+ html = readFileSync(0, 'utf8');
93
+ } else {
94
+ process.stdout.write(HELP);
95
+ process.exit(1);
96
+ }
97
+
98
+ const png = await renderHtml(html, opts);
99
+
100
+ if (opts.out) {
101
+ if (opts.out === '-') {
102
+ process.stdout.write(png);
103
+ } else {
104
+ writeFileSync(opts.out, png);
105
+ process.stderr.write(`kittyhtml: wrote ${opts.out} (${png.length} bytes)\n`);
106
+ }
107
+ return;
108
+ }
109
+
110
+ const encoded = encode(png, opts.format);
111
+ if (encoded == null) {
112
+ const term = process.env.TERM || '';
113
+ const tp = process.env.TERM_PROGRAM || '';
114
+ process.stderr.write(
115
+ `kittyhtml: no graphics-capable terminal detected (TERM=${term}, TERM_PROGRAM=${tp}).\n` +
116
+ ` Use --format kitty|iterm2 to force, or --out FILE to write a PNG.\n`,
117
+ );
118
+ process.exit(2);
119
+ }
120
+ process.stdout.write(encoded);
121
+ }
122
+
123
+ main().catch(err => {
124
+ process.stderr.write(`kittyhtml: ${err.stack || err.message}\n`);
125
+ process.exit(1);
126
+ });
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { renderHtml } from './render.js';
2
+ export { encode, encodeKitty, encodeIterm2, detectTerminal } from './protocols.js';
@@ -0,0 +1,74 @@
1
+ const ESC = '\x1b';
2
+ const ST = '\x1b\\';
3
+ const BEL = '\x07';
4
+
5
+ /**
6
+ * Detect a terminal graphics protocol from the environment.
7
+ * Returns 'kitty' | 'iterm2' | null.
8
+ */
9
+ export function detectTerminal(env = process.env) {
10
+ if (env.KITTY_WINDOW_ID || env.TERM === 'xterm-kitty') return 'kitty';
11
+ if (env.TERM_PROGRAM === 'WezTerm') return 'kitty';
12
+ if (env.TERM === 'xterm-ghostty' || env.TERM_PROGRAM === 'ghostty') return 'kitty';
13
+ if (env.TERM_PROGRAM === 'iTerm.app' || env.LC_TERMINAL === 'iTerm2') return 'iterm2';
14
+ return null;
15
+ }
16
+
17
+ /**
18
+ * Encode a PNG buffer for the Kitty graphics protocol.
19
+ * https://sw.kovidgoyal.net/kitty/graphics-protocol/
20
+ *
21
+ * The payload is base64-encoded and split into chunks of at most 4096 bytes.
22
+ * Only the first chunk carries the action/format keys; subsequent chunks carry
23
+ * only `m=1` (more) or `m=0` (last).
24
+ */
25
+ export function encodeKitty(pngBuffer) {
26
+ const b64 = pngBuffer.toString('base64');
27
+ const chunkSize = 4096;
28
+
29
+ if (b64.length <= chunkSize) {
30
+ return `${ESC}_Ga=T,f=100;${b64}${ST}\n`;
31
+ }
32
+
33
+ let out = '';
34
+ let i = 0;
35
+ let first = true;
36
+ while (i < b64.length) {
37
+ const slice = b64.slice(i, i + chunkSize);
38
+ i += chunkSize;
39
+ const more = i < b64.length ? 1 : 0;
40
+ if (first) {
41
+ out += `${ESC}_Ga=T,f=100,m=${more};${slice}${ST}`;
42
+ first = false;
43
+ } else {
44
+ out += `${ESC}_Gm=${more};${slice}${ST}`;
45
+ }
46
+ }
47
+ return out + '\n';
48
+ }
49
+
50
+ /**
51
+ * Encode a PNG buffer for the iTerm2 inline image protocol.
52
+ * https://iterm2.com/documentation-images.html
53
+ */
54
+ export function encodeIterm2(pngBuffer) {
55
+ const b64 = pngBuffer.toString('base64');
56
+ return `${ESC}]1337;File=inline=1;size=${pngBuffer.length}:${b64}${BEL}\n`;
57
+ }
58
+
59
+ /**
60
+ * Encode a PNG for the chosen format. 'auto' uses detectTerminal().
61
+ * Returns null if format is 'auto' and no graphics-capable terminal was detected.
62
+ */
63
+ export function encode(pngBuffer, format = 'auto') {
64
+ let fmt = format;
65
+ if (fmt === 'auto') {
66
+ fmt = detectTerminal();
67
+ if (!fmt) return null;
68
+ }
69
+ switch (fmt) {
70
+ case 'kitty': return encodeKitty(pngBuffer);
71
+ case 'iterm2': return encodeIterm2(pngBuffer);
72
+ default: throw new Error(`Unknown terminal format: ${fmt}`);
73
+ }
74
+ }
package/src/render.js ADDED
@@ -0,0 +1,64 @@
1
+ import * as flow from 'dropflow';
2
+ import parse from 'dropflow/parse.js';
3
+ import { createCanvas } from 'canvas';
4
+
5
+ const FONTS_DIR = new URL('../assets/fonts/', import.meta.url);
6
+ const BUNDLED_FONTS = [
7
+ 'NotoSans-Regular.ttf',
8
+ 'NotoSans-Bold.ttf',
9
+ 'NotoSans-Italic.ttf',
10
+ 'NotoSans-BoldItalic.ttf',
11
+ 'NotoSansMono-Regular.ttf',
12
+ 'NotoSansMono-Bold.ttf',
13
+ ];
14
+
15
+ let fontsReady = false;
16
+ function ensureFonts() {
17
+ if (fontsReady) return;
18
+ for (const file of BUNDLED_FONTS) {
19
+ flow.fonts.add(flow.createFaceFromTablesSync(new URL(file, FONTS_DIR)));
20
+ }
21
+ fontsReady = true;
22
+ }
23
+
24
+ /**
25
+ * Render an HTML string to a PNG buffer using DropFlow.
26
+ *
27
+ * @param {string} html
28
+ * @param {object} [opts]
29
+ * @param {number} [opts.width=800] Viewport width in CSS px before scaling.
30
+ * @param {number|null} [opts.height] Fixed canvas height; if null, auto-fit to content.
31
+ * @param {number} [opts.scale=1] Pixel ratio (2 = retina-sharp).
32
+ * @param {string|null} [opts.background] Optional canvas background fill (CSS color).
33
+ * @returns {Promise<Buffer>} PNG image bytes.
34
+ */
35
+ export async function renderHtml(html, opts = {}) {
36
+ const { width = 800, height = null, scale = 1, background = null } = opts;
37
+ ensureFonts();
38
+
39
+ const root = parse(html);
40
+ await flow.load(root);
41
+
42
+ const pxWidth = Math.max(1, Math.round(width * scale));
43
+ const layout = flow.generate(root);
44
+
45
+ let pxHeight;
46
+ if (height != null) {
47
+ pxHeight = Math.max(1, Math.round(height * scale));
48
+ flow.layout(layout, pxWidth, pxHeight);
49
+ } else {
50
+ flow.layout(layout, pxWidth, 1_000_000);
51
+ const measured = layout.getBorderArea().height;
52
+ pxHeight = Math.max(1, Math.ceil(measured));
53
+ flow.layout(layout, pxWidth, pxHeight);
54
+ }
55
+
56
+ const canvas = createCanvas(pxWidth, pxHeight);
57
+ const ctx = canvas.getContext('2d');
58
+ if (background) {
59
+ ctx.fillStyle = background;
60
+ ctx.fillRect(0, 0, pxWidth, pxHeight);
61
+ }
62
+ flow.paintToCanvas(layout, ctx);
63
+ return canvas.toBuffer('image/png');
64
+ }