wasm-html-to-image 1.0.10 → 1.0.12

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.
@@ -42,6 +42,7 @@ export interface Orchestration {
42
42
  onLog: HtmlToImageOptions["onLog"];
43
43
  diagnostics: boolean;
44
44
  onDiagnostics: HtmlToImageOptions["onDiagnostics"];
45
+ onLayout: HtmlToImageOptions["onLayout"];
45
46
  resolveResource: ResolveResourceHook | undefined;
46
47
  mediaType: "screen" | "print";
47
48
  textToPaths: boolean | undefined;
@@ -34,7 +34,7 @@ export function defaultSpeed(format) {
34
34
  }
35
35
  /** Build per-call orchestration state (log gate, timings, diag, resolve ctx). */
36
36
  export function createOrchestration(module, options) {
37
- const { format = "png", quality = 85, speed: speedOption, width, height, crop, fit, value, url, baseUrl, fontMap, userAgent, fallbackFonts, logLevel = LogLevel.None, onLog, diagnostics = false, onDiagnostics, resolveResource, mediaType = "screen", textToPaths, css, fonts: preloadFonts, limits = {}, pdfTitle, pdfAuthor, pdfSubject, pdfKeywords, pdfCreator, pdfProducer, pdfMargin, pdfHeader, pdfFooter, } = options;
37
+ const { format = "png", quality = 85, speed: speedOption, width, height, crop, fit, value, url, baseUrl, fontMap, userAgent, fallbackFonts, logLevel = LogLevel.None, onLog, diagnostics = false, onDiagnostics, onLayout, resolveResource, mediaType = "screen", textToPaths, css, fonts: preloadFonts, limits = {}, pdfTitle, pdfAuthor, pdfSubject, pdfKeywords, pdfCreator, pdfProducer, pdfMargin, pdfHeader, pdfFooter, } = options;
38
38
  // `animation` accepts a boolean (image input: keep frames) or, for HTML
39
39
  // input, an object requesting keyframe sampling. The object form turns on
40
40
  // the boolean flag for the encode stage and enables the frame path.
@@ -167,6 +167,7 @@ export function createOrchestration(module, options) {
167
167
  onLog,
168
168
  diagnostics,
169
169
  onDiagnostics,
170
+ onLayout,
170
171
  resolveResource,
171
172
  mediaType,
172
173
  textToPaths,
@@ -407,6 +408,10 @@ export async function tryRunHtmlInstance(module, htmls, satoruOpts, defaultFontM
407
408
  await resolveHtmlResources(module, sInst, htmls, width, height, baseUrl, fontMap ?? defaultFontMap, userAgent ?? DEFAULT_USER_AGENT, fallbackFonts, resolveCtx);
408
409
  addTime("resolveResources", now() - resolveStart);
409
410
  emitLog(LogLevel.Info, "resources resolved");
411
+ if (orch.onLayout && typeof module.satoru_layout_json === "function") {
412
+ const page = Array.isArray(htmls) ? htmls[0] : htmls;
413
+ orch.onLayout(JSON.parse(module.satoru_layout_json(sInst, page, width, height ?? 0, orch.mediaTypeInt)));
414
+ }
410
415
  if (format === "svg" || format === "pdf") {
411
416
  const renderStart = now();
412
417
  const out = (await renderBinding(sInst, htmls, width, height ?? 0, fmtInt, satoruOpts));
@@ -108,7 +108,8 @@ async function fetchResourceBytesInner(url, baseUrl, userAgent, timeoutMs, confi
108
108
  const headers = userAgent && isNodeRuntime() ? { "User-Agent": userAgent } : undefined;
109
109
  // Abort hung connections so one stalled server cannot outlive `timeoutMs`.
110
110
  const signal = timeoutMs !== undefined
111
- ? AbortSignal.timeout(Math.max(1, timeoutMs))
111
+ ? // Node >= 24 requires an integer delay; the remaining budget is fractional.
112
+ AbortSignal.timeout(Math.max(1, Math.ceil(timeoutMs)))
112
113
  : undefined;
113
114
  const fetchBytes = async (target) => {
114
115
  // Servers may vary the body by User-Agent (e.g. woff2 vs ttf CSS).
@@ -440,7 +441,17 @@ export async function resolveHtmlResources(module, satoruInst, htmls, width, hei
440
441
  // later sheet must not land before (and so lose to) a large earlier one. They are
441
442
  // buffered here and injected in discovery order once the whole round has settled.
442
443
  const loadedCss = [];
443
- await mapWithLimit(pending, FETCH_CONCURRENCY, async (r) => {
444
+ // When maxResourceCount cuts a round short, what is dropped should be the least harmful: a missing
445
+ // image leaves a hole, a missing font turns the whole text into tofu. Stylesheets go first (they
446
+ // cascade by document order and may reveal more), then fonts, then images; discovery order is
447
+ // kept within each kind.
448
+ const rank = (t) => (t === "css" ? 0 : t === "font" ? 1 : 2);
449
+ const ordered = pending
450
+ .map((r, i) => ({ r, i }))
451
+ .sort((a, b) => rank(a.r.type) - rank(b.r.type) || a.i - b.i)
452
+ .map((e) => e.r);
453
+ let fontsSkippedByCount = 0;
454
+ await mapWithLimit(ordered, FETCH_CONCURRENCY, async (r) => {
444
455
  const diag = ctx.diag;
445
456
  const entry = diag
446
457
  ? {
@@ -482,6 +493,8 @@ export async function resolveHtmlResources(module, satoruInst, htmls, width, hei
482
493
  if (ctx.limits.maxResourceCount !== undefined &&
483
494
  ctx.usage.resourceCount >= ctx.limits.maxResourceCount) {
484
495
  const message = `Maximum resource count (${ctx.limits.maxResourceCount}) exceeded`;
496
+ if (r.type === "font")
497
+ fontsSkippedByCount++;
485
498
  skipWithError(DIAGNOSTIC_CODES.LIMIT_RESOURCE_COUNT, message);
486
499
  return;
487
500
  }
@@ -538,6 +551,17 @@ export async function resolveHtmlResources(module, satoruInst, htmls, width, hei
538
551
  diag?.warnings.push(d);
539
552
  }
540
553
  });
554
+ if (fontsSkippedByCount > 0) {
555
+ // Text in those fonts renders as tofu: say what to change instead of leaving a silent bad image.
556
+ const message = `${fontsSkippedByCount} font file(s) were not loaded because maxResourceCount ` +
557
+ `(${ctx.limits.maxResourceCount}) was reached; text using them may render as missing glyphs. ` +
558
+ "Raise limits.maxResourceCount (a Google Fonts stylesheet alone can reference over a hundred files).";
559
+ ctx.diag?.warnings.push({
560
+ code: DIAGNOSTIC_CODES.LIMIT_RESOURCE_COUNT,
561
+ message,
562
+ });
563
+ ctx.emitLog(LogLevel.Warning, message);
564
+ }
541
565
  loadedCss.sort((a, b) => a.index - b.index);
542
566
  for (const css of loadedCss) {
543
567
  const text = new TextDecoder().decode(css.bytes);
package/dist/core.d.ts CHANGED
@@ -60,6 +60,21 @@ export interface RenderOptions {
60
60
  * - Image input: render stage is skipped; `width`/`height`/`crop`/`fit`
61
61
  * are applied as output resize on the encode stage.
62
62
  */
63
+ /** One element's border box from `onLayout` (page coordinates, CSS px). */
64
+ export interface LayoutBox {
65
+ /** Path of element-child indexes from the root, e.g. "0.1.3". */
66
+ p: string;
67
+ /** Lower-case tag name. */
68
+ t: string;
69
+ /** `id` attribute ("" when absent). */
70
+ i: string;
71
+ /** `class` attribute, truncated ("" when absent). */
72
+ c: string;
73
+ x: number;
74
+ y: number;
75
+ w: number;
76
+ h: number;
77
+ }
63
78
  export interface HtmlToImageOptions extends RenderOptions {
64
79
  /** Encode quality 0-100 (encode stage only, default 85; ignored for svg/pdf). */
65
80
  quality?: number;
@@ -127,6 +142,13 @@ export interface HtmlToImageOptions extends RenderOptions {
127
142
  diagnostics?: boolean;
128
143
  /** Receives the diagnostics report when `diagnostics` is enabled. */
129
144
  onDiagnostics?: (report: RenderDiagnostics) => void;
145
+ /**
146
+ * Debug aid for comparing against a reference browser: receives every laid-out element's border
147
+ * box in page coordinates (HTML input only; `html-to-image` instance pipeline). `p` is the
148
+ * element's path among element children of the shadow-flattened tree ("0.1.3"), so a browser
149
+ * walk produces the same keys. Inline boxes are omitted except replaced ones (img, svg, ...).
150
+ */
151
+ onLayout?: (boxes: LayoutBox[]) => void;
130
152
  /**
131
153
  * Override hook for resource resolution, applied to every fetch path
132
154
  * (discovery fetches, fallback-font URLs, HTML `url` fetch). Return