wasm-html-to-image 1.0.9 → 1.0.10

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/README.md CHANGED
@@ -123,7 +123,7 @@ npx wasm-html-to-image loader.html -o loader.gif -f gif --animation 2000
123
123
  ### Outputs (`format`)
124
124
  `png`, `jpeg`, `webp`, `avif`, `jxl`, `apng`, `gif`, `tiff`, `svg`, `pdf`, `thumbhash`, `raw` (uncompressed RGBA).
125
125
 
126
- **Print colors:** `format: "tiff"` keeps RGBA; add `colorSpace: "cmyk"` (CLI: `--color-space cmyk`) to get a CMYK TIFF, or a PDF whose colors and images are DeviceCMYK. The conversion is the plain device formula (no ICC profile), so convert with your print shop's profile if you need exact press colors.
126
+ **Print colors:** `format: "tiff"` keeps RGBA; add `colorSpace: "cmyk"` (CLI: `--color-space cmyk`) to get a CMYK TIFF, or a PDF whose colors and images are DeviceCMYK. The conversion is the plain device formula (no ICC profile), so convert with your print shop's profile if you need exact press colors. For print resolution, add `scale` (CLI: `--scale`): an 8.33in-wide page at 300 dpi is `width: 800, scale: 3.125`, and the TIFF records `96 × scale` ppi (override with `dpi`).
127
127
 
128
128
  Every call returns a `RenderResult` object:
129
129
  ```typescript
@@ -10,6 +10,10 @@ export interface CliFlags {
10
10
  baseUrl?: string;
11
11
  /** `--color-space <srgb|cmyk>`; absent means the default (srgb). */
12
12
  colorSpace?: "srgb" | "cmyk";
13
+ /** `--scale <n>`; absent means 1. */
14
+ scale?: number;
15
+ /** `--dpi <n>`; absent means 96 x scale. */
16
+ dpi?: number;
13
17
  /** `--animation <duration-ms>`; absent means "no keyframe animation". */
14
18
  animation?: number;
15
19
  /** `--animation-fps <n>`; defaults to 15 when `--animation` is set. */
@@ -86,6 +86,8 @@ export async function buildRenderOptions(input, flags) {
86
86
  ...source,
87
87
  ...(flags.baseUrl !== undefined ? { baseUrl: flags.baseUrl } : {}),
88
88
  ...(flags.colorSpace !== undefined ? { colorSpace: flags.colorSpace } : {}),
89
+ ...(flags.scale !== undefined ? { scale: flags.scale } : {}),
90
+ ...(flags.dpi !== undefined ? { dpi: flags.dpi } : {}),
89
91
  ...(animation ? { animation } : {}),
90
92
  };
91
93
  }
package/dist/cli.js CHANGED
@@ -34,6 +34,8 @@ program
34
34
  .option("-q, --quality <number>", "encode quality 0-100 (encode stage only)", (v) => parseFloat(v), 85)
35
35
  .option("--base-url <url>", "base URL for relative resources (URL input: defaults to the post-redirect document URL; file input: defaults to the file's directory)")
36
36
  .option("--color-space <srgb|cmyk>", "output color space of tiff and pdf (default srgb; cmyk rewrites colors for print, with no ICC profile)", (v) => v.toLowerCase())
37
+ .option("--scale <n>", "pixel density of raster output from HTML (default 1; 3.125 draws an 800px layout as 2500px)", (v) => parseFloat(v))
38
+ .option("--dpi <n>", "resolution written to tiff output, in pixels per inch (default 96 x scale)", (v) => parseInt(v, 10))
37
39
  .option("--animation <duration-ms>", "sample the document's @keyframes opacity / transform / color animation into an animated WebP / APNG / GIF; one loop duration in ms (webp, apng and gif output only; the CSS animation-duration wins and this value is an upper bound)", (v) => parseInt(v, 10))
38
40
  .option("--animation-fps <n>", "animation frame rate (default 15, capped at ~75 frames total; requires --animation)", (v) => parseInt(v, 10))
39
41
  .option("--animation-loop <n>", "how many times the animated WebP / APNG / GIF repeats (0 = infinite, the default; 1 = play once; requires --animation)", (v) => parseInt(v, 10))
@@ -70,6 +72,8 @@ program
70
72
  quality: options.quality,
71
73
  baseUrl: options.baseUrl,
72
74
  colorSpace: options.colorSpace,
75
+ scale: options.scale,
76
+ dpi: options.dpi,
73
77
  animation: options.animation,
74
78
  animationFps: options.animationFps,
75
79
  animationLoop: options.animationLoop,
@@ -63,6 +63,12 @@ export interface Orchestration {
63
63
  fmtInt: number;
64
64
  /** Output color space of `tiff` / `pdf`. */
65
65
  colorSpace: "srgb" | "cmyk";
66
+ /** Raster pixel density of HTML input (1 = CSS px). */
67
+ scale: number;
68
+ /** TIFF resolution tag in pixels per inch (image input: 96 unless set). */
69
+ dpi: number;
70
+ /** The caller's explicit `dpi` (image input has no `scale`, so it falls back to 96). */
71
+ dpiOption: number | undefined;
66
72
  mediaTypeInt: number;
67
73
  t0: number;
68
74
  diag: DiagState | null;
@@ -47,6 +47,16 @@ export function createOrchestration(module, options) {
47
47
  const animationLoop = Math.max(0, Math.trunc(animationFrames?.loop ?? 0));
48
48
  const speed = speedOption ?? defaultSpeed(format);
49
49
  const colorSpace = options.colorSpace === "cmyk" ? "cmyk" : "srgb";
50
+ const scale = options.scale ?? 1;
51
+ if (!Number.isFinite(scale) || scale <= 0 || scale > 16) {
52
+ throw new Error(`wasm-html-to-image: scale must be a number in (0, 16], got ${String(options.scale)}`);
53
+ }
54
+ if (options.dpi !== undefined &&
55
+ (!Number.isFinite(options.dpi) || options.dpi < 1 || options.dpi > 65535)) {
56
+ throw new Error(`wasm-html-to-image: dpi must be a number in [1, 65535], got ${String(options.dpi)}`);
57
+ }
58
+ // One CSS px is 1/96 in, so a scaled HTML render carries 96 * scale ppi by default.
59
+ const dpi = Math.round(options.dpi ?? 96 * scale);
50
60
  const fmtInt = format === "tiff" && colorSpace === "cmyk"
51
61
  ? TIFF_CMYK_INT
52
62
  : FORMAT_INT[format];
@@ -174,6 +184,9 @@ export function createOrchestration(module, options) {
174
184
  pdfFooter,
175
185
  fmtInt,
176
186
  colorSpace,
187
+ scale,
188
+ dpi,
189
+ dpiOption: options.dpi,
177
190
  mediaTypeInt,
178
191
  t0,
179
192
  diag,
@@ -248,7 +261,7 @@ export async function resolveHtmlSource(orch) {
248
261
  * `imageBytesFromUnknown` (`unknown` in, no `as` casts).
249
262
  */
250
263
  export async function runImageInput(module, value, orch) {
251
- const { format, quality, speed, animation, animationLoop, width, height, crop, fit, fmtInt, colorSpace, t0, emitLog, logged, loggedBinding, addTime, deliverReport, } = orch;
264
+ const { format, quality, speed, animation, animationLoop, width, height, crop, fit, fmtInt, colorSpace, dpiOption, t0, emitLog, logged, loggedBinding, addTime, deliverReport, } = orch;
252
265
  // Narrowed here: caller selected this branch via `isImageInput(value)`.
253
266
  const loadImage = module.converter_load_image;
254
267
  if (typeof loadImage !== "function")
@@ -343,6 +356,8 @@ export async function runImageInput(module, value, orch) {
343
356
  const encodeBinding = module.converter_encode;
344
357
  if (typeof encodeBinding !== "function")
345
358
  throw loggedBinding("converter_encode");
359
+ if (format === "tiff")
360
+ module.converter_set_dpi?.(inst, Math.round(dpiOption ?? 96));
346
361
  const encodeStart = now();
347
362
  const out = (await encodeBinding(inst, fmtInt, quality, speed, animation, animationLoop));
348
363
  addTime("encode", now() - encodeStart);
@@ -376,7 +391,7 @@ export async function runImageInput(module, value, orch) {
376
391
  * pre-resolved resources, so it stays a fallback only).
377
392
  */
378
393
  export async function tryRunHtmlInstance(module, htmls, satoruOpts, defaultFontMap, orch) {
379
- const { format, quality, speed, animation, animationFrames, animationLoop, width, height, baseUrl, fontMap, userAgent, fallbackFonts, fmtInt, t0, emitLog, logged, loggedBinding, addTime, deliverReport, resolveCtx, } = orch;
394
+ const { format, quality, speed, animation, animationFrames, animationLoop, width, height, baseUrl, fontMap, userAgent, fallbackFonts, fmtInt, dpi, t0, emitLog, logged, loggedBinding, addTime, deliverReport, resolveCtx, } = orch;
380
395
  const renderBinding = module.satoru_render;
381
396
  const encodeBinding = module.converter_encode;
382
397
  if (typeof renderBinding !== "function" ||
@@ -458,6 +473,8 @@ export async function tryRunHtmlInstance(module, htmls, satoruOpts, defaultFontM
458
473
  }
459
474
  const outWidth = module.converter_get_width?.(cInst) ?? width;
460
475
  const outHeight = module.converter_get_height?.(cInst) ?? height ?? 0;
476
+ if (format === "tiff")
477
+ module.converter_set_dpi?.(cInst, dpi);
461
478
  const encodeStart = now();
462
479
  const out = (await encodeBinding(cInst, fmtInt, quality, speed, animation, animationLoop));
463
480
  addTime("encode", now() - encodeStart);
package/dist/core.d.ts CHANGED
@@ -146,6 +146,19 @@ export interface HtmlToImageOptions extends RenderOptions {
146
146
  }[];
147
147
  /** Safety/performance limits, enforced in JS around resource resolution. */
148
148
  limits?: RenderLimits;
149
+ /**
150
+ * Pixel density of raster output from HTML input (default `1`). The page is laid out at
151
+ * `width` CSS px and drawn `scale` times larger, so `scale: 3.125` turns an 800px layout
152
+ * into a 2500px bitmap (300 dpi for an 8.33in page) with text and shapes re-rasterized
153
+ * sharply. `width` / `height` / `crop` stay in CSS px; the result's `width` / `height` are
154
+ * the bitmap size. Range 0-16; ignored for `svg` / `pdf` and for image input.
155
+ */
156
+ scale?: number;
157
+ /**
158
+ * Resolution written to the `tiff` file, in pixels per inch (default `96 * scale`, rounded:
159
+ * one CSS px is 1/96 in). A print shop reads the physical size from it.
160
+ */
161
+ dpi?: number;
149
162
  /**
150
163
  * Output color space of `tiff` and `pdf` (default `"srgb"`).
151
164
  *
package/dist/core.js CHANGED
@@ -47,6 +47,7 @@ export async function htmlToImage(module, options) {
47
47
  textToPaths: orch.textToPaths,
48
48
  mediaTypeInt: orch.mediaTypeInt,
49
49
  colorSpace: orch.colorSpace,
50
+ scale: orch.scale,
50
51
  pdfTitle: orch.pdfTitle,
51
52
  pdfAuthor: orch.pdfAuthor,
52
53
  pdfSubject: orch.pdfSubject,
@@ -17,6 +17,7 @@ export interface FullSatoruOptionsInput {
17
17
  textToPaths?: boolean;
18
18
  mediaTypeInt: number;
19
19
  colorSpace?: "srgb" | "cmyk";
20
+ scale?: number;
20
21
  pdfTitle?: string;
21
22
  pdfAuthor?: string;
22
23
  pdfSubject?: string;
@@ -45,6 +45,7 @@ export function buildFullSatoruOptions(input) {
45
45
  svgTextToPaths: textToPaths ?? true,
46
46
  mediaType: mediaTypeInt,
47
47
  ...(input.colorSpace === "cmyk" ? { colorSpace: 1 } : {}),
48
+ ...(input.scale !== undefined && input.scale !== 1 ? { scale: input.scale } : {}),
48
49
  pdfTitle: pdfTitle ?? "",
49
50
  pdfAuthor: pdfAuthor ?? "",
50
51
  pdfSubject: pdfSubject ?? "",