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 +4 -0
- package/dist/cli-options.d.ts +32 -0
- package/dist/cli-options.js +55 -0
- package/dist/cli.js +8 -25
- package/dist/core-orchestrate.js +13 -0
- package/dist/html-to-image-wasm.js +1 -1
- package/dist/html-to-image.wasm +0 -0
- package/dist/web-workers.js +5 -1
- package/package.json +1 -1
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
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
58
|
+
baseUrl: options.baseUrl,
|
|
76
59
|
};
|
|
77
|
-
const result = await render(
|
|
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));
|
package/dist/core-orchestrate.js
CHANGED
|
@@ -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
|
}
|