wasm-html-to-image 1.0.22 → 1.0.23
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/dist/cli-options.d.ts +44 -0
- package/dist/cli-options.js +94 -1
- package/dist/cli.js +41 -3
- package/dist/core-orchestrate.d.ts +2 -0
- package/dist/core-orchestrate.js +6 -4
- package/dist/core-resources.d.ts +8 -0
- package/dist/core-resources.js +16 -3
- package/dist/core.d.ts +10 -0
- package/dist/core.js +2 -0
- package/dist/encode-args.d.ts +7 -0
- package/dist/encode-args.js +18 -1
- package/dist/html-to-image-wasm.js +3 -3
- package/dist/html-to-image.wasm +0 -0
- package/dist/katex-css.d.ts +6 -0
- package/dist/katex-css.js +7 -0
- package/dist/markdown-math-syntax.d.ts +20 -0
- package/dist/markdown-math-syntax.js +60 -0
- package/dist/markdown-math.d.ts +27 -0
- package/dist/markdown-math.js +25 -0
- package/dist/markdown-template.d.ts +113 -0
- package/dist/markdown-template.js +174 -0
- package/dist/markdown.d.ts +88 -0
- package/dist/markdown.js +302 -0
- package/dist/web-workers.js +34 -5
- package/package.json +13 -2
package/dist/cli-options.d.ts
CHANGED
|
@@ -27,6 +27,32 @@ export interface CliFlags {
|
|
|
27
27
|
font?: string[];
|
|
28
28
|
/** `--js`: run the page's scripts (jsdom) before rendering. */
|
|
29
29
|
js?: boolean;
|
|
30
|
+
/** `--markdown`: treat the input as Markdown (implied by a `.md` / `.markdown` file). */
|
|
31
|
+
markdown?: boolean;
|
|
32
|
+
/** `--markdown-theme <light|dark>`; absent means light. */
|
|
33
|
+
markdownTheme?: string;
|
|
34
|
+
/** `--template <name|file>`: a bundled template, or a .json / .css file (see `loadTemplate`). */
|
|
35
|
+
template?: string;
|
|
36
|
+
/** `--page-size <A4|A5|...|"210mm 297mm">`. */
|
|
37
|
+
pageSize?: string;
|
|
38
|
+
/** `--accent <colour>`: links, rules and the cover accent. */
|
|
39
|
+
accent?: string;
|
|
40
|
+
/** `--body-font <family>`: the body font of the page. */
|
|
41
|
+
bodyFont?: string;
|
|
42
|
+
/** `--stamp <text>`: printed in the header of every page. */
|
|
43
|
+
stamp?: string;
|
|
44
|
+
/** `--cover` / `--no-cover`. */
|
|
45
|
+
cover?: boolean;
|
|
46
|
+
/** `--no-toc` sets this to false: a PDF from Markdown gets a table of contents by default. */
|
|
47
|
+
toc?: boolean;
|
|
48
|
+
/** `--toc-depth <n>`: deepest heading level in the table of contents (default 3). */
|
|
49
|
+
tocDepth?: number;
|
|
50
|
+
/** `--no-outline` sets this to false: the PDF's bookmarks (outline) are on by default. */
|
|
51
|
+
outline?: boolean;
|
|
52
|
+
/** `--no-math` sets this to false: `$...$` / `$$...$$` are typeset when katex is installed. */
|
|
53
|
+
math?: boolean;
|
|
54
|
+
/** `--markdown-css <path>`: extra stylesheet appended after the built-in one. */
|
|
55
|
+
markdownCss?: string;
|
|
30
56
|
/** `--js-wait <ms|selector>`: a number is the settle time after load, anything else a CSS selector to wait for. */
|
|
31
57
|
jsWait?: string;
|
|
32
58
|
}
|
|
@@ -60,6 +86,24 @@ export interface AnimationResolution {
|
|
|
60
86
|
export declare function resolveAnimation(flags: CliFlags): AnimationResolution;
|
|
61
87
|
/** Output format implied by the output file's extension, if recognised. */
|
|
62
88
|
export declare function inferFormat(output: string | undefined): OutputFormat | undefined;
|
|
89
|
+
/** Whether `input` names an existing file (a Markdown text or HTML string is not a path). */
|
|
90
|
+
export declare function isFile(input: string): Promise<boolean>;
|
|
91
|
+
/** Markdown text passed with `--markdown` instead of a file path. */
|
|
92
|
+
export declare function isInlineMarkdown(input: string, flags: {
|
|
93
|
+
markdown?: boolean;
|
|
94
|
+
}): Promise<boolean>;
|
|
95
|
+
/** A `.md` / `.markdown` file path is Markdown input without `--markdown`. */
|
|
96
|
+
export declare function isMarkdownPath(input: string): boolean;
|
|
97
|
+
/** Convert Markdown text to a styled HTML document (`marked` is an optional dependency). */
|
|
98
|
+
export declare function convertMarkdown(markdown: string, flags: CliFlags): Promise<{
|
|
99
|
+
html: string;
|
|
100
|
+
renderOptions: import("./markdown.js").MarkdownRenderOptions;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* `--template`: a bundled name, or a file. A `.json` file is a {@link MarkdownTemplate} (`type`, `page`,
|
|
104
|
+
* `defaults`, `css`); a `.css` file is extra CSS laid over the default template.
|
|
105
|
+
*/
|
|
106
|
+
export declare function loadTemplate(spec: string): Promise<string | import("./markdown-template.js").MarkdownTemplate>;
|
|
63
107
|
/** Inline markup passed on the command line (as opposed to a path or URL). */
|
|
64
108
|
export declare function isInlineHtml(input: string): boolean;
|
|
65
109
|
/** Read all of standard input as UTF-8 text. */
|
package/dist/cli-options.js
CHANGED
|
@@ -65,6 +65,87 @@ export function inferFormat(output) {
|
|
|
65
65
|
const ext = path.extname(output).slice(1).toLowerCase();
|
|
66
66
|
return EXTENSION_FORMATS[ext];
|
|
67
67
|
}
|
|
68
|
+
/** Whether `input` names an existing file (a Markdown text or HTML string is not a path). */
|
|
69
|
+
export async function isFile(input) {
|
|
70
|
+
try {
|
|
71
|
+
return (await fs.stat(input)).isFile();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Markdown text passed with `--markdown` instead of a file path. */
|
|
78
|
+
export async function isInlineMarkdown(input, flags) {
|
|
79
|
+
return !!flags.markdown && input !== "-" && !isUrlInput(input) && !(await isFile(input));
|
|
80
|
+
}
|
|
81
|
+
/** A `.md` / `.markdown` file path is Markdown input without `--markdown`. */
|
|
82
|
+
export function isMarkdownPath(input) {
|
|
83
|
+
return /\.(md|markdown)$/i.test(input) && !isUrlInput(input);
|
|
84
|
+
}
|
|
85
|
+
/** Convert Markdown text to a styled HTML document (`marked` is an optional dependency). */
|
|
86
|
+
export async function convertMarkdown(markdown, flags) {
|
|
87
|
+
let mod;
|
|
88
|
+
try {
|
|
89
|
+
mod = await import("./markdown.js");
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (e.code !== "ERR_MODULE_NOT_FOUND")
|
|
93
|
+
throw e;
|
|
94
|
+
throw new Error("Markdown input needs the optional dependency `marked`. Install it with `npm i marked`.");
|
|
95
|
+
}
|
|
96
|
+
const theme = flags.markdownTheme?.toLowerCase();
|
|
97
|
+
if (theme !== undefined && theme !== "light" && theme !== "dark") {
|
|
98
|
+
throw new Error(`unsupported markdown theme "${flags.markdownTheme}". Choose from: light, dark`);
|
|
99
|
+
}
|
|
100
|
+
const css = flags.markdownCss ? await fs.readFile(flags.markdownCss, "utf-8") : undefined;
|
|
101
|
+
const settings = {
|
|
102
|
+
...(flags.pageSize !== undefined ? { page_size: flags.pageSize } : {}),
|
|
103
|
+
...(flags.accent !== undefined ? { accent: flags.accent } : {}),
|
|
104
|
+
...(flags.bodyFont !== undefined ? { font: flags.bodyFont } : {}),
|
|
105
|
+
...(flags.stamp !== undefined ? { stamp: flags.stamp } : {}),
|
|
106
|
+
...(flags.cover !== undefined ? { cover: flags.cover } : {}),
|
|
107
|
+
};
|
|
108
|
+
const template = flags.template !== undefined ? await loadTemplate(flags.template) : undefined;
|
|
109
|
+
return mod.markdownToDocument(markdown, {
|
|
110
|
+
theme,
|
|
111
|
+
css,
|
|
112
|
+
...(template !== undefined ? { template } : {}),
|
|
113
|
+
...(Object.keys(settings).length ? { settings } : {}),
|
|
114
|
+
paged: flags.format === "pdf",
|
|
115
|
+
// Default: a table of contents in a PDF (markdownToHtml's own default), none in an image.
|
|
116
|
+
...(flags.toc === false ? { toc: false } : flags.tocDepth !== undefined ? { toc: { depth: flags.tocDepth } } : {}),
|
|
117
|
+
...(flags.math === false ? {} : await loadMath()),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `--template`: a bundled name, or a file. A `.json` file is a {@link MarkdownTemplate} (`type`, `page`,
|
|
122
|
+
* `defaults`, `css`); a `.css` file is extra CSS laid over the default template.
|
|
123
|
+
*/
|
|
124
|
+
export async function loadTemplate(spec) {
|
|
125
|
+
if (!/\.(json|css)$/i.test(spec))
|
|
126
|
+
return spec; // a name: resolveTemplate reports an unknown one
|
|
127
|
+
const text = await fs.readFile(spec, "utf-8");
|
|
128
|
+
if (/\.css$/i.test(spec))
|
|
129
|
+
return { description: spec, css: text };
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
throw new Error(`template ${spec} is not valid JSON: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** KaTeX maths for the Markdown, or nothing when `katex` is not installed (it is optional). */
|
|
138
|
+
async function loadMath() {
|
|
139
|
+
try {
|
|
140
|
+
const { katexMath } = await import("./markdown-math.js");
|
|
141
|
+
return { math: katexMath() };
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (e.code === "ERR_MODULE_NOT_FOUND")
|
|
145
|
+
return {};
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
68
149
|
/** Inline markup passed on the command line (as opposed to a path or URL). */
|
|
69
150
|
export function isInlineHtml(input) {
|
|
70
151
|
return input.trimStart().startsWith("<");
|
|
@@ -152,13 +233,23 @@ export async function readInput(input) {
|
|
|
152
233
|
* (`resolveHtmlSource`), while a file input falls back to its own directory.
|
|
153
234
|
*/
|
|
154
235
|
export async function buildRenderOptions(input, flags) {
|
|
155
|
-
const
|
|
236
|
+
const markdown = flags.markdown || isMarkdownPath(input);
|
|
237
|
+
// `--markdown` takes a file path, `-` (stdin) or the Markdown text itself.
|
|
238
|
+
const inlineMarkdown = await isInlineMarkdown(input, flags);
|
|
239
|
+
let source = inlineMarkdown || isInlineHtml(input)
|
|
156
240
|
? { value: input, baseUrl: process.cwd() }
|
|
157
241
|
: input === "-"
|
|
158
242
|
? { value: await readStdin(), baseUrl: process.cwd() }
|
|
159
243
|
: isUrlInput(input)
|
|
160
244
|
? { url: input }
|
|
161
245
|
: await readInput(input);
|
|
246
|
+
// A template's running header / footer and the PDF's title and author travel with the page.
|
|
247
|
+
let templateOptions = {};
|
|
248
|
+
if (markdown && "value" in source && typeof source.value === "string") {
|
|
249
|
+
const converted = await convertMarkdown(source.value, flags);
|
|
250
|
+
source = { ...source, value: converted.html };
|
|
251
|
+
templateOptions = converted.renderOptions;
|
|
252
|
+
}
|
|
162
253
|
const fonts = flags.font?.length ? await loadFonts(flags.font) : [];
|
|
163
254
|
const { animation, warning } = resolveAnimation(flags);
|
|
164
255
|
if (warning)
|
|
@@ -169,6 +260,7 @@ export async function buildRenderOptions(input, flags) {
|
|
|
169
260
|
format: flags.format,
|
|
170
261
|
quality: flags.quality,
|
|
171
262
|
...source,
|
|
263
|
+
...templateOptions,
|
|
172
264
|
...(flags.baseUrl !== undefined ? { baseUrl: flags.baseUrl } : {}),
|
|
173
265
|
...(flags.colorSpace !== undefined ? { colorSpace: flags.colorSpace } : {}),
|
|
174
266
|
...(flags.scale !== undefined ? { scale: flags.scale } : {}),
|
|
@@ -176,5 +268,6 @@ export async function buildRenderOptions(input, flags) {
|
|
|
176
268
|
...(animation ? { animation } : {}),
|
|
177
269
|
...(fonts.length ? { fonts } : {}),
|
|
178
270
|
...(flags.js ? { javascript: resolveJavaScript(flags.jsWait) } : {}),
|
|
271
|
+
...(flags.outline === false ? { pdfOutline: false } : {}),
|
|
179
272
|
};
|
|
180
273
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { program } from "commander";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { render } from "./single.js";
|
|
6
|
-
import { buildRenderOptions, inferFormat, isInlineHtml, isUrlInput, } from "./cli-options.js";
|
|
6
|
+
import { buildRenderOptions, inferFormat, isInlineHtml, isInlineMarkdown, isUrlInput, } from "./cli-options.js";
|
|
7
7
|
const OUTPUT_FORMATS = [
|
|
8
8
|
"svg",
|
|
9
9
|
"png",
|
|
@@ -26,7 +26,7 @@ program
|
|
|
26
26
|
.name("wasm-html-to-image")
|
|
27
27
|
.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)")
|
|
28
28
|
.version("0.1.0")
|
|
29
|
-
.argument("
|
|
29
|
+
.argument("[input]", "input HTML/image file path, URL, inline HTML string (starts with <), or - for stdin")
|
|
30
30
|
.option("-o, --output <path>", "output file path")
|
|
31
31
|
.option("-w, --width <number>", "viewport width", (v) => parseInt(v, 10), 800)
|
|
32
32
|
.option("-h, --height <number>", "viewport height (omit for auto)", (v) => parseInt(v, 10))
|
|
@@ -40,10 +40,35 @@ program
|
|
|
40
40
|
.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))
|
|
41
41
|
.option("--animation-fps <n>", "animation frame rate (default 15, capped at ~75 frames total; requires --animation)", (v) => parseInt(v, 10))
|
|
42
42
|
.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))
|
|
43
|
+
.option("--markdown", "treat the input as Markdown (implied by a .md / .markdown file; inline text and stdin need it; needs the optional dependency marked)")
|
|
44
|
+
.option("--template <name|file>", "look of a PDF from Markdown: a bundled template (see --list-templates) or a .json / .css file")
|
|
45
|
+
.option("--list-templates", "print the bundled templates and exit")
|
|
46
|
+
.option("--page-size <size>", "PDF page size for Markdown: A3, A4, A5, B4, B5, Letter, or e.g. '210mm 297mm'")
|
|
47
|
+
.option("--accent <colour>", "accent colour of a Markdown document (links, rules, cover), e.g. '#c2410c'")
|
|
48
|
+
.option("--body-font <family>", "body font family of a Markdown document (the renderer loads it; serif and sans-serif work)")
|
|
49
|
+
.option("--stamp <text>", "text printed in the header of every page of a Markdown PDF (DRAFT, Confidential, ...)")
|
|
50
|
+
.option("--cover", "a cover page with the title, subtitle, author and date (from the front matter)")
|
|
51
|
+
.option("--no-cover", "no cover page, whatever the template says")
|
|
52
|
+
.option("--no-toc", "no table of contents in a PDF made from Markdown (there is one by default)")
|
|
53
|
+
.option("--toc-depth <n>", "deepest heading level in the table of contents (default 3)", (v) => parseInt(v, 10))
|
|
54
|
+
.option("--no-outline", "leave the bookmarks (outline) out of a PDF (they are built from the headings by default)")
|
|
55
|
+
.option("--no-math", "do not typeset $...$ and $$...$$ in Markdown (done with KaTeX when it is installed)")
|
|
56
|
+
.option("--markdown-theme <light|dark>", "colour scheme of the built-in Markdown stylesheet (default light)")
|
|
57
|
+
.option("--markdown-css <path>", "extra CSS file appended after the built-in Markdown stylesheet")
|
|
43
58
|
.option("--js", "run the page's scripts in jsdom before rendering (needs the optional dependency jsdom; trusted pages only)")
|
|
44
59
|
.option("--js-wait <ms|selector>", "with --js: milliseconds to wait after load (default 1000), or a CSS selector to wait for")
|
|
45
60
|
.action(async (input, options) => {
|
|
46
61
|
try {
|
|
62
|
+
if (options.listTemplates) {
|
|
63
|
+
const { listTemplates } = await import("./markdown-template.js");
|
|
64
|
+
for (const t of listTemplates())
|
|
65
|
+
console.log(`${t.name.padEnd(10)} ${t.description}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (input === undefined) {
|
|
69
|
+
console.error("error: missing required argument 'input'");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
47
72
|
const format = String(options.format ?? inferFormat(options.output) ?? "png").toLowerCase();
|
|
48
73
|
if (!isOutputFormat(format)) {
|
|
49
74
|
console.error(`Error: unsupported format "${options.format}". Choose from: ${OUTPUT_FORMATS.join(", ")}`);
|
|
@@ -56,7 +81,7 @@ program
|
|
|
56
81
|
process.exit(1);
|
|
57
82
|
}
|
|
58
83
|
const isUrl = isUrlInput(input);
|
|
59
|
-
const isVirtual = isInlineHtml(input) || input === "-";
|
|
84
|
+
const isVirtual = isInlineHtml(input) || input === "-" || (await isInlineMarkdown(input, options));
|
|
60
85
|
if (isVirtual && !options.output) {
|
|
61
86
|
console.error("Error: --output is required for inline HTML or stdin input");
|
|
62
87
|
process.exit(1);
|
|
@@ -88,6 +113,19 @@ program
|
|
|
88
113
|
font: options.font,
|
|
89
114
|
js: options.js,
|
|
90
115
|
jsWait: options.jsWait,
|
|
116
|
+
markdown: options.markdown,
|
|
117
|
+
markdownTheme: options.markdownTheme,
|
|
118
|
+
math: options.math,
|
|
119
|
+
template: options.template,
|
|
120
|
+
pageSize: options.pageSize,
|
|
121
|
+
accent: options.accent,
|
|
122
|
+
bodyFont: options.bodyFont,
|
|
123
|
+
stamp: options.stamp,
|
|
124
|
+
cover: options.cover,
|
|
125
|
+
toc: options.toc,
|
|
126
|
+
tocDepth: options.tocDepth,
|
|
127
|
+
outline: options.outline,
|
|
128
|
+
markdownCss: options.markdownCss,
|
|
91
129
|
};
|
|
92
130
|
const result = await render(await buildRenderOptions(input, flags));
|
|
93
131
|
await fs.writeFile(outputPath, typeof result.data === "string"
|
|
@@ -54,6 +54,8 @@ export interface Orchestration {
|
|
|
54
54
|
data: Uint8Array;
|
|
55
55
|
}[] | undefined;
|
|
56
56
|
limits: RenderLimits;
|
|
57
|
+
pdfOutline: boolean | undefined;
|
|
58
|
+
pdfBackground: string | undefined;
|
|
57
59
|
pdfTitle: string | undefined;
|
|
58
60
|
pdfAuthor: string | undefined;
|
|
59
61
|
pdfSubject: string | undefined;
|
package/dist/core-orchestrate.js
CHANGED
|
@@ -14,7 +14,7 @@ import { pdfFirstPageSize, svgRootHeight } from "./output-size.js";
|
|
|
14
14
|
import { FIT_INT, FORMAT_INT, TIFF_CMYK_INT } from "./encode-args.js";
|
|
15
15
|
import { imageBytesFromUnknown, toBytes, } from "./input.js";
|
|
16
16
|
import { DIAGNOSTIC_CODES, LogLevel, } from "./diagnostics.js";
|
|
17
|
-
import {
|
|
17
|
+
import { canSetUserAgent, now, resolveHtmlResources, } from "./core-resources.js";
|
|
18
18
|
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
19
19
|
export { DEFAULT_USER_AGENT };
|
|
20
20
|
export function requireBindingError(name) {
|
|
@@ -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, 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;
|
|
37
|
+
const { format = "png", quality = 85, speed: speedOption, 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 = {}, pdfOutline, pdfBackground, 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.
|
|
@@ -179,6 +179,8 @@ export function createOrchestration(module, options) {
|
|
|
179
179
|
css,
|
|
180
180
|
preloadFonts,
|
|
181
181
|
limits,
|
|
182
|
+
pdfOutline,
|
|
183
|
+
pdfBackground,
|
|
182
184
|
pdfTitle,
|
|
183
185
|
pdfAuthor,
|
|
184
186
|
pdfSubject,
|
|
@@ -216,9 +218,9 @@ export async function resolveHtmlSource(orch) {
|
|
|
216
218
|
return value;
|
|
217
219
|
}
|
|
218
220
|
if (typeof url === "string") {
|
|
219
|
-
// Same as resource fetch: User-Agent override is Node-only. In browsers
|
|
221
|
+
// Same as resource fetch: User-Agent override is Node/Workers-only. In browsers
|
|
220
222
|
// it is forbidden and triggers a Firefox preflight that breaks CORS.
|
|
221
|
-
const headers =
|
|
223
|
+
const headers = canSetUserAgent()
|
|
222
224
|
? { "User-Agent": userAgent ?? DEFAULT_USER_AGENT }
|
|
223
225
|
: undefined;
|
|
224
226
|
const fetchStart = now();
|
package/dist/core-resources.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ import { LogLevel, type DiagnosticMessage, type FontDiagnostic, type RenderLimit
|
|
|
11
11
|
/** Drop every cached resource (e.g. after the source assets changed). */
|
|
12
12
|
export declare function clearResourceCache(): void;
|
|
13
13
|
export declare function isNodeRuntime(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether `fetch` may send a `User-Agent` of our choosing: Node and Cloudflare Workers can; browsers
|
|
16
|
+
* cannot (and Firefox preflights it, which breaks Google Fonts / jsDelivr CORS). The UA decides what
|
|
17
|
+
* Google Fonts serves: a browser UA gets woff2 split by unicode-range (a few KB per page), anything
|
|
18
|
+
* else (a Worker's own UA, none) gets the whole `.ttf` per weight (about 5 MB each, embedded whole
|
|
19
|
+
* into a PDF).
|
|
20
|
+
*/
|
|
21
|
+
export declare function canSetUserAgent(): boolean;
|
|
14
22
|
/**
|
|
15
23
|
* Fetch external resource bytes. `data:` URLs resolve inline in C++
|
|
16
24
|
* (`request()`), so they are skipped here. Relative URLs resolve against
|
package/dist/core-resources.js
CHANGED
|
@@ -61,6 +61,19 @@ export function isNodeRuntime() {
|
|
|
61
61
|
.process;
|
|
62
62
|
return typeof proc?.versions?.node === "string";
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether `fetch` may send a `User-Agent` of our choosing: Node and Cloudflare Workers can; browsers
|
|
66
|
+
* cannot (and Firefox preflights it, which breaks Google Fonts / jsDelivr CORS). The UA decides what
|
|
67
|
+
* Google Fonts serves: a browser UA gets woff2 split by unicode-range (a few KB per page), anything
|
|
68
|
+
* else (a Worker's own UA, none) gets the whole `.ttf` per weight (about 5 MB each, embedded whole
|
|
69
|
+
* into a PDF).
|
|
70
|
+
*/
|
|
71
|
+
export function canSetUserAgent() {
|
|
72
|
+
if (isNodeRuntime())
|
|
73
|
+
return true;
|
|
74
|
+
const nav = globalThis.navigator;
|
|
75
|
+
return nav?.userAgent === "Cloudflare-Workers";
|
|
76
|
+
}
|
|
64
77
|
function fileUrlToFsPath(p) {
|
|
65
78
|
if (!p.startsWith("file://"))
|
|
66
79
|
return p;
|
|
@@ -103,9 +116,9 @@ async function isInsideDir(fs, path, root, target) {
|
|
|
103
116
|
/** Built-in resolution behind the `resolveResource` hook (see above). */
|
|
104
117
|
async function fetchResourceBytesInner(url, baseUrl, userAgent, timeoutMs, confineFiles) {
|
|
105
118
|
// Browser fetch forbids overriding User-Agent (and Firefox preflights it,
|
|
106
|
-
// breaking Google Fonts/jsDelivr CORS). Spoof only
|
|
107
|
-
//
|
|
108
|
-
const headers = userAgent &&
|
|
119
|
+
// breaking Google Fonts/jsDelivr CORS). Spoof only where we control the UA
|
|
120
|
+
// (Node, Workers) to get woff2 CSS.
|
|
121
|
+
const headers = userAgent && canSetUserAgent() ? { "User-Agent": userAgent } : undefined;
|
|
109
122
|
// Abort hung connections so one stalled server cannot outlive `timeoutMs`.
|
|
110
123
|
const signal = timeoutMs !== undefined
|
|
111
124
|
? // Node >= 24 requires an integer delay; the remaining budget is fractional.
|
package/dist/core.d.ts
CHANGED
|
@@ -200,6 +200,16 @@ export interface HtmlToImageOptions extends RenderOptions {
|
|
|
200
200
|
* gamut-mapped. Other formats ignore it.
|
|
201
201
|
*/
|
|
202
202
|
colorSpace?: "srgb" | "cmyk";
|
|
203
|
+
/**
|
|
204
|
+
* PDF outline (bookmarks) built from the document's `h1`-`h6`. On by default; `false` leaves it out.
|
|
205
|
+
*/
|
|
206
|
+
pdfOutline?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* PDF page background, painted over the whole page (margins included) before the content: `#rrggbb`
|
|
209
|
+
* or `#rrggbbaa`. Without it only the content area takes the document's `background`, so a dark page
|
|
210
|
+
* would show white margins. Other formats ignore it.
|
|
211
|
+
*/
|
|
212
|
+
pdfBackground?: string;
|
|
203
213
|
/** PDF Title metadata. */
|
|
204
214
|
pdfTitle?: string;
|
|
205
215
|
/** PDF Author metadata. */
|
package/dist/core.js
CHANGED
|
@@ -48,6 +48,8 @@ export async function htmlToImage(module, options) {
|
|
|
48
48
|
mediaTypeInt: orch.mediaTypeInt,
|
|
49
49
|
colorSpace: orch.colorSpace,
|
|
50
50
|
scale: orch.scale,
|
|
51
|
+
pdfOutline: orch.pdfOutline,
|
|
52
|
+
pdfBackground: orch.pdfBackground,
|
|
51
53
|
pdfTitle: orch.pdfTitle,
|
|
52
54
|
pdfAuthor: orch.pdfAuthor,
|
|
53
55
|
pdfSubject: orch.pdfSubject,
|
package/dist/encode-args.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface FullSatoruOptionsInput {
|
|
|
18
18
|
mediaTypeInt: number;
|
|
19
19
|
colorSpace?: "srgb" | "cmyk";
|
|
20
20
|
scale?: number;
|
|
21
|
+
pdfOutline?: boolean;
|
|
22
|
+
pdfBackground?: string;
|
|
21
23
|
pdfTitle?: string;
|
|
22
24
|
pdfAuthor?: string;
|
|
23
25
|
pdfSubject?: string;
|
|
@@ -38,3 +40,8 @@ export interface FullSatoruOptionsInput {
|
|
|
38
40
|
* `core.ts` HTML パスはこの関数のみを使い、個別組み立てをしない。
|
|
39
41
|
*/
|
|
40
42
|
export declare function buildFullSatoruOptions(input: FullSatoruOptionsInput): Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* `#rgb`, `#rrggbb` or `#rrggbbaa` as the engine's ARGB integer (`0xAARRGGBB`). Anything else is an error:
|
|
45
|
+
* a mistyped colour should not silently paint nothing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseCssColor(color: string): number;
|
package/dist/encode-args.js
CHANGED
|
@@ -39,13 +39,16 @@ export function buildSatoruOptions(crop, fit) {
|
|
|
39
39
|
* `core.ts` HTML パスはこの関数のみを使い、個別組み立てをしない。
|
|
40
40
|
*/
|
|
41
41
|
export function buildFullSatoruOptions(input) {
|
|
42
|
-
const { textToPaths, mediaTypeInt, pdfTitle, pdfAuthor, pdfSubject, pdfKeywords, pdfCreator, pdfProducer, pdfMargin, pdfHeader, pdfFooter, } = input;
|
|
42
|
+
const { textToPaths, mediaTypeInt, pdfOutline, pdfBackground, pdfTitle, pdfAuthor, pdfSubject, pdfKeywords, pdfCreator, pdfProducer, pdfMargin, pdfHeader, pdfFooter, } = input;
|
|
43
43
|
return {
|
|
44
44
|
...buildSatoruOptions(input.crop, input.fit),
|
|
45
45
|
svgTextToPaths: textToPaths ?? true,
|
|
46
46
|
mediaType: mediaTypeInt,
|
|
47
47
|
...(input.colorSpace === "cmyk" ? { colorSpace: 1 } : {}),
|
|
48
48
|
...(input.scale !== undefined && input.scale !== 1 ? { scale: input.scale } : {}),
|
|
49
|
+
// The engine's default is on: only an explicit false has to be sent.
|
|
50
|
+
...(pdfOutline === false ? { pdfOutline: false } : {}),
|
|
51
|
+
...(pdfBackground !== undefined ? { backgroundColor: parseCssColor(pdfBackground) } : {}),
|
|
49
52
|
pdfTitle: pdfTitle ?? "",
|
|
50
53
|
pdfAuthor: pdfAuthor ?? "",
|
|
51
54
|
pdfSubject: pdfSubject ?? "",
|
|
@@ -60,3 +63,17 @@ export function buildFullSatoruOptions(input) {
|
|
|
60
63
|
pdfFooter: pdfFooter ?? "",
|
|
61
64
|
};
|
|
62
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* `#rgb`, `#rrggbb` or `#rrggbbaa` as the engine's ARGB integer (`0xAARRGGBB`). Anything else is an error:
|
|
68
|
+
* a mistyped colour should not silently paint nothing.
|
|
69
|
+
*/
|
|
70
|
+
export function parseCssColor(color) {
|
|
71
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(color.trim());
|
|
72
|
+
if (!m)
|
|
73
|
+
throw new Error(`invalid colour "${color}": use #rgb, #rrggbb or #rrggbbaa`);
|
|
74
|
+
let hex = m[1];
|
|
75
|
+
if (hex.length === 3)
|
|
76
|
+
hex = hex.split("").map((c) => c + c).join("");
|
|
77
|
+
const a = hex.length === 8 ? hex.slice(6, 8) : "ff";
|
|
78
|
+
return parseInt(a + hex.slice(0, 6), 16) >>> 0;
|
|
79
|
+
}
|