wasm-html-to-image 1.0.1 → 1.0.2

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
@@ -267,6 +267,10 @@ npx wasm-html-to-image template.html -o banner.webp -w 1200 -h 630 -f webp -q 90
267
267
  # Capture screenshot from URL
268
268
  npx wasm-html-to-image https://example.com -o site.png -w 1280 -h 720
269
269
 
270
+ # Pin the base for relative resources (default: URL input follows redirects,
271
+ # file input uses the file's own directory)
272
+ npx wasm-html-to-image https://example.com/news -o news.png --base-url https://example.com/
273
+
270
274
  # Convert and resize an image
271
275
  npx wasm-html-to-image photo.jpg -o photo.avif -w 800 -f avif -q 80
272
276
  ```
@@ -0,0 +1,32 @@
1
+ import type { HtmlToImageOptions, OutputFormat } from "./core.js";
2
+ /** Flags as parsed by commander (see `./cli.ts`). */
3
+ export interface CliFlags {
4
+ output?: string;
5
+ width: number;
6
+ height?: number;
7
+ format: OutputFormat;
8
+ quality: number;
9
+ /** Explicit `--base-url`; absent means "let the source decide". */
10
+ baseUrl?: string;
11
+ }
12
+ /**
13
+ * URL inputs are fetched, not read from disk. `data:` URLs stay values so
14
+ * image inputs keep flowing through the converter pipeline.
15
+ */
16
+ export declare function isUrlInput(input: string): boolean;
17
+ /**
18
+ * Read a local input file as binary; image files (magic-byte sniffed) are
19
+ * passed through as bytes, everything else as UTF-8 text (HTML).
20
+ */
21
+ export declare function readInput(input: string): Promise<{
22
+ value: string | Uint8Array;
23
+ baseUrl: string;
24
+ }>;
25
+ /**
26
+ * Map CLI flags to `render()` options.
27
+ *
28
+ * `--base-url` is the only way to pin the base: without it a URL input leaves
29
+ * `baseUrl` unset so relative resources follow the post-redirect final URL
30
+ * (`resolveHtmlSource`), while a file input falls back to its own directory.
31
+ */
32
+ export declare function buildRenderOptions(input: string, flags: CliFlags): Promise<HtmlToImageOptions>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CLI flag plumbing split from `./cli.ts` (behavior unchanged): input-type
3
+ * detection, local file reading, and the flag -> `render()` option mapping.
4
+ * Kept separate from the executable entry point so the mapping is
5
+ * unit-testable without running the CLI.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { isImageInput } from "./input.js";
10
+ /**
11
+ * URL inputs are fetched, not read from disk. `data:` URLs stay values so
12
+ * image inputs keep flowing through the converter pipeline.
13
+ */
14
+ export function isUrlInput(input) {
15
+ const isWindowsPath = /^[a-zA-Z]:[\\/]/.test(input);
16
+ return (!isWindowsPath &&
17
+ /^[a-z][a-z0-9+.-]*:/i.test(input) &&
18
+ !input.startsWith("data:"));
19
+ }
20
+ /**
21
+ * Read a local input file as binary; image files (magic-byte sniffed) are
22
+ * passed through as bytes, everything else as UTF-8 text (HTML).
23
+ */
24
+ export async function readInput(input) {
25
+ const data = await fs.readFile(input);
26
+ let value = data.toString("utf-8");
27
+ try {
28
+ if (isImageInput(data))
29
+ value = data;
30
+ }
31
+ catch {
32
+ // Unknown binary magic -> treat as text and let rendering decide.
33
+ }
34
+ return { value, baseUrl: path.dirname(path.resolve(input)) };
35
+ }
36
+ /**
37
+ * Map CLI flags to `render()` options.
38
+ *
39
+ * `--base-url` is the only way to pin the base: without it a URL input leaves
40
+ * `baseUrl` unset so relative resources follow the post-redirect final URL
41
+ * (`resolveHtmlSource`), while a file input falls back to its own directory.
42
+ */
43
+ export async function buildRenderOptions(input, flags) {
44
+ const source = isUrlInput(input)
45
+ ? { url: input }
46
+ : await readInput(input);
47
+ return {
48
+ width: flags.width,
49
+ height: flags.height,
50
+ format: flags.format,
51
+ quality: flags.quality,
52
+ ...source,
53
+ ...(flags.baseUrl !== undefined ? { baseUrl: flags.baseUrl } : {}),
54
+ };
55
+ }
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { program } from "commander";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { render, } from "./single.js";
6
- import { isImageInput } from "./core.js";
5
+ import { render } from "./single.js";
6
+ import { buildRenderOptions, isUrlInput } from "./cli-options.js";
7
7
  const OUTPUT_FORMATS = [
8
8
  "svg",
9
9
  "png",
@@ -19,22 +19,6 @@ const OUTPUT_FORMATS = [
19
19
  function isOutputFormat(value) {
20
20
  return OUTPUT_FORMATS.includes(value);
21
21
  }
22
- /**
23
- * Read a local input file as binary; image files (magic-byte sniffed) are
24
- * passed through as bytes, everything else as UTF-8 text (HTML).
25
- */
26
- async function readInput(input) {
27
- const data = await fs.readFile(input);
28
- let value = data.toString("utf-8");
29
- try {
30
- if (isImageInput(data))
31
- value = data;
32
- }
33
- catch {
34
- // Unknown binary magic -> treat as text and let rendering decide.
35
- }
36
- return { value, baseUrl: path.dirname(path.resolve(input)) };
37
- }
38
22
  program
39
23
  .name("wasm-html-to-image")
40
24
  .description("HTML to image converter on the single unified WASM module (svg/pdf direct, png/jpeg/webp/avif/jxl/raw/thumbhash via encode or html_to_image)")
@@ -45,6 +29,7 @@ program
45
29
  .option("-h, --height <number>", "viewport height (omit for auto)", (v) => parseInt(v, 10))
46
30
  .option("-f, --format <format>", `output format (${OUTPUT_FORMATS.join(", ")})`, "png")
47
31
  .option("-q, --quality <number>", "encode quality 0-100 (encode stage only)", (v) => parseFloat(v), 85)
32
+ .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)")
48
33
  .action(async (input, options) => {
49
34
  try {
50
35
  const format = String(options.format).toLowerCase();
@@ -52,10 +37,7 @@ program
52
37
  console.error(`Error: unsupported format "${options.format}". Choose from: ${OUTPUT_FORMATS.join(", ")}`);
53
38
  process.exit(1);
54
39
  }
55
- const isWindowsPath = /^[a-zA-Z]:[\\/]/.test(input);
56
- const isUrl = !isWindowsPath &&
57
- /^[a-z][a-z0-9+.-]*:/i.test(input) &&
58
- !input.startsWith("data:");
40
+ const isUrl = isUrlInput(input);
59
41
  let outputPath = options.output;
60
42
  if (!outputPath) {
61
43
  const ext = format;
@@ -67,14 +49,15 @@ program
67
49
  ext: `.${ext}`,
68
50
  });
69
51
  }
70
- const renderOptions = {
52
+ const flags = {
53
+ output: options.output,
71
54
  width: options.width,
72
55
  height: options.height,
73
56
  format,
74
57
  quality: options.quality,
75
- ...(isUrl ? { url: input, baseUrl: input } : await readInput(input)),
58
+ baseUrl: options.baseUrl,
76
59
  };
77
- const result = await render(renderOptions);
60
+ const result = await render(await buildRenderOptions(input, flags));
78
61
  await fs.writeFile(outputPath, typeof result.data === "string"
79
62
  ? result.data
80
63
  : Buffer.from(result.data));
@@ -183,12 +183,18 @@ export async function resolveHtmlSource(orch) {
183
183
  if (typeof url === "string") {
184
184
  const headers = { "User-Agent": userAgent ?? DEFAULT_USER_AGENT };
185
185
  const fetchStart = now();
186
+ // Post-redirect document URL (`Response.url`). Relative resources must
187
+ // resolve against the final URL, not the stale request URL: a 301 to a new
188
+ // host otherwise leaves every stylesheet/font/image on the old host.
189
+ let finalUrl;
186
190
  let htmls;
187
191
  if (resolveResource) {
188
192
  const fallbackFetch = async () => {
189
193
  const res = await fetch(url, { headers });
190
194
  if (!res.ok)
191
195
  return null;
196
+ if (res.url)
197
+ finalUrl = res.url;
192
198
  return toBytes(await res.arrayBuffer());
193
199
  };
194
200
  const raw = await resolveResource(url, fallbackFetch);
@@ -202,8 +208,15 @@ export async function resolveHtmlSource(orch) {
202
208
  if (!res.ok) {
203
209
  throw logged(`wasm-html-to-image: failed to fetch HTML from URL: ${url} (${res.status})`);
204
210
  }
211
+ if (res.url)
212
+ finalUrl = res.url;
205
213
  htmls = await res.text();
206
214
  }
215
+ // An explicit `baseUrl` always wins; otherwise adopt the document URL so
216
+ // relative resource resolution follows redirects.
217
+ if (orch.baseUrl === undefined && finalUrl !== undefined) {
218
+ orch.baseUrl = finalUrl;
219
+ }
207
220
  addTime("fetchHtml", now() - fetchStart);
208
221
  return htmls;
209
222
  }