domotion-svg 0.3.2 → 0.4.1

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,6 +155,13 @@ export interface AnimationConfig {
155
155
  * them in every frame's local defs.
156
156
  */
157
157
  sharedDefs?: string;
158
+ /**
159
+ * DM-839: embedded-font `@font-face` rules collected once across all frames
160
+ * (the caller renders each frame with `includeEmbeddedFontCss=false` and
161
+ * passes the accumulated `getEmbeddedFontFaceCss()` here). Injected into the
162
+ * top-level `<style>` so the base64 font bytes appear once, not per frame.
163
+ */
164
+ fontFaceCss?: string;
158
165
  /**
159
166
  * Optional cursor / click overlay (DM-277). Renders a macOS-style cursor
160
167
  * moving along the script timeline with QuickTime-style click pulses.
@@ -222,7 +222,7 @@ export function generateAnimatedSvg(config) {
222
222
  <clipPath id="viewport-clip"><rect width="${width}" height="${height}" /></clipPath>${sharedDefsMarkup}
223
223
  </defs>
224
224
  <style>
225
- :root { --scene-dur: ${totalSec.toFixed(2)}s; }
225
+ ${config.fontFaceCss != null && config.fontFaceCss !== "" ? config.fontFaceCss + "\n" : ""} :root { --scene-dur: ${totalSec.toFixed(2)}s; }
226
226
  .f { opacity: 0; visibility: hidden; }
227
227
  ${keyframes.join("\n")}${animationCss}${cullCss === "" ? "" : "\n" + cullCss}
228
228
  </style>
@@ -444,7 +444,7 @@ function composeMergedSvg(config, frameTiming, totalSec) {
444
444
  <clipPath id="viewport-clip"><rect width="${width}" height="${height}" /></clipPath>${sharedDefsMarkup}
445
445
  </defs>
446
446
  <style>
447
- :root { --scene-dur: ${totalSec.toFixed(2)}s; }
447
+ ${config.fontFaceCss != null && config.fontFaceCss !== "" ? config.fontFaceCss + "\n" : ""} :root { --scene-dur: ${totalSec.toFixed(2)}s; }
448
448
  ${css}${animationCss}${cullCss === "" ? "" : "\n" + cullCss}
449
449
  </style>
450
450
  <g clip-path="url(#viewport-clip)">
@@ -10,7 +10,7 @@ import { elementTreeToSvg } from "../render/element-tree-to-svg.js";
10
10
  import { embedRemoteImages } from "./embed.js";
11
11
  import { resizeEmbeddedImages } from "../tree-ops/resize-embedded-images.js";
12
12
  import { rasterizeConicGradients } from "../render/conic-raster.js";
13
- import { registerLocalFontAlias, registerWebfont } from "../render/text-to-path.js";
13
+ import { clearEmbeddedFonts, registerLocalFontAlias, registerWebfont } from "../render/text-to-path.js";
14
14
  import { CAPTURE_SCRIPT } from "./script.generated.js";
15
15
  import { rasterizeBitmapGlyphs } from "./emoji.js";
16
16
  import { _resetLastCaptureWarnings } from "./warnings.js";
@@ -141,6 +141,10 @@ export class DemoRecorder {
141
141
  // tree contains no conic content; otherwise the renderer (DM-550) emits
142
142
  // <pattern><image href="data:..."/></pattern> instead of dropping the layer.
143
143
  await rasterizeConicGradients(tree, { hiDPIFactor: this.embedRemoteImagesHiDPIFactor });
144
+ // DM-839: reset the embedded-font builder so this capture's `@font-face`
145
+ // block contains only its own fonts (the renderer repopulates it during
146
+ // elementTreeToSvg, which emits the CSS into this frame's <defs>).
147
+ clearEmbeddedFonts();
144
148
  return elementTreeToSvg(tree, this.width, this.height, idPrefix, true, this.embedRemoteImagesHiDPIFactor ?? 2);
145
149
  }
146
150
  /**
@@ -165,6 +169,7 @@ export class DemoRecorder {
165
169
  }
166
170
  // DM-549: rasterize conic-gradient layers — see captureCurrent above.
167
171
  await rasterizeConicGradients(tree, { hiDPIFactor: this.embedRemoteImagesHiDPIFactor });
172
+ clearEmbeddedFonts(); // DM-839: see captureCurrent
168
173
  const svgContent = elementTreeToSvg(tree, this.width, pageHeight, idPrefix, true, this.embedRemoteImagesHiDPIFactor ?? 2);
169
174
  return { svgContent, pageHeight };
170
175
  }
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { dirname, resolve } from "node:path";
10
10
  import { parseArgs } from "node:util";
11
- import { captureElementTree, clearWebfonts, composeScrollSvg, cullElementsOutsideViewBox, elementTreeToSvg, executeScrollPattern, generateAnimatedSvg, launchChromium, optimizeSvg, parseScrollPattern, } from "../index.js";
11
+ import { captureElementTree, clearEmbeddedFonts, clearWebfonts, composeScrollSvg, cullElementsOutsideViewBox, elementTreeToSvg, executeScrollPattern, generateAnimatedSvg, getEmbeddedFontFaceCss, launchChromium, optimizeSvg, parseScrollPattern, } from "../index.js";
12
12
  import { attachWebfontTracker, discoverAndRegisterWebfonts } from "../capture/index.js";
13
13
  import { applyReadyWaits, isSvgzPath, loadInputIntoPage, makeLogger, resolveOutputPath, timed, writeOutput, } from "./common.js";
14
14
  export async function runAnimate(args, help) {
@@ -61,6 +61,11 @@ export async function runAnimate(args, help) {
61
61
  // same registry. Multiple frames declaring the same family register
62
62
  // multiple variants and the resolver picks the closest weight/style.
63
63
  clearWebfonts();
64
+ // DM-839: embedded-font is the default render mode. Reset the builder once
65
+ // here; each frame renders with includeEmbeddedFontCss=false (below) and we
66
+ // collect the deduped @font-face block once into the animator's top-level
67
+ // <style> after the loop — so the base64 font bytes appear once, not per frame.
68
+ clearEmbeddedFonts();
64
69
  // One tracker for the whole animate run — fonts fetched by any frame
65
70
  // get accumulated, and we deduplicate URLs inside discoverAndRegister.
66
71
  const tracker = attachWebfontTracker(page);
@@ -156,7 +161,7 @@ export async function runAnimate(args, help) {
156
161
  const totalDurationMs = cfg.frames.reduce((sum, f) => sum + f.duration + (f.transition?.type === "cut" ? 0 : (f.transition?.duration ?? 300)), 0);
157
162
  const result = cullElementsOutsideViewBox(tree, cfg.width, cfg.height, resolvedAnimations, frameStartMs, totalDurationMs);
158
163
  frameCullCss = result.css;
159
- svgContent = elementTreeToSvg(tree, cfg.width, cfg.height, `f${i}-`);
164
+ svgContent = elementTreeToSvg(tree, cfg.width, cfg.height, `f${i}-`, true, 2, false);
160
165
  }
161
166
  // Resolve SVG-kind overlays: read each `src` from disk, namespace its
162
167
  // ids, and replace with `innerSvg`. Other overlay kinds pass through
@@ -172,7 +177,10 @@ export async function runAnimate(args, help) {
172
177
  });
173
178
  }
174
179
  tracker.detach();
175
- let svg = await timed(log, `Composed animated SVG (${cfg.frames.length} frames)`, () => Promise.resolve(generateAnimatedSvg({ width: cfg.width, height: cfg.height, frames })));
180
+ // DM-839: collect the embedded-font @font-face rules accumulated across all
181
+ // frames once, for the animator's top-level <style>.
182
+ const fontFaceCss = getEmbeddedFontFaceCss();
183
+ let svg = await timed(log, `Composed animated SVG (${cfg.frames.length} frames)`, () => Promise.resolve(generateAnimatedSvg({ width: cfg.width, height: cfg.height, frames, fontFaceCss })));
176
184
  // svgz is auto-detected from the output filename; implies --optimize
177
185
  // unless --no-optimize was passed.
178
186
  const outputArg = values.output ?? cfg.output;
@@ -6,7 +6,7 @@
6
6
  * scroll machinery.
7
7
  */
8
8
  import { parseArgs } from "node:util";
9
- import { captureElementTree, clearWebfonts, composeScrollSvg, cullElementsOutsideViewBox, elementTreeToSvg, executeScrollPattern, launchChromium, logCaptureWarnings, optimizeSvg, parseScrollPattern, wrapSvg, } from "../index.js";
9
+ import { captureElementTree, clearEmbeddedFonts, clearWebfonts, composeScrollSvg, cullElementsOutsideViewBox, elementTreeToSvg, executeScrollPattern, launchChromium, logCaptureWarnings, optimizeSvg, parseScrollPattern, wrapSvg, } from "../index.js";
10
10
  import { attachWebfontTracker, discoverAndRegisterWebfonts } from "../capture/index.js";
11
11
  import { applyReadyWaits, isSvgzPath, loadInputIntoPage, makeLogger, parseColorScheme, parseIntFlag, parseTuple, resolveOutputPath, timed, writeOutput, } from "./common.js";
12
12
  export async function runCapture(args, help) {
@@ -139,6 +139,7 @@ export async function runCapture(args, help) {
139
139
  // so this is a defense-in-depth pass for the position:fixed-descendant
140
140
  // escape cases where an off-viewport ancestor still gets captured.
141
141
  cullElementsOutsideViewBox(tree, clip[2], clip[3], undefined, 0, 1);
142
+ clearEmbeddedFonts(); // DM-839: reset embedded-font builder before this single-frame render
142
143
  const inner = elementTreeToSvg(tree, clip[2], clip[3]);
143
144
  svg = wrapSvg(inner, clip[2], clip[3]);
144
145
  }
@@ -67,7 +67,17 @@ includeGlyphDefs?: boolean,
67
67
  * `resizeEmbeddedImages` for the same tree, or the lookup misses and
68
68
  * the renderer falls back to the source-resolution data URI. Default 2.
69
69
  */
70
- hiDPIFactor?: number): string;
70
+ hiDPIFactor?: number,
71
+ /**
72
+ * DM-839: when true, emit the embedded-font `@font-face` rules
73
+ * (`getEmbeddedFontFaceCss()`) as a `<style>` inside this frame's `<defs>`.
74
+ * Defaults to `includeGlyphDefs` — single-frame standalone producers
75
+ * (capture, CLI, the test harness) own their top-level defs and so should
76
+ * carry their font CSS here. Multi-frame producers (animator, scroll
77
+ * composer) pass `false` and collect the CSS once at the top level instead,
78
+ * so the (potentially large) base64 font bytes aren't duplicated per frame.
79
+ */
80
+ includeEmbeddedFontCss?: boolean): string;
71
81
  /**
72
82
  * Rewrite a captured `<mask>` element's `outerHTML` so it can be safely
73
83
  * inlined in the output SVG's `<defs>`. The mask's own `id` becomes
@@ -4,7 +4,7 @@
4
4
  * Uses Playwright to inspect DOM elements and recreate them as native SVG.
5
5
  */
6
6
  import { renderSingleLineText, renderMultiSegmentText, renderMultiLineText, renderInputText } from "./text.js";
7
- import { getGlyphDefs, measureLastGlyphRsb } from "./text-to-path.js";
7
+ import { getEmbeddedFontFaceCss, getGlyphDefs, measureLastGlyphRsb } from "./text-to-path.js";
8
8
  import { renderFormControl } from "./form-controls.js";
9
9
  import { r, esc, stopFmt } from "./format.js";
10
10
  import { parseColor, colorStr, sameColor } from "./colors.js";
@@ -104,7 +104,17 @@ includeGlyphDefs = true,
104
104
  * `resizeEmbeddedImages` for the same tree, or the lookup misses and
105
105
  * the renderer falls back to the source-resolution data URI. Default 2.
106
106
  */
107
- hiDPIFactor = 2) {
107
+ hiDPIFactor = 2,
108
+ /**
109
+ * DM-839: when true, emit the embedded-font `@font-face` rules
110
+ * (`getEmbeddedFontFaceCss()`) as a `<style>` inside this frame's `<defs>`.
111
+ * Defaults to `includeGlyphDefs` — single-frame standalone producers
112
+ * (capture, CLI, the test harness) own their top-level defs and so should
113
+ * carry their font CSS here. Multi-frame producers (animator, scroll
114
+ * composer) pass `false` and collect the CSS once at the top level instead,
115
+ * so the (potentially large) base64 font bytes aren't duplicated per frame.
116
+ */
117
+ includeEmbeddedFontCss = includeGlyphDefs) {
108
118
  setActiveHiDPIFactor(hiDPIFactor);
109
119
  const svgParts = [];
110
120
  const defsParts = [];
@@ -2956,7 +2966,13 @@ hiDPIFactor = 2) {
2956
2966
  // `getEmbeddedFontFaceCss()` themselves once at the top level (see how
2957
2967
  // `composeScrollSvg` injects it into the outer <style>).
2958
2968
  const glyphDefsMarkup = includeGlyphDefs ? getGlyphDefs() : "";
2959
- const allDefs = defsParts.join("") + glyphDefsMarkup;
2969
+ // DM-839: embedded-font `@font-face` rules for the text runs rendered above
2970
+ // (empty in paths mode, or when no run was embeddable). A `<style>` inside
2971
+ // `<defs>` is valid SVG. Single-frame producers emit it here; multi-frame
2972
+ // producers pass includeEmbeddedFontCss=false and inject once at the top.
2973
+ const embeddedFontCss = includeEmbeddedFontCss ? getEmbeddedFontFaceCss() : "";
2974
+ const fontStyleMarkup = embeddedFontCss !== "" ? `<style>${embeddedFontCss}</style>` : "";
2975
+ const allDefs = defsParts.join("") + glyphDefsMarkup + fontStyleMarkup;
2960
2976
  const defs = allDefs !== "" ? ` <defs>${allDefs}</defs>\n` : "";
2961
2977
  return defs + svgParts.join("\n");
2962
2978
  }
@@ -1,2 +1,2 @@
1
1
  export { elementTreeToSvg, wrapSvg, } from "./element-tree-to-svg.js";
2
- export { getGlyphDefs, clearGlyphDefs, registerWebfont, clearWebfonts, } from "./text-to-path.js";
2
+ export { getGlyphDefs, clearGlyphDefs, registerWebfont, clearWebfonts, setRenderTextMode, getRenderTextMode, clearEmbeddedFonts, getEmbeddedFontFaceCss, type RenderTextMode, } from "./text-to-path.js";
@@ -1,4 +1,8 @@
1
1
  // Render-side public surface. Pure functions that take a captured element
2
2
  // tree (or fragments of one) and emit SVG markup.
3
3
  export { elementTreeToSvg, wrapSvg, } from "./element-tree-to-svg.js";
4
- export { getGlyphDefs, clearGlyphDefs, registerWebfont, clearWebfonts, } from "./text-to-path.js";
4
+ export { getGlyphDefs, clearGlyphDefs, registerWebfont, clearWebfonts,
5
+ // DM-839: embedded-font is the default render mode; expose the controls so
6
+ // consumers can opt back into per-pixel-faithful `paths` mode and manage the
7
+ // embedded-font lifecycle.
8
+ setRenderTextMode, getRenderTextMode, clearEmbeddedFonts, getEmbeddedFontFaceCss, } from "./text-to-path.js";
@@ -109,6 +109,12 @@ export declare function __pickWebfontVariantMetaForCodepointForTest(family: stri
109
109
  /** Drop all registered webfonts. Call at the start of a fresh capture run. */
110
110
  export declare function clearWebfonts(): void;
111
111
  export declare function registerLocalFontAlias(family: string, resolvedKey: string, weight?: number, italic?: boolean): void;
112
+ /** Test-only window into the platform path resolver (DM-258). */
113
+ export declare function __resolveFontSpecForTest(key: string): {
114
+ path: string;
115
+ postscriptName?: string;
116
+ extractor?: string;
117
+ } | null;
112
118
  /**
113
119
  * Ordered list of fallback font keys to try when the primary font lacks a
114
120
  * glyph for `codepoint`. Caller iterates the chain and picks the first font
@@ -139,7 +145,27 @@ export declare function registerLocalFontAlias(family: string, resolvedKey: stri
139
145
  * "ja" / "ja-JP" → hiragino-jp (PingFang has no JP subfont on macOS)
140
146
  */
141
147
  export declare function pingfangKeyForLang(lang: string | undefined): string | null;
148
+ /**
149
+ * Linux fallback chain (DM-259) — calibrated to what Chromium-on-Linux paints
150
+ * in the Playwright `*-noble` CI image, measured via CDP
151
+ * `CSS.getPlatformFontsForNode` (tools/probe-fallbacks-linux.mjs). Returns
152
+ * logical keys that `LINUX_FONT_PATHS` maps to the image's real faces
153
+ * (Liberation / WenQuanYi / FreeFont / Loma / IPAGothic). As on macOS, the
154
+ * caller has already tried the primary font, so what reaches here is the
155
+ * residue the primary lacks. The comment after each branch names the face the
156
+ * probe showed Chromium using for that block.
157
+ */
158
+ export declare function linuxFallbackChain(codepoint: number, primaryKey?: string, _lang?: string): string[];
142
159
  export declare function fallbackFontChain(codepoint: number, primaryKey?: string, lang?: string): string[];
160
+ /**
161
+ * macOS (CoreText) fallback chain — reverse-engineered from Chromium-on-macOS
162
+ * painted widths (DM-241 / DM-256 / DM-257 / …). Exported so the macOS-
163
+ * calibration unit tests assert it directly: the suite runs on Linux in CI,
164
+ * where `fallbackFontChain` dispatches to `linuxFallbackChain`, so those tests
165
+ * must call this function (not `fallbackFontChain`) to validate macOS routing
166
+ * regardless of the host platform (DM-842).
167
+ */
168
+ export declare function darwinFallbackChain(codepoint: number, primaryKey?: string, lang?: string): string[];
143
169
  /** @deprecated Single-key wrapper for back-compat — prefer `fallbackFontChain`. */
144
170
  export declare function fallbackFontKey(codepoint: number): string | null;
145
171
  export declare function resolveFontKey(fontFamily: string): string;