wasm-html-to-image 1.0.21 → 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.
@@ -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. */
@@ -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 source = isInlineHtml(input)
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("<input>", "input HTML/image file path, URL, inline HTML string (starts with <), or - for stdin")
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;
@@ -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 { isNodeRuntime, now, resolveHtmlResources, } from "./core-resources.js";
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 = isNodeRuntime()
223
+ const headers = canSetUserAgent()
222
224
  ? { "User-Agent": userAgent ?? DEFAULT_USER_AGENT }
223
225
  : undefined;
224
226
  const fetchStart = now();
@@ -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
@@ -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 in Node where we
107
- // control the UA to get woff2 CSS.
108
- const headers = userAgent && isNodeRuntime() ? { "User-Agent": userAgent } : undefined;
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,
@@ -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;
@@ -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
+ }