autokap 1.6.7 → 1.7.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.
@@ -155,11 +155,14 @@ function attr(s) {
155
155
  }
156
156
  // ── Chrome generator (SVG, for sharp compositing — no Playwright) ──────
157
157
  // Produces a self-contained SVG string rasterizable by @resvg/resvg-js.
158
- /** SF Pro font CSS for embedding inside SVG <defs><style>. No local() resvg only supports url(). */
159
- const SVG_BB_FONT_CSS = `
160
- @font-face{font-family:'SF Pro Text';src:url('${SF_PRO_TEXT_REGULAR}') format('woff2');font-weight:400;font-style:normal}
161
- @font-face{font-family:'SF Pro Text';src:url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2');font-weight:600;font-style:normal}
162
- `;
158
+ // SF Pro fonts for the SVG output are NOT embedded inline Resvg-js 2.6.2
159
+ // does not honor `@font-face url(data:font/woff2;base64,…)` declarations
160
+ // inside an SVG <defs><style> block. They must instead be supplied via the
161
+ // `font.fontFiles` Resvg option, which is the job of
162
+ // `mockup.ts::rasterizeSvg` + `sf-pro-resvg-fonts.ts`. The `font-family`
163
+ // declarations on the <text> elements below remain as a hint for any
164
+ // downstream consumer that DOES honor CSS @font-face (e.g. a future Resvg
165
+ // release, or a different SVG renderer entirely).
163
166
  const SVG_BB_FF = "'SF Pro Text',system-ui,sans-serif";
164
167
  /**
165
168
  * Generate a self-contained SVG string for the Chrome browser bar.
@@ -215,7 +218,6 @@ export function generateChromeBrowserBarSvg(options) {
215
218
  // and hide the colored circles underneath.
216
219
  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${iw} ${REF_H}" fill="none">
217
220
  <defs>
218
- <style>${SVG_BB_FONT_CSS}</style>
219
221
  <linearGradient id="bb_profile_grad" x1="${profileX + 15}" y1="109.5" x2="${profileX + 15}" y2="79.5" gradientUnits="userSpaceOnUse">
220
222
  <stop stop-color="#D5D8E4"/><stop offset="0.45" stop-color="#D1CAD6"/><stop offset="1" stop-color="#B7BAD1"/>
221
223
  </linearGradient>
package/dist/mockup.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import { renderStatusBarBuffer } from './status-bar-render.js';
6
6
  import { generateBrowserBarSvg } from './browser-bar.js';
7
7
  import { computeMockupLayout } from './mockup-html.js';
8
+ import { getSfProResvgFontFiles } from './sf-pro-resvg-fonts.js';
8
9
  import { logger } from './logger.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
@@ -415,11 +416,27 @@ export async function getDeviceFrame(id) {
415
416
  }
416
417
  // ── Sharp Compositing Helpers ──────────────────────────────────────────
417
418
  /** Rasterize an SVG string to a PNG buffer using resvg-js.
418
- * Resolves external image references (e.g. favicon URLs) before rendering. */
419
+ * Resolves external image references (e.g. favicon URLs) before rendering.
420
+ *
421
+ * Font handling: Resvg-js 2.6.2 does NOT read fonts from `@font-face url(data:...)`
422
+ * declarations embedded inside the SVG. They MUST be supplied via `font.fontFiles`
423
+ * with paths to on-disk TTF/OTF files. Without this, browser-bar text falls back
424
+ * to Linux system fonts (DejaVu/Liberation) on Vercel, producing the wrong glyph
425
+ * shapes that the user calls "SF Pro fonts don't load". `getSfProResvgFontFiles`
426
+ * decompresses the SF Pro woff2 base64 data URIs to TTF in `os.tmpdir()` and
427
+ * caches the paths for the lifetime of the function instance.
428
+ * `loadSystemFonts: false` keeps the render deterministic across OSes — a font
429
+ * load failure produces blank text (loud) instead of fallback (silent). */
419
430
  async function rasterizeSvg(svg, width) {
420
431
  const { Resvg } = await import('@resvg/resvg-js');
432
+ const fontFiles = await getSfProResvgFontFiles();
421
433
  const opts = {
422
434
  fitTo: { mode: 'width', value: width },
435
+ font: {
436
+ fontFiles,
437
+ loadSystemFonts: false,
438
+ defaultFontFamily: 'SF Pro Text',
439
+ },
423
440
  };
424
441
  const resvg = new Resvg(svg, opts);
425
442
  // Resolve external image references (e.g. <image href="https://..."/>)
@@ -17,10 +17,13 @@ const FONT_CSS_HTML = `<style>
17
17
  @font-face{font-family:'SF Pro Text';src:local('SF Pro Text'),local('.SFNSText'),url('${SF_PRO_TEXT_REGULAR}') format('woff2');font-weight:400;font-style:normal}
18
18
  @font-face{font-family:'SF Pro Text';src:local('SF Pro Text Semibold'),local('.SFNSText-Semibold'),url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2');font-weight:600;font-style:normal}
19
19
  </style>`;
20
- const FONT_CSS_SVG = `
21
- @font-face{font-family:'SF Pro Text';src:url('${SF_PRO_TEXT_REGULAR}') format('woff2');font-weight:400;font-style:normal}
22
- @font-face{font-family:'SF Pro Text';src:url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2');font-weight:600;font-style:normal}
23
- `;
20
+ // SVG output: fonts are NOT embedded inline. Resvg-js 2.6.2 does not honor
21
+ // `@font-face url(data:font/woff2;base64,…)` declarations inside SVG style
22
+ // blocks; fonts must instead be supplied via `font.fontFiles` to the Resvg
23
+ // constructor. The browser-bar pipeline (mockup.ts::rasterizeSvg) loads SF
24
+ // Pro Text from disk via `sf-pro-resvg-fonts.ts`. The `font-family` hint on
25
+ // the URL <text> element below remains for any consumer that DOES honor
26
+ // CSS @font-face (a future Resvg release, a different SVG renderer, etc.).
24
27
  const FF = "'SF Pro Text',-apple-system,BlinkMacSystemFont,system-ui,sans-serif";
25
28
  // ── Helpers ──────────────────────────────────────────────────────────────
26
29
  function escText(s) {
@@ -88,7 +91,6 @@ export function generateSafariBrowserBarSvg(options) {
88
91
  const inner = buildSafariSvgInner(url, isDark);
89
92
  const vb = SAFARI_TOOLBAR_VIEWBOX;
90
93
  return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="${vb.x} ${vb.y} ${vb.width} ${vb.height}" preserveAspectRatio="none" fill="none">
91
- <defs><style>${FONT_CSS_SVG}</style></defs>
92
94
  ${inner}
93
95
  </svg>`;
94
96
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SF Pro font files on disk for @resvg/resvg-js consumption.
3
+ *
4
+ * Why this exists: Resvg-js 2.6.2 cannot load fonts from in-memory buffers,
5
+ * and it does NOT honor `@font-face url('data:font/woff2;base64,…')`
6
+ * declarations inside an SVG `<defs><style>` block. It only reads fonts
7
+ * from on-disk files (`fontFiles`) or directories (`fontDirs`).
8
+ *
9
+ * The browser-bar rendering pipeline (`generateBrowserBarSvg` →
10
+ * `rasterizeSvg` in mockup.ts) used to rely on the SVG <defs><style>
11
+ * @font-face approach. That silently fell back to system fonts
12
+ * (DejaVu/Liberation on Vercel Linux) — empirically verified by rendering
13
+ * the same SVG with and without `font.fontFiles` and observing the output
14
+ * change only when `fontFiles` is provided.
15
+ *
16
+ * This module decompresses the SF Pro Text Regular/Semibold woff2 data URIs
17
+ * from `sf-pro-fonts.ts` to TTF and writes them to `os.tmpdir()`, returning
18
+ * the file paths so callers can pass them to `new Resvg(svg, { font:
19
+ * { fontFiles: paths, loadSystemFonts: false } })`.
20
+ *
21
+ * Caching: paths are memoized per-process. On Vercel Fluid Compute the
22
+ * instance is reused across requests so the woff2→ttf decompression and
23
+ * disk writes happen at most once per cold start (~200ms total).
24
+ *
25
+ * Concurrency: two requests arriving before the first decompression
26
+ * finishes share a single in-flight Promise (no double work, no
27
+ * partial-file races).
28
+ *
29
+ * Robustness: if /tmp gets wiped between requests on the same instance
30
+ * (rare but possible), `fs.access` detects the missing files and a fresh
31
+ * init re-writes them.
32
+ *
33
+ * Windows-safe: uses `os.tmpdir()` + `path.join` (no hard-coded /tmp/),
34
+ * `fs.chmod` is intentionally skipped (no-op on NTFS anyway), no bash-isms.
35
+ */
36
+ /**
37
+ * Returns the absolute paths to SF Pro Text Regular + Semibold TTF files on
38
+ * disk, suitable for `font.fontFiles` on a `new Resvg(svg, { … })` call.
39
+ *
40
+ * First call: decompresses woff2 → TTF and writes to `os.tmpdir()`. Subsequent
41
+ * calls: returns cached paths in O(2) `fs.access` checks (skipped after a
42
+ * single confirmed lookup per process — see implementation).
43
+ */
44
+ export declare function getSfProResvgFontFiles(): Promise<string[]>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SF Pro font files on disk for @resvg/resvg-js consumption.
3
+ *
4
+ * Why this exists: Resvg-js 2.6.2 cannot load fonts from in-memory buffers,
5
+ * and it does NOT honor `@font-face url('data:font/woff2;base64,…')`
6
+ * declarations inside an SVG `<defs><style>` block. It only reads fonts
7
+ * from on-disk files (`fontFiles`) or directories (`fontDirs`).
8
+ *
9
+ * The browser-bar rendering pipeline (`generateBrowserBarSvg` →
10
+ * `rasterizeSvg` in mockup.ts) used to rely on the SVG <defs><style>
11
+ * @font-face approach. That silently fell back to system fonts
12
+ * (DejaVu/Liberation on Vercel Linux) — empirically verified by rendering
13
+ * the same SVG with and without `font.fontFiles` and observing the output
14
+ * change only when `fontFiles` is provided.
15
+ *
16
+ * This module decompresses the SF Pro Text Regular/Semibold woff2 data URIs
17
+ * from `sf-pro-fonts.ts` to TTF and writes them to `os.tmpdir()`, returning
18
+ * the file paths so callers can pass them to `new Resvg(svg, { font:
19
+ * { fontFiles: paths, loadSystemFonts: false } })`.
20
+ *
21
+ * Caching: paths are memoized per-process. On Vercel Fluid Compute the
22
+ * instance is reused across requests so the woff2→ttf decompression and
23
+ * disk writes happen at most once per cold start (~200ms total).
24
+ *
25
+ * Concurrency: two requests arriving before the first decompression
26
+ * finishes share a single in-flight Promise (no double work, no
27
+ * partial-file races).
28
+ *
29
+ * Robustness: if /tmp gets wiped between requests on the same instance
30
+ * (rare but possible), `fs.access` detects the missing files and a fresh
31
+ * init re-writes them.
32
+ *
33
+ * Windows-safe: uses `os.tmpdir()` + `path.join` (no hard-coded /tmp/),
34
+ * `fs.chmod` is intentionally skipped (no-op on NTFS anyway), no bash-isms.
35
+ */
36
+ import fs from 'node:fs/promises';
37
+ import os from 'node:os';
38
+ import path from 'node:path';
39
+ import { SF_PRO_TEXT_REGULAR, SF_PRO_TEXT_SEMIBOLD } from './sf-pro-fonts.js';
40
+ const FONT_DIR_NAME = 'autokap-sf-pro';
41
+ // Browser bar (Chrome + Safari chrome) uses only SF Pro Text Regular/Semibold.
42
+ // Other variants (Display, Symbols) are consumed by the Satori-based status
43
+ // bar renderer which already handles font embedding through its own
44
+ // in-memory buffer pipeline (see `status-bar-render.ts::getSatoriFonts`).
45
+ const FONTS = [
46
+ { filename: 'SF-Pro-Text-Regular.ttf', dataUri: SF_PRO_TEXT_REGULAR },
47
+ { filename: 'SF-Pro-Text-Semibold.ttf', dataUri: SF_PRO_TEXT_SEMIBOLD },
48
+ ];
49
+ let cachedPaths = null;
50
+ let pendingInit = null;
51
+ async function decompressWoff2DataUri(dataUri) {
52
+ const raw = dataUri.replace(/^data:font\/woff2;base64,/, '');
53
+ const woff2 = Buffer.from(raw, 'base64');
54
+ const { decompress } = await import('wawoff2');
55
+ return Buffer.from(await decompress(woff2));
56
+ }
57
+ async function fileExists(p) {
58
+ try {
59
+ await fs.access(p);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ /**
67
+ * Write a single TTF file atomically. If a concurrent call (same process or
68
+ * a sibling instance sharing the tmpdir) already produced the final file,
69
+ * the second `rename` overwrites identical bytes — still safe.
70
+ *
71
+ * The `.${pid}.partial` suffix prevents Resvg from reading a half-written
72
+ * file if it loads paths while a write is in flight.
73
+ */
74
+ async function ensureFontFile(targetPath, dataUri) {
75
+ if (await fileExists(targetPath))
76
+ return;
77
+ const ttf = await decompressWoff2DataUri(dataUri);
78
+ const tmpPath = `${targetPath}.${process.pid}.partial`;
79
+ await fs.writeFile(tmpPath, ttf);
80
+ try {
81
+ await fs.rename(tmpPath, targetPath);
82
+ }
83
+ catch (err) {
84
+ // Best-effort cleanup of the .partial if rename failed for some reason
85
+ // (e.g. cross-device rename on exotic mounts). Swallow — the rename
86
+ // error is what matters and will surface to the caller.
87
+ await fs.rm(tmpPath, { force: true }).catch(() => { });
88
+ throw err;
89
+ }
90
+ }
91
+ async function initFontFiles() {
92
+ const dir = path.join(os.tmpdir(), FONT_DIR_NAME);
93
+ await fs.mkdir(dir, { recursive: true });
94
+ const paths = FONTS.map((f) => path.join(dir, f.filename));
95
+ await Promise.all(FONTS.map((f, i) => ensureFontFile(paths[i], f.dataUri)));
96
+ return paths;
97
+ }
98
+ /**
99
+ * Returns the absolute paths to SF Pro Text Regular + Semibold TTF files on
100
+ * disk, suitable for `font.fontFiles` on a `new Resvg(svg, { … })` call.
101
+ *
102
+ * First call: decompresses woff2 → TTF and writes to `os.tmpdir()`. Subsequent
103
+ * calls: returns cached paths in O(2) `fs.access` checks (skipped after a
104
+ * single confirmed lookup per process — see implementation).
105
+ */
106
+ export async function getSfProResvgFontFiles() {
107
+ if (cachedPaths) {
108
+ // Defensive: ensure /tmp wasn't wiped under us.
109
+ const allExist = await Promise.all(cachedPaths.map(fileExists));
110
+ if (allExist.every(Boolean))
111
+ return cachedPaths;
112
+ cachedPaths = null;
113
+ pendingInit = null;
114
+ }
115
+ if (!pendingInit) {
116
+ pendingInit = initFontFiles().then((paths) => {
117
+ cachedPaths = paths;
118
+ return paths;
119
+ }, (err) => {
120
+ pendingInit = null;
121
+ throw err;
122
+ });
123
+ }
124
+ return pendingInit;
125
+ }
126
+ //# sourceMappingURL=sf-pro-resvg-fonts.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",