bookcover-web 0.1.0
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 +92 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
- package/src/index.ts +412 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
# bookcover-web
|
|
3
|
+
|
|
4
|
+
Generate print-ready book covers. Part of the [bookcover](https://github.com/gracious-tech/bookcover) generation system.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm install bookcover-web
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
[](../sample.pdf "View example PDF")
|
|
11
|
+
|
|
12
|
+
This is a browser wrapper for the book cover generator. It compiles covers via WASM using
|
|
13
|
+
[typst.ts](https://github.com/Myriad-Dreamin/typst.ts).
|
|
14
|
+
|
|
15
|
+
## See the [schema reference](https://github.com/gracious-tech/bookcover/blob/main/generator/README.md) which is passed as an argument and is what actually configures the cover design.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {init, CoverGenerator} from 'bookcover-web'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## `init(options): Promise<CoverGenerator>`
|
|
22
|
+
|
|
23
|
+
Create a generator instance with an initialised WASM compiler and optional renderer.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface InitOptions {
|
|
27
|
+
wasm_url: string // URL to typst_ts_web_compiler_bg.wasm
|
|
28
|
+
renderer_wasm_url?: string // URL to typst_ts_renderer_bg.wasm (required for SVG/PNG)
|
|
29
|
+
assets_prefix?: string // URL prefix for generator assets (fonts, templates, frames)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**`wasm_url` / `renderer_wasm_url`** — serve the `.wasm` files from your origin. With Vite, import them directly:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import wasm_url from '@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm?url'
|
|
37
|
+
import renderer_wasm_url from '@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm?url'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Without a bundler, copy the files from `node_modules` into your `public/` dir and reference by path.
|
|
41
|
+
|
|
42
|
+
**`assets_prefix`** — points to the `generator/assets/` directory (fonts, Typst templates, frame images). The easiest setup is a symlink from your `public/` dir:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ln -s ../node_modules/bookcover-core/assets public/generator_assets
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then pass `assets_prefix: '/generator_assets/'`. With Vite the symlink is served automatically at dev time and included in the production build.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## `CoverGenerator.generate(options): Promise<GenerateResult>`
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
interface GenerateOptions {
|
|
55
|
+
schema: unknown // raw schema (see above)
|
|
56
|
+
image?: Blob // background image
|
|
57
|
+
format?: 'pdf'|'svg'|'png' // default: 'pdf'
|
|
58
|
+
ppi?: number // PNG resolution, default: 144
|
|
59
|
+
split?: boolean // include split panels in result
|
|
60
|
+
custom_fonts?: Uint8Array[] // raw TTF bytes for custom fonts
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface GenerateResult {
|
|
64
|
+
data: Uint8Array | string // full cover (bytes for PDF/PNG, string for SVG)
|
|
65
|
+
split?: SplitResult<Uint8Array> | SplitResult<string>
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## `CoverGenerator.destroy()`
|
|
70
|
+
|
|
71
|
+
Revoke blob URLs to free memory. Call when the instance is no longer needed.
|
|
72
|
+
|
|
73
|
+
### Example
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {init} from 'bookcover-web'
|
|
77
|
+
|
|
78
|
+
const generator = await init({
|
|
79
|
+
wasm_url: '/typst_ts_web_compiler_bg.wasm',
|
|
80
|
+
renderer_wasm_url: '/typst_ts_renderer_bg.wasm',
|
|
81
|
+
assets_prefix: '/generator_assets/',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const result = await generator.generate({
|
|
85
|
+
schema: {title1: 'My Book', author: 'Jane Doe', /* ... */},
|
|
86
|
+
format: 'svg',
|
|
87
|
+
split: true,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// result.data is the full SVG string
|
|
91
|
+
// result.split.front / .back / .spine are individual panel SVGs
|
|
92
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TypstRenderer } from '@myriaddreamin/typst.ts/renderer';
|
|
2
|
+
import type { OutputFormat, SplitResult } from 'bookcover-core';
|
|
3
|
+
export type { CoverSchema, TitlePosition, FontConfig, OutputFormat, SplitResult, PatternDef } from 'bookcover-core';
|
|
4
|
+
export type { BundledFont, CjkVariant } from 'typst-fonts';
|
|
5
|
+
export { font_file_url } from 'typst-fonts/web';
|
|
6
|
+
export { get_fonts, get_bundled_font } from 'typst-fonts';
|
|
7
|
+
export { list_patterns, collect_fonts, collect_all_fonts, default_spine_title } from 'bookcover-core';
|
|
8
|
+
export interface InitOptions {
|
|
9
|
+
wasm_url: string;
|
|
10
|
+
renderer_wasm_url?: string;
|
|
11
|
+
assets_prefix?: string;
|
|
12
|
+
fonts_prefix?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GenerateOptions {
|
|
15
|
+
schema: unknown;
|
|
16
|
+
image?: Blob;
|
|
17
|
+
format?: OutputFormat;
|
|
18
|
+
ppi?: number;
|
|
19
|
+
split?: boolean;
|
|
20
|
+
custom_fonts?: Uint8Array[];
|
|
21
|
+
}
|
|
22
|
+
export interface GenerateResult {
|
|
23
|
+
data: Uint8Array | string;
|
|
24
|
+
split?: SplitResult<Uint8Array> | SplitResult<string>;
|
|
25
|
+
}
|
|
26
|
+
/** Stateful cover generator — each instance owns its own compiler and renderer */
|
|
27
|
+
export declare class CoverGenerator {
|
|
28
|
+
private opts;
|
|
29
|
+
private compiler;
|
|
30
|
+
private renderer;
|
|
31
|
+
private active_fonts;
|
|
32
|
+
private font_bytes;
|
|
33
|
+
private font_blob_urls;
|
|
34
|
+
private custom_font_ids;
|
|
35
|
+
private next_custom_font_id;
|
|
36
|
+
constructor(opts: InitOptions, renderer: TypstRenderer | null);
|
|
37
|
+
/** Build fetch URLs for the given font families against fonts_prefix — curated families
|
|
38
|
+
* live at <prefix>/<family>/, Noto fallback families at <prefix>/_noto/<family>/,
|
|
39
|
+
* mirroring the top-level fonts/ directory's own layout exactly */
|
|
40
|
+
private font_urls_for;
|
|
41
|
+
/** Fetch a font file's bytes, memoised in font_bytes (failures aren't cached, so a
|
|
42
|
+
* later generate retries the fetch) */
|
|
43
|
+
private fetch_font;
|
|
44
|
+
/** Pre-fetch the given font families into the in-memory byte cache */
|
|
45
|
+
prefetch_fonts(families: string[]): Promise<void>;
|
|
46
|
+
/** Stable id for a custom font's byte array. The widget passes the same Uint8Array
|
|
47
|
+
* references across generates, so object identity distinguishes fonts without hashing
|
|
48
|
+
* their bytes (byteLength alone could collide between two different fonts). */
|
|
49
|
+
private custom_font_id;
|
|
50
|
+
/** (Re)initialise the compiler with a different font set. Bundled font bytes come from
|
|
51
|
+
* the in-memory cache (fetched at most once per session) and everything is handed to
|
|
52
|
+
* loadFonts() as blob URLs, keeping the network out of the reinit path. The renderer
|
|
53
|
+
* never needs reinitialisation because it reads glyph outlines from compiled vector
|
|
54
|
+
* data. */
|
|
55
|
+
private reinit_compiler;
|
|
56
|
+
/** Load the typst file map into the compiler's shadow filesystem */
|
|
57
|
+
private load_files;
|
|
58
|
+
/** Compile to PDF and return raw bytes */
|
|
59
|
+
private compile_pdf;
|
|
60
|
+
/** Compile to vector format, then render as SVG string covering the full canvas */
|
|
61
|
+
private compile_svg;
|
|
62
|
+
/** Compile to vector format, then render as PNG via offscreen canvas */
|
|
63
|
+
private compile_png;
|
|
64
|
+
/** Throw if the renderer was not initialised */
|
|
65
|
+
private assert_renderer;
|
|
66
|
+
/** Generate a book cover using this instance's compiler and renderer */
|
|
67
|
+
generate(options: GenerateOptions): Promise<GenerateResult>;
|
|
68
|
+
/** Revoke blob URLs and drop cached font bytes to free memory */
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a CoverGenerator instance with an optional initialised WASM renderer.
|
|
73
|
+
* The compiler itself is created lazily by the first generate() call, once the schema's
|
|
74
|
+
* font set is known. Each instance is independent — multiple generators can run concurrently.
|
|
75
|
+
*/
|
|
76
|
+
export declare function init(options: InitOptions): Promise<CoverGenerator>;
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,kCAAkC,CAAA;AAInE,OAAO,KAAK,EAAC,YAAY,EAAE,WAAW,EAAY,MAAM,gBAAgB,CAAA;AAKxE,YAAY,EAAC,WAAW,EAAE,aAAa,EAAE,UAAU,EAC/C,YAAY,EAAE,WAAW,EAAE,UAAU,EAAC,MAAM,gBAAgB,CAAA;AAChE,YAAY,EAAC,WAAW,EAAE,UAAU,EAAC,MAAM,aAAa,CAAA;AACxD,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAC,SAAS,EAAE,gBAAgB,EAAC,MAAM,aAAa,CAAA;AACvD,OAAO,EAAC,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAA;AAInG,MAAM,WAAW,WAAW;IAExB,QAAQ,EAAC,MAAM,CAAA;IAEf,iBAAiB,CAAC,EAAC,MAAM,CAAA;IAGzB,aAAa,CAAC,EAAC,MAAM,CAAA;IAKrB,YAAY,CAAC,EAAC,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAC,OAAO,CAAA;IAEd,KAAK,CAAC,EAAC,IAAI,CAAA;IAEX,MAAM,CAAC,EAAC,YAAY,CAAA;IAEpB,GAAG,CAAC,EAAC,MAAM,CAAA;IAEX,KAAK,CAAC,EAAC,OAAO,CAAA;IAEd,YAAY,CAAC,EAAC,UAAU,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAE3B,IAAI,EAAC,UAAU,GAAG,MAAM,CAAA;IAExB,KAAK,CAAC,EAAC,WAAW,CAAC,UAAU,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAA;CACvD;AAmDD,kFAAkF;AAClF,qBAAa,cAAc;IACvB,OAAO,CAAC,IAAI,CAAY;IAGxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAqB;IAGrC,OAAO,CAAC,YAAY,CAAK;IAGzB,OAAO,CAAC,UAAU,CAAgC;IAElD,OAAO,CAAC,cAAc,CAAc;IAEpC,OAAO,CAAC,eAAe,CAAoC;IAC3D,OAAO,CAAC,mBAAmB,CAAI;gBAEnB,IAAI,EAAC,WAAW,EAAE,QAAQ,EAAC,aAAa,GAAG,IAAI;IAK3D;;wEAEoE;IACpE,OAAO,CAAC,aAAa;IAIrB;4CACwC;YAC1B,UAAU;IAUxB,sEAAsE;IAChE,cAAc,CAAC,QAAQ,EAAC,MAAM,EAAE,GAAE,OAAO,CAAC,IAAI,CAAC;IAIrD;;oFAEgF;IAChF,OAAO,CAAC,cAAc;IAUtB;;;;gBAIY;YACE,eAAe;IAwB7B,oEAAoE;IACpE,OAAO,CAAC,UAAU;IAalB,0CAA0C;YAC5B,WAAW;IAWzB,mFAAmF;YACrE,WAAW;IA0BzB,wEAAwE;YAC1D,WAAW;IA8BzB,gDAAgD;IAChD,OAAO,CAAC,eAAe;IAQvB,wEAAwE;IAClE,QAAQ,CAAC,OAAO,EAAC,eAAe,GAAE,OAAO,CAAC,cAAc,CAAC;IAkF/D,iEAAiE;IACjE,OAAO,IAAG,IAAI;CAKjB;AAED;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAC,WAAW,GAAE,OAAO,CAAC,cAAc,CAAC,CAuBtE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Browser cover generator — compiles typst files via WebAssembly, supports PDF/SVG/PNG output
|
|
2
|
+
import { createTypstCompiler, CompileFormatEnum } from '@myriaddreamin/typst.ts/compiler';
|
|
3
|
+
import { createTypstRenderer } from '@myriaddreamin/typst.ts/renderer';
|
|
4
|
+
import { loadFonts } from '@myriaddreamin/typst.ts';
|
|
5
|
+
import { build, cover_schema, split_svg, split_png, split_pdf, frame_image, frame_asset_path, asset_path, TYPST_DIR, TEMPLATE_FILES, collect_all_fonts } from 'bookcover-core';
|
|
6
|
+
import { base_font } from 'typst-fonts';
|
|
7
|
+
import { load_fonts_prefix, font_urls_for as build_font_urls, fetch_font_bytes, fonts_to_blob_urls, revoke_blob_urls } from 'typst-fonts/web';
|
|
8
|
+
export { font_file_url } from 'typst-fonts/web';
|
|
9
|
+
export { get_fonts, get_bundled_font } from 'typst-fonts';
|
|
10
|
+
export { list_patterns, collect_fonts, collect_all_fonts, default_spine_title } from 'bookcover-core';
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
/** Format compilation diagnostics into a readable error */
|
|
13
|
+
function throw_compile_error(diagnostics) {
|
|
14
|
+
const diag = Array.isArray(diagnostics)
|
|
15
|
+
? diagnostics.map((d) => (typeof d === 'string' ? d : JSON.stringify(d))).join('\n')
|
|
16
|
+
: 'unknown error';
|
|
17
|
+
throw new Error(`[generator-web] Typst compilation failed:\n${diag}`);
|
|
18
|
+
}
|
|
19
|
+
/** Create a 2D canvas — a DOM element on the main thread, OffscreenCanvas inside a worker */
|
|
20
|
+
function make_canvas(width, height) {
|
|
21
|
+
if (typeof document === 'undefined') {
|
|
22
|
+
return new OffscreenCanvas(width, height);
|
|
23
|
+
}
|
|
24
|
+
const canvas = document.createElement('canvas');
|
|
25
|
+
canvas.width = width;
|
|
26
|
+
canvas.height = height;
|
|
27
|
+
return canvas;
|
|
28
|
+
}
|
|
29
|
+
/** Encode a canvas' contents as PNG bytes (handles both canvas types) */
|
|
30
|
+
async function canvas_png_bytes(canvas) {
|
|
31
|
+
const blob = 'convertToBlob' in canvas
|
|
32
|
+
? await canvas.convertToBlob({ type: 'image/png' })
|
|
33
|
+
: await new Promise((resolve) => {
|
|
34
|
+
canvas.toBlob((b) => resolve(b), 'image/png');
|
|
35
|
+
});
|
|
36
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
37
|
+
}
|
|
38
|
+
/** Crop a region from a PNG using the Canvas API — used as PngCropFn */
|
|
39
|
+
async function canvas_crop(data, x, y, w, h) {
|
|
40
|
+
const blob = new Blob([data], { type: 'image/png' });
|
|
41
|
+
const bitmap = await createImageBitmap(blob);
|
|
42
|
+
const canvas = make_canvas(w, h);
|
|
43
|
+
const ctx = canvas.getContext('2d');
|
|
44
|
+
ctx.drawImage(bitmap, x, y, w, h, 0, 0, w, h);
|
|
45
|
+
bitmap.close();
|
|
46
|
+
return canvas_png_bytes(canvas);
|
|
47
|
+
}
|
|
48
|
+
/** Stateful cover generator — each instance owns its own compiler and renderer */
|
|
49
|
+
export class CoverGenerator {
|
|
50
|
+
opts;
|
|
51
|
+
// Created lazily on first generate() — the font set isn't known until a schema arrives,
|
|
52
|
+
// so creating one earlier would just instantiate WASM with fonts that get replaced
|
|
53
|
+
compiler;
|
|
54
|
+
renderer;
|
|
55
|
+
// Cache key of the font set the current compiler was init'd with ('' = no compiler yet)
|
|
56
|
+
active_fonts = '';
|
|
57
|
+
// Fetched font bytes keyed by URL, so a compiler reinit (any font-set change) never
|
|
58
|
+
// refetches families already seen this session
|
|
59
|
+
font_bytes = new Map();
|
|
60
|
+
// Blob URLs handed to the current compiler (bundled + custom) — revoked on reinit
|
|
61
|
+
font_blob_urls = [];
|
|
62
|
+
// Identity-based ids for custom font byte arrays (see custom_font_id)
|
|
63
|
+
custom_font_ids = new WeakMap();
|
|
64
|
+
next_custom_font_id = 1;
|
|
65
|
+
constructor(opts, renderer) {
|
|
66
|
+
this.opts = opts;
|
|
67
|
+
this.renderer = renderer;
|
|
68
|
+
}
|
|
69
|
+
/** Build fetch URLs for the given font families against fonts_prefix — curated families
|
|
70
|
+
* live at <prefix>/<family>/, Noto fallback families at <prefix>/_noto/<family>/,
|
|
71
|
+
* mirroring the top-level fonts/ directory's own layout exactly */
|
|
72
|
+
font_urls_for(families) {
|
|
73
|
+
return build_font_urls(this.opts.fonts_prefix ?? '/fonts/', families);
|
|
74
|
+
}
|
|
75
|
+
/** Fetch a font file's bytes, memoised in font_bytes (failures aren't cached, so a
|
|
76
|
+
* later generate retries the fetch) */
|
|
77
|
+
async fetch_font(url) {
|
|
78
|
+
const cached = this.font_bytes.get(url);
|
|
79
|
+
if (cached) {
|
|
80
|
+
return cached;
|
|
81
|
+
}
|
|
82
|
+
const bytes = await fetch_font_bytes(url);
|
|
83
|
+
this.font_bytes.set(url, bytes);
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
/** Pre-fetch the given font families into the in-memory byte cache */
|
|
87
|
+
async prefetch_fonts(families) {
|
|
88
|
+
await Promise.all(this.font_urls_for(families).map(url => this.fetch_font(url)));
|
|
89
|
+
}
|
|
90
|
+
/** Stable id for a custom font's byte array. The widget passes the same Uint8Array
|
|
91
|
+
* references across generates, so object identity distinguishes fonts without hashing
|
|
92
|
+
* their bytes (byteLength alone could collide between two different fonts). */
|
|
93
|
+
custom_font_id(data) {
|
|
94
|
+
let id = this.custom_font_ids.get(data);
|
|
95
|
+
if (id === undefined) {
|
|
96
|
+
id = this.next_custom_font_id;
|
|
97
|
+
this.next_custom_font_id += 1;
|
|
98
|
+
this.custom_font_ids.set(data, id);
|
|
99
|
+
}
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
/** (Re)initialise the compiler with a different font set. Bundled font bytes come from
|
|
103
|
+
* the in-memory cache (fetched at most once per session) and everything is handed to
|
|
104
|
+
* loadFonts() as blob URLs, keeping the network out of the reinit path. The renderer
|
|
105
|
+
* never needs reinitialisation because it reads glyph outlines from compiled vector
|
|
106
|
+
* data. */
|
|
107
|
+
async reinit_compiler(font_urls, custom_fonts) {
|
|
108
|
+
// Fetch any fonts not yet cached, in parallel
|
|
109
|
+
const bundled_bytes = await Promise.all(font_urls.map(url => this.fetch_font(url)));
|
|
110
|
+
// Revoke the previous compiler's blob URLs to avoid memory leaks
|
|
111
|
+
revoke_blob_urls(this.font_blob_urls);
|
|
112
|
+
// One blob URL per font file (bundled + custom) for loadFonts() to read —
|
|
113
|
+
// mapShadow only adds to the virtual filesystem, not the font book
|
|
114
|
+
this.font_blob_urls = fonts_to_blob_urls([...bundled_bytes, ...(custom_fonts ?? [])]);
|
|
115
|
+
// assets:false stops typst.ts silently appending its default text fonts (NewCM,
|
|
116
|
+
// Libertinus, DejaVu... fetched from jsdelivr) — all fonts we need are passed explicitly
|
|
117
|
+
const compiler_font_opts = loadFonts(this.font_blob_urls, { assets: false });
|
|
118
|
+
const c = createTypstCompiler();
|
|
119
|
+
await c.init({
|
|
120
|
+
getModule: () => ({ module_or_path: this.opts.wasm_url }),
|
|
121
|
+
beforeBuild: [compiler_font_opts],
|
|
122
|
+
});
|
|
123
|
+
this.compiler = c;
|
|
124
|
+
}
|
|
125
|
+
/** Load the typst file map into the compiler's shadow filesystem */
|
|
126
|
+
load_files(files) {
|
|
127
|
+
this.compiler.resetShadow();
|
|
128
|
+
for (const [filename, bytes] of files) {
|
|
129
|
+
const vpath = `/${filename}`;
|
|
130
|
+
if (filename.endsWith('.typ')) {
|
|
131
|
+
this.compiler.addSource(vpath, decoder.decode(bytes));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.compiler.mapShadow(vpath, bytes);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Compile to PDF and return raw bytes */
|
|
139
|
+
async compile_pdf() {
|
|
140
|
+
const result = await this.compiler.compile({
|
|
141
|
+
mainFilePath: '/cover.typ',
|
|
142
|
+
format: CompileFormatEnum.pdf,
|
|
143
|
+
});
|
|
144
|
+
if (!result.result) {
|
|
145
|
+
throw_compile_error(result.diagnostics);
|
|
146
|
+
}
|
|
147
|
+
return result.result;
|
|
148
|
+
}
|
|
149
|
+
/** Compile to vector format, then render as SVG string covering the full canvas */
|
|
150
|
+
async compile_svg() {
|
|
151
|
+
this.assert_renderer('SVG');
|
|
152
|
+
const result = await this.compiler.compile({
|
|
153
|
+
mainFilePath: '/cover.typ',
|
|
154
|
+
format: CompileFormatEnum.vector,
|
|
155
|
+
});
|
|
156
|
+
if (!result.result) {
|
|
157
|
+
throw_compile_error(result.diagnostics);
|
|
158
|
+
}
|
|
159
|
+
const svg = await this.renderer.renderSvg({
|
|
160
|
+
artifactContent: result.result,
|
|
161
|
+
format: 'vector',
|
|
162
|
+
data_selection: { js: false, css: true, body: true, defs: true },
|
|
163
|
+
});
|
|
164
|
+
// Patch unitless width/height to add `pt` suffix (Typst SVG output uses pt) so
|
|
165
|
+
// browsers render at 96 CSS px per inch, matching the split SVGs from split_svg()
|
|
166
|
+
return svg.replace(/<svg([^>]*)>/, (_match, attrs) => {
|
|
167
|
+
const patched = attrs
|
|
168
|
+
.replace(/width="([\d.]+)"/, 'width="$1pt"')
|
|
169
|
+
.replace(/height="([\d.]+)"/, 'height="$1pt"');
|
|
170
|
+
return `<svg${patched}>`;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Compile to vector format, then render as PNG via offscreen canvas */
|
|
174
|
+
async compile_png(ppi) {
|
|
175
|
+
this.assert_renderer('PNG');
|
|
176
|
+
const result = await this.compiler.compile({
|
|
177
|
+
mainFilePath: '/cover.typ',
|
|
178
|
+
format: CompileFormatEnum.vector,
|
|
179
|
+
});
|
|
180
|
+
if (!result.result) {
|
|
181
|
+
throw_compile_error(result.diagnostics);
|
|
182
|
+
}
|
|
183
|
+
// Render to a canvas at the desired PPI (typst uses 72pt/inch)
|
|
184
|
+
const scale = ppi / 72;
|
|
185
|
+
const canvas = document.createElement('canvas');
|
|
186
|
+
await this.renderer.renderCanvas({
|
|
187
|
+
artifactContent: result.result,
|
|
188
|
+
format: 'vector',
|
|
189
|
+
canvas,
|
|
190
|
+
pageOffset: 0,
|
|
191
|
+
pixelPerPt: scale,
|
|
192
|
+
backgroundColor: '#ffffff',
|
|
193
|
+
});
|
|
194
|
+
// Extract PNG from canvas
|
|
195
|
+
const blob = await new Promise((resolve) => {
|
|
196
|
+
canvas.toBlob((b) => resolve(b), 'image/png');
|
|
197
|
+
});
|
|
198
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
199
|
+
}
|
|
200
|
+
/** Throw if the renderer was not initialised */
|
|
201
|
+
assert_renderer(format) {
|
|
202
|
+
if (!this.renderer) {
|
|
203
|
+
throw new Error(`[generator-web] ${format} output requires renderer_wasm_url in init() options`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Generate a book cover using this instance's compiler and renderer */
|
|
207
|
+
async generate(options) {
|
|
208
|
+
const format = options.format ?? 'pdf';
|
|
209
|
+
// Default to 144 PPI (2x typographic resolution, 2 × 72pt/in)
|
|
210
|
+
const ppi = options.ppi ?? 144;
|
|
211
|
+
const split = options.split ?? false;
|
|
212
|
+
// Parse schema and get dimensions from printing-services
|
|
213
|
+
const parsed = cover_schema.parse(options.schema);
|
|
214
|
+
const assets = this.opts.assets_prefix ?? '/assets/';
|
|
215
|
+
// (Re)initialise the compiler when the set of required fonts changes — including on
|
|
216
|
+
// the very first generate, since the compiler is created lazily ('' never matches).
|
|
217
|
+
// Noto Serif (base font) is always included — it's not bundled in the WASM assets.
|
|
218
|
+
// collect_all_fonts also includes every Noto fallback family needed to cover non-Latin
|
|
219
|
+
// scripts detected in the schema's text, so only those specific families get fetched.
|
|
220
|
+
const needed_fonts = collect_all_fonts(parsed);
|
|
221
|
+
const custom_fonts_id = options.custom_fonts
|
|
222
|
+
? options.custom_fonts.map(b => String(this.custom_font_id(b))).join(':')
|
|
223
|
+
: '';
|
|
224
|
+
const cache_key = needed_fonts.join(',') + '|' + custom_fonts_id;
|
|
225
|
+
if (cache_key !== this.active_fonts) {
|
|
226
|
+
await this.reinit_compiler(this.font_urls_for(needed_fonts), options.custom_fonts);
|
|
227
|
+
this.active_fonts = cache_key;
|
|
228
|
+
}
|
|
229
|
+
// Convert Blob to ImageInput (Uint8Array + extension) for the core builder
|
|
230
|
+
let image_input;
|
|
231
|
+
if (options.image) {
|
|
232
|
+
const blob = options.image;
|
|
233
|
+
const ext = '.' + (blob.type.split('/')[1] ?? 'jpg').replace('jpeg', 'jpg');
|
|
234
|
+
image_input = { data: new Uint8Array(await blob.arrayBuffer()), ext };
|
|
235
|
+
}
|
|
236
|
+
// Load typst templates from assets
|
|
237
|
+
const [cover_typ, helpers_typ] = await Promise.all([
|
|
238
|
+
fetch(asset_path(assets, TYPST_DIR, TEMPLATE_FILES.cover)).then(r => r.text()),
|
|
239
|
+
fetch(asset_path(assets, TYPST_DIR, TEMPLATE_FILES.helpers)).then(r => r.text()),
|
|
240
|
+
]);
|
|
241
|
+
const templates = { cover: cover_typ, helpers: helpers_typ };
|
|
242
|
+
// Load the frame PNG from assets when the schema uses painted coverage
|
|
243
|
+
let frame_blob;
|
|
244
|
+
if (parsed.bg_image_coverage === 'painted') {
|
|
245
|
+
const url = frame_asset_path(assets, 'painted');
|
|
246
|
+
const resp = await fetch(url);
|
|
247
|
+
frame_blob = await resp.blob();
|
|
248
|
+
}
|
|
249
|
+
const { files, dims } = await build(templates, parsed, image_input, frame_image, frame_blob);
|
|
250
|
+
this.load_files(files);
|
|
251
|
+
// Compile to the requested format
|
|
252
|
+
let data;
|
|
253
|
+
if (format === 'pdf') {
|
|
254
|
+
data = await this.compile_pdf();
|
|
255
|
+
}
|
|
256
|
+
else if (format === 'svg') {
|
|
257
|
+
data = await this.compile_svg();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
data = await this.compile_png(ppi);
|
|
261
|
+
}
|
|
262
|
+
const result = { data };
|
|
263
|
+
// Split into front/back/spine panels
|
|
264
|
+
if (split) {
|
|
265
|
+
if (format === 'pdf') {
|
|
266
|
+
result.split = split_pdf(data, dims);
|
|
267
|
+
}
|
|
268
|
+
else if (format === 'svg') {
|
|
269
|
+
result.split = split_svg(data, dims);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
result.split = await split_png(data, dims, ppi, canvas_crop);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
/** Revoke blob URLs and drop cached font bytes to free memory */
|
|
278
|
+
destroy() {
|
|
279
|
+
revoke_blob_urls(this.font_blob_urls);
|
|
280
|
+
this.font_blob_urls = [];
|
|
281
|
+
this.font_bytes.clear();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Create a CoverGenerator instance with an optional initialised WASM renderer.
|
|
286
|
+
* The compiler itself is created lazily by the first generate() call, once the schema's
|
|
287
|
+
* font set is known. Each instance is independent — multiple generators can run concurrently.
|
|
288
|
+
*/
|
|
289
|
+
export async function init(options) {
|
|
290
|
+
// Load the curated font manifest before anything else below resolves a font family
|
|
291
|
+
await load_fonts_prefix(options.fonts_prefix ?? '/fonts/');
|
|
292
|
+
// Renderer only needs base fonts (glyph shapes are embedded in compiled vector data)
|
|
293
|
+
let renderer = null;
|
|
294
|
+
if (options.renderer_wasm_url) {
|
|
295
|
+
const renderer_font_opts = loadFonts([]);
|
|
296
|
+
const r = createTypstRenderer();
|
|
297
|
+
await r.init({
|
|
298
|
+
getModule: () => ({ module_or_path: options.renderer_wasm_url }),
|
|
299
|
+
beforeBuild: [renderer_font_opts],
|
|
300
|
+
});
|
|
301
|
+
renderer = r;
|
|
302
|
+
}
|
|
303
|
+
const gen = new CoverGenerator(options, renderer);
|
|
304
|
+
// Warm the font byte cache with the always-needed base font while the rest of the app
|
|
305
|
+
// finishes loading — a failure here is fine, the first generate simply refetches
|
|
306
|
+
gen.prefetch_fonts([base_font()]).catch(() => { });
|
|
307
|
+
return gen;
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,8FAA8F;AAE9F,OAAO,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,kCAAkC,CAAA;AACvF,OAAO,EAAC,mBAAmB,EAAC,MAAM,kCAAkC,CAAA;AAGpE,OAAO,EAAC,SAAS,EAAC,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAC,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EACvF,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAC,MAAM,gBAAgB,CAAA;AAEnF,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAA;AACrC,OAAO,EAAC,iBAAiB,EAAE,aAAa,IAAI,eAAe,EAAE,gBAAgB,EACzE,kBAAkB,EAAE,gBAAgB,EAAC,MAAM,iBAAiB,CAAA;AAKhE,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAC,SAAS,EAAE,gBAAgB,EAAC,MAAM,aAAa,CAAA;AACvD,OAAO,EAAC,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAA;AAEnG,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAuCjC,2DAA2D;AAC3D,SAAS,mBAAmB,CAAC,WAAmB;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;QACnC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACpF,CAAC,CAAC,eAAe,CAAA;IACrB,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,EAAE,CAAC,CAAA;AACzE,CAAC;AAED,6FAA6F;AAC7F,SAAS,WAAW,CAAC,KAAY,EAAE,MAAa;IAC5C,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC7C,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;IAC/C,MAAM,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,OAAO,MAAM,CAAA;AACjB,CAAC;AAED,yEAAyE;AACzE,KAAK,UAAU,gBAAgB,CAAC,MAA0C;IACtE,MAAM,IAAI,GAAG,eAAe,IAAI,MAAM;QAClC,CAAC,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,EAAC,IAAI,EAAE,WAAW,EAAC,CAAC;QACjD,CAAC,CAAC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAE,CAAC,EAAE,WAAW,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;IACN,OAAO,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;AACnD,CAAC;AAED,wEAAwE;AACxE,KAAK,UAAU,WAAW,CACtB,IAAe,EACf,CAAQ,EACR,CAAQ,EACR,CAAQ,EACR,CAAQ;IAER,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAgB,CAAC,EAAE,EAAC,IAAI,EAAE,WAAW,EAAC,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAA;IAE5C,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAChC,MAAM,GAAG,GAAI,MAA4B,CAAC,UAAU,CAAC,IAAI,CAAE,CAAA;IAC3D,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7C,MAAM,CAAC,KAAK,EAAE,CAAA;IAEd,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAA;AACnC,CAAC;AAED,kFAAkF;AAClF,MAAM,OAAO,cAAc;IACf,IAAI,CAAY;IACxB,wFAAwF;IACxF,mFAAmF;IAC3E,QAAQ,CAAe;IACvB,QAAQ,CAAqB;IAErC,wFAAwF;IAChF,YAAY,GAAG,EAAE,CAAA;IACzB,oFAAoF;IACpF,+CAA+C;IACvC,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAA;IAClD,kFAAkF;IAC1E,cAAc,GAAY,EAAE,CAAA;IACpC,sEAAsE;IAC9D,eAAe,GAAG,IAAI,OAAO,EAAsB,CAAA;IACnD,mBAAmB,GAAG,CAAC,CAAA;IAE/B,YAAY,IAAgB,EAAE,QAA6B;QACvD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC5B,CAAC;IAED;;wEAEoE;IAC5D,aAAa,CAAC,QAAiB;QACnC,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,SAAS,EAAE,QAAQ,CAAC,CAAA;IACzE,CAAC;IAED;4CACwC;IAChC,KAAK,CAAC,UAAU,CAAC,GAAU;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACvC,IAAI,MAAM,EAAE,CAAC;YACT,OAAO,MAAM,CAAA;QACjB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAA;QACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC/B,OAAO,KAAK,CAAA;IAChB,CAAC;IAED,sEAAsE;IACtE,KAAK,CAAC,cAAc,CAAC,QAAiB;QAClC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACpF,CAAC;IAED;;oFAEgF;IACxE,cAAc,CAAC,IAAe;QAClC,IAAI,EAAE,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACvC,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACnB,EAAE,GAAG,IAAI,CAAC,mBAAmB,CAAA;YAC7B,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAA;YAC7B,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACtC,CAAC;QACD,OAAO,EAAE,CAAA;IACb,CAAC;IAED;;;;gBAIY;IACJ,KAAK,CAAC,eAAe,CAAC,SAAkB,EAAE,YAA0B;QACxE,8CAA8C;QAC9C,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAEnF,iEAAiE;QACjE,gBAAgB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAErC,0EAA0E;QAC1E,mEAAmE;QACnE,IAAI,CAAC,cAAc,GAAG,kBAAkB,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAErF,gFAAgF;QAChF,yFAAyF;QACzF,MAAM,kBAAkB,GAAG,SAAS,CAAC,IAAI,CAAC,cAAc,EAAE,EAAC,MAAM,EAAE,KAAK,EAAC,CAAC,CAAA;QAE1E,MAAM,CAAC,GAAG,mBAAmB,EAAE,CAAA;QAC/B,MAAM,CAAC,CAAC,IAAI,CAAC;YACT,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAC,CAAC;YACvD,WAAW,EAAE,CAAC,kBAAkB,CAAC;SACpC,CAAC,CAAA;QAEF,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;IACrB,CAAC;IAED,oEAAoE;IAC5D,UAAU,CAAC,KAA6B;QAC5C,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;QAC3B,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAA;YAC5B,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACzD,CAAC;iBACI,CAAC;gBACF,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACzC,CAAC;QACL,CAAC;IACL,CAAC;IAED,0CAA0C;IAClC,KAAK,CAAC,WAAW;QACrB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YACvC,YAAY,EAAE,YAAY;YAC1B,MAAM,EAAE,iBAAiB,CAAC,GAAG;SAChC,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACjB,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAC3C,CAAC;QACD,OAAO,MAAM,CAAC,MAAO,CAAA;IACzB,CAAC;IAED,mFAAmF;IAC3E,KAAK,CAAC,WAAW;QACrB,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;QAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YACvC,YAAY,EAAE,YAAY;YAC1B,MAAM,EAAE,iBAAiB,CAAC,MAAM;SACnC,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACjB,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAC3C,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAS,CAAC,SAAS,CAAC;YACvC,eAAe,EAAE,MAAM,CAAC,MAAO;YAC/B,MAAM,EAAE,QAAQ;YAChB,cAAc,EAAE,EAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAC;SACjE,CAAC,CAAA;QAEF,+EAA+E;QAC/E,kFAAkF;QAClF,OAAO,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,KAAY,EAAE,EAAE;YACxD,MAAM,OAAO,GAAG,KAAK;iBAChB,OAAO,CAAC,kBAAkB,EAAE,cAAc,CAAC;iBAC3C,OAAO,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAA;YAClD,OAAO,OAAO,OAAO,GAAG,CAAA;QAC5B,CAAC,CAAC,CAAA;IACN,CAAC;IAED,wEAAwE;IAChE,KAAK,CAAC,WAAW,CAAC,GAAU;QAChC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;QAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YACvC,YAAY,EAAE,YAAY;YAC1B,MAAM,EAAE,iBAAiB,CAAC,MAAM;SACnC,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACjB,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAC3C,CAAC;QAED,+DAA+D;QAC/D,MAAM,KAAK,GAAG,GAAG,GAAG,EAAE,CAAA;QACtB,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;QAE/C,MAAM,IAAI,CAAC,QAAS,CAAC,YAAY,CAAC;YAC9B,eAAe,EAAE,MAAM,CAAC,MAAO;YAC/B,MAAM,EAAE,QAAQ;YAChB,MAAM;YACN,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,KAAK;YACjB,eAAe,EAAE,SAAS;SAC7B,CAAC,CAAA;QAEF,0BAA0B;QAC1B,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAE,CAAC,EAAE,WAAW,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;QACF,OAAO,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACnD,CAAC;IAED,gDAAgD;IACxC,eAAe,CAAC,MAAa;QACjC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACX,mBAAmB,MAAM,sDAAsD,CAClF,CAAA;QACL,CAAC;IACL,CAAC;IAED,wEAAwE;IACxE,KAAK,CAAC,QAAQ,CAAC,OAAuB;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,KAAK,CAAA;QACtC,8DAA8D;QAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,GAAG,CAAA;QAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;QAEpC,yDAAyD;QACzD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,UAAU,CAAA;QAEpD,oFAAoF;QACpF,oFAAoF;QACpF,mFAAmF;QACnF,uFAAuF;QACvF,sFAAsF;QACtF,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAA;QAC9C,MAAM,eAAe,GAAG,OAAO,CAAC,YAAY;YACxC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YACzE,CAAC,CAAC,EAAE,CAAA;QACR,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,eAAe,CAAA;QAChE,IAAI,SAAS,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC;YAClC,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;YAClF,IAAI,CAAC,YAAY,GAAG,SAAS,CAAA;QACjC,CAAC;QAED,2EAA2E;QAC3E,IAAI,WAAqD,CAAA;QACzD,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAA;YAC1B,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;YAC3E,WAAW,GAAG,EAAC,IAAI,EAAE,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,GAAG,EAAC,CAAA;QACvE,CAAC;QAED,mCAAmC;QACnC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC/C,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9E,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACnF,CAAC,CAAA;QACF,MAAM,SAAS,GAAa,EAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAC,CAAA;QAEpE,uEAAuE;QACvE,IAAI,UAA2B,CAAA;QAC/B,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACzC,MAAM,GAAG,GAAG,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;YAC/C,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAA;YAC7B,UAAU,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;QAClC,CAAC;QAED,MAAM,EAAC,KAAK,EAAE,IAAI,EAAC,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,CAAC,CAAA;QAC1F,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;QAEtB,kCAAkC;QAClC,IAAI,IAAwB,CAAA;QAE5B,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACnB,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;QACnC,CAAC;aACI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACxB,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;QACnC,CAAC;aACI,CAAC;YACF,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QACtC,CAAC;QAED,MAAM,MAAM,GAAkB,EAAC,IAAI,EAAC,CAAA;QAEpC,qCAAqC;QACrC,IAAI,KAAK,EAAE,CAAC;YACR,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACnB,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC,IAAkB,EAAE,IAAI,CAAC,CAAA;YACtD,CAAC;iBACI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACxB,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC,IAAc,EAAE,IAAI,CAAC,CAAA;YAClD,CAAC;iBACI,CAAC;gBACF,MAAM,CAAC,KAAK,GAAG,MAAM,SAAS,CAAC,IAAkB,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;YAC9E,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAA;IACjB,CAAC;IAED,iEAAiE;IACjE,OAAO;QACH,gBAAgB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QACrC,IAAI,CAAC,cAAc,GAAG,EAAE,CAAA;QACxB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAC3B,CAAC;CACJ;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAmB;IAC1C,mFAAmF;IACnF,MAAM,iBAAiB,CAAC,OAAO,CAAC,YAAY,IAAI,SAAS,CAAC,CAAA;IAE1D,qFAAqF;IACrF,IAAI,QAAQ,GAAwB,IAAI,CAAA;IACxC,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;QAC5B,MAAM,kBAAkB,GAAG,SAAS,CAAC,EAAE,CAAC,CAAA;QACxC,MAAM,CAAC,GAAG,mBAAmB,EAAE,CAAA;QAC/B,MAAM,CAAC,CAAC,IAAI,CAAC;YACT,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAC,cAAc,EAAE,OAAO,CAAC,iBAAkB,EAAC,CAAC;YAC/D,WAAW,EAAE,CAAC,kBAAkB,CAAC;SACpC,CAAC,CAAA;QACF,QAAQ,GAAG,CAAC,CAAA;IAChB,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAEjD,sFAAsF;IACtF,iFAAiF;IACjF,GAAG,CAAC,cAAc,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAEjD,OAAO,GAAG,CAAA;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bookcover-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT-0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/gracious-tech/bookcover.git",
|
|
8
|
+
"directory": "generator-web"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/gracious-tech/bookcover/tree/main/generator-web#readme",
|
|
11
|
+
"bugs": "https://github.com/gracious-tech/bookcover/issues",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@myriaddreamin/typst-ts-renderer": "^0.7.0",
|
|
21
|
+
"@myriaddreamin/typst-ts-web-compiler": "^0.7.0",
|
|
22
|
+
"@myriaddreamin/typst.ts": "^0.7.0",
|
|
23
|
+
"bookcover-core": "file:../generator",
|
|
24
|
+
"typst-fonts": "^0.1.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
|
|
2
|
+
// Browser cover generator — compiles typst files via WebAssembly, supports PDF/SVG/PNG output
|
|
3
|
+
|
|
4
|
+
import {createTypstCompiler, CompileFormatEnum} from '@myriaddreamin/typst.ts/compiler'
|
|
5
|
+
import {createTypstRenderer} from '@myriaddreamin/typst.ts/renderer'
|
|
6
|
+
import type {TypstCompiler} from '@myriaddreamin/typst.ts/compiler'
|
|
7
|
+
import type {TypstRenderer} from '@myriaddreamin/typst.ts/renderer'
|
|
8
|
+
import {loadFonts} from '@myriaddreamin/typst.ts'
|
|
9
|
+
import {build, cover_schema, split_svg, split_png, split_pdf, frame_image, frame_asset_path,
|
|
10
|
+
asset_path, TYPST_DIR, TEMPLATE_FILES, collect_all_fonts} from 'bookcover-core'
|
|
11
|
+
import type {OutputFormat, SplitResult, Templates} from 'bookcover-core'
|
|
12
|
+
import {base_font} from 'typst-fonts'
|
|
13
|
+
import {load_fonts_prefix, font_urls_for as build_font_urls, fetch_font_bytes,
|
|
14
|
+
fonts_to_blob_urls, revoke_blob_urls} from 'typst-fonts/web'
|
|
15
|
+
|
|
16
|
+
export type {CoverSchema, TitlePosition, FontConfig,
|
|
17
|
+
OutputFormat, SplitResult, PatternDef} from 'bookcover-core'
|
|
18
|
+
export type {BundledFont, CjkVariant} from 'typst-fonts'
|
|
19
|
+
export {font_file_url} from 'typst-fonts/web'
|
|
20
|
+
export {get_fonts, get_bundled_font} from 'typst-fonts'
|
|
21
|
+
export {list_patterns, collect_fonts, collect_all_fonts, default_spine_title} from 'bookcover-core'
|
|
22
|
+
|
|
23
|
+
const decoder = new TextDecoder()
|
|
24
|
+
|
|
25
|
+
export interface InitOptions {
|
|
26
|
+
// URL or path to typst_ts_web_compiler_bg.wasm — required
|
|
27
|
+
wasm_url:string
|
|
28
|
+
// URL or path to typst_ts_renderer_bg.wasm — required for SVG/PNG output
|
|
29
|
+
renderer_wasm_url?:string
|
|
30
|
+
// URL prefix for generator assets (e.g. '/generator_assets/').
|
|
31
|
+
// Used to load typst templates, frames, backgrounds, etc.
|
|
32
|
+
assets_prefix?:string
|
|
33
|
+
// URL prefix for fonts — curated and Noto fallback alike (e.g.
|
|
34
|
+
// 'http://localhost:5300/generator_assets/fonts' in dev, 'https://assets.paper.bible/fonts' in
|
|
35
|
+
// production). Kept separate from assets_prefix since the fonts tree is managed and
|
|
36
|
+
// published by its own separate repo.
|
|
37
|
+
fonts_prefix?:string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GenerateOptions {
|
|
41
|
+
schema:unknown
|
|
42
|
+
// Background image as a Blob (content type included)
|
|
43
|
+
image?:Blob
|
|
44
|
+
// Output format: 'pdf' (default), 'svg', or 'png'
|
|
45
|
+
format?:OutputFormat
|
|
46
|
+
// PPI for PNG output (default 144)
|
|
47
|
+
ppi?:number
|
|
48
|
+
// Whether to split the result into front/back/spine panels
|
|
49
|
+
split?:boolean
|
|
50
|
+
// Raw TTF bytes for user-uploaded custom fonts
|
|
51
|
+
custom_fonts?:Uint8Array[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GenerateResult {
|
|
55
|
+
// The full cover data (PDF/PNG bytes, or SVG string)
|
|
56
|
+
data:Uint8Array | string
|
|
57
|
+
// Split panel data (only present when split is true and format is svg/png)
|
|
58
|
+
split?:SplitResult<Uint8Array> | SplitResult<string>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/** Format compilation diagnostics into a readable error */
|
|
63
|
+
function throw_compile_error(diagnostics:unknown):never {
|
|
64
|
+
const diag = Array.isArray(diagnostics)
|
|
65
|
+
? diagnostics.map((d) => (typeof d === 'string' ? d : JSON.stringify(d))).join('\n')
|
|
66
|
+
: 'unknown error'
|
|
67
|
+
throw new Error(`[generator-web] Typst compilation failed:\n${diag}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Create a 2D canvas — a DOM element on the main thread, OffscreenCanvas inside a worker */
|
|
71
|
+
function make_canvas(width:number, height:number):HTMLCanvasElement | OffscreenCanvas {
|
|
72
|
+
if (typeof document === 'undefined') {
|
|
73
|
+
return new OffscreenCanvas(width, height)
|
|
74
|
+
}
|
|
75
|
+
const canvas = document.createElement('canvas')
|
|
76
|
+
canvas.width = width
|
|
77
|
+
canvas.height = height
|
|
78
|
+
return canvas
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Encode a canvas' contents as PNG bytes (handles both canvas types) */
|
|
82
|
+
async function canvas_png_bytes(canvas:HTMLCanvasElement | OffscreenCanvas):Promise<Uint8Array> {
|
|
83
|
+
const blob = 'convertToBlob' in canvas
|
|
84
|
+
? await canvas.convertToBlob({type: 'image/png'})
|
|
85
|
+
: await new Promise<Blob>((resolve) => {
|
|
86
|
+
canvas.toBlob((b) => resolve(b!), 'image/png')
|
|
87
|
+
})
|
|
88
|
+
return new Uint8Array(await blob.arrayBuffer())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Crop a region from a PNG using the Canvas API — used as PngCropFn */
|
|
92
|
+
async function canvas_crop(
|
|
93
|
+
data:Uint8Array,
|
|
94
|
+
x:number,
|
|
95
|
+
y:number,
|
|
96
|
+
w:number,
|
|
97
|
+
h:number,
|
|
98
|
+
):Promise<Uint8Array> {
|
|
99
|
+
const blob = new Blob([data as BlobPart], {type: 'image/png'})
|
|
100
|
+
const bitmap = await createImageBitmap(blob)
|
|
101
|
+
|
|
102
|
+
const canvas = make_canvas(w, h)
|
|
103
|
+
const ctx = (canvas as HTMLCanvasElement).getContext('2d')!
|
|
104
|
+
ctx.drawImage(bitmap, x, y, w, h, 0, 0, w, h)
|
|
105
|
+
bitmap.close()
|
|
106
|
+
|
|
107
|
+
return canvas_png_bytes(canvas)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Stateful cover generator — each instance owns its own compiler and renderer */
|
|
111
|
+
export class CoverGenerator {
|
|
112
|
+
private opts:InitOptions
|
|
113
|
+
// Created lazily on first generate() — the font set isn't known until a schema arrives,
|
|
114
|
+
// so creating one earlier would just instantiate WASM with fonts that get replaced
|
|
115
|
+
private compiler!:TypstCompiler
|
|
116
|
+
private renderer:TypstRenderer | null
|
|
117
|
+
|
|
118
|
+
// Cache key of the font set the current compiler was init'd with ('' = no compiler yet)
|
|
119
|
+
private active_fonts = ''
|
|
120
|
+
// Fetched font bytes keyed by URL, so a compiler reinit (any font-set change) never
|
|
121
|
+
// refetches families already seen this session
|
|
122
|
+
private font_bytes = new Map<string, Uint8Array>()
|
|
123
|
+
// Blob URLs handed to the current compiler (bundled + custom) — revoked on reinit
|
|
124
|
+
private font_blob_urls:string[] = []
|
|
125
|
+
// Identity-based ids for custom font byte arrays (see custom_font_id)
|
|
126
|
+
private custom_font_ids = new WeakMap<Uint8Array, number>()
|
|
127
|
+
private next_custom_font_id = 1
|
|
128
|
+
|
|
129
|
+
constructor(opts:InitOptions, renderer:TypstRenderer | null) {
|
|
130
|
+
this.opts = opts
|
|
131
|
+
this.renderer = renderer
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Build fetch URLs for the given font families against fonts_prefix — curated families
|
|
135
|
+
* live at <prefix>/<family>/, Noto fallback families at <prefix>/_noto/<family>/,
|
|
136
|
+
* mirroring the top-level fonts/ directory's own layout exactly */
|
|
137
|
+
private font_urls_for(families:string[]):string[] {
|
|
138
|
+
return build_font_urls(this.opts.fonts_prefix ?? '/fonts/', families)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Fetch a font file's bytes, memoised in font_bytes (failures aren't cached, so a
|
|
142
|
+
* later generate retries the fetch) */
|
|
143
|
+
private async fetch_font(url:string):Promise<Uint8Array> {
|
|
144
|
+
const cached = this.font_bytes.get(url)
|
|
145
|
+
if (cached) {
|
|
146
|
+
return cached
|
|
147
|
+
}
|
|
148
|
+
const bytes = await fetch_font_bytes(url)
|
|
149
|
+
this.font_bytes.set(url, bytes)
|
|
150
|
+
return bytes
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Pre-fetch the given font families into the in-memory byte cache */
|
|
154
|
+
async prefetch_fonts(families:string[]):Promise<void> {
|
|
155
|
+
await Promise.all(this.font_urls_for(families).map(url => this.fetch_font(url)))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Stable id for a custom font's byte array. The widget passes the same Uint8Array
|
|
159
|
+
* references across generates, so object identity distinguishes fonts without hashing
|
|
160
|
+
* their bytes (byteLength alone could collide between two different fonts). */
|
|
161
|
+
private custom_font_id(data:Uint8Array):number {
|
|
162
|
+
let id = this.custom_font_ids.get(data)
|
|
163
|
+
if (id === undefined) {
|
|
164
|
+
id = this.next_custom_font_id
|
|
165
|
+
this.next_custom_font_id += 1
|
|
166
|
+
this.custom_font_ids.set(data, id)
|
|
167
|
+
}
|
|
168
|
+
return id
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** (Re)initialise the compiler with a different font set. Bundled font bytes come from
|
|
172
|
+
* the in-memory cache (fetched at most once per session) and everything is handed to
|
|
173
|
+
* loadFonts() as blob URLs, keeping the network out of the reinit path. The renderer
|
|
174
|
+
* never needs reinitialisation because it reads glyph outlines from compiled vector
|
|
175
|
+
* data. */
|
|
176
|
+
private async reinit_compiler(font_urls:string[], custom_fonts?:Uint8Array[]):Promise<void> {
|
|
177
|
+
// Fetch any fonts not yet cached, in parallel
|
|
178
|
+
const bundled_bytes = await Promise.all(font_urls.map(url => this.fetch_font(url)))
|
|
179
|
+
|
|
180
|
+
// Revoke the previous compiler's blob URLs to avoid memory leaks
|
|
181
|
+
revoke_blob_urls(this.font_blob_urls)
|
|
182
|
+
|
|
183
|
+
// One blob URL per font file (bundled + custom) for loadFonts() to read —
|
|
184
|
+
// mapShadow only adds to the virtual filesystem, not the font book
|
|
185
|
+
this.font_blob_urls = fonts_to_blob_urls([...bundled_bytes, ...(custom_fonts ?? [])])
|
|
186
|
+
|
|
187
|
+
// assets:false stops typst.ts silently appending its default text fonts (NewCM,
|
|
188
|
+
// Libertinus, DejaVu... fetched from jsdelivr) — all fonts we need are passed explicitly
|
|
189
|
+
const compiler_font_opts = loadFonts(this.font_blob_urls, {assets: false})
|
|
190
|
+
|
|
191
|
+
const c = createTypstCompiler()
|
|
192
|
+
await c.init({
|
|
193
|
+
getModule: () => ({module_or_path: this.opts.wasm_url}),
|
|
194
|
+
beforeBuild: [compiler_font_opts],
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
this.compiler = c
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Load the typst file map into the compiler's shadow filesystem */
|
|
201
|
+
private load_files(files:Map<string, Uint8Array>):void {
|
|
202
|
+
this.compiler.resetShadow()
|
|
203
|
+
for (const [filename, bytes] of files) {
|
|
204
|
+
const vpath = `/${filename}`
|
|
205
|
+
if (filename.endsWith('.typ')) {
|
|
206
|
+
this.compiler.addSource(vpath, decoder.decode(bytes))
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.compiler.mapShadow(vpath, bytes)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Compile to PDF and return raw bytes */
|
|
215
|
+
private async compile_pdf():Promise<Uint8Array> {
|
|
216
|
+
const result = await this.compiler.compile({
|
|
217
|
+
mainFilePath: '/cover.typ',
|
|
218
|
+
format: CompileFormatEnum.pdf,
|
|
219
|
+
})
|
|
220
|
+
if (!result.result) {
|
|
221
|
+
throw_compile_error(result.diagnostics)
|
|
222
|
+
}
|
|
223
|
+
return result.result!
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Compile to vector format, then render as SVG string covering the full canvas */
|
|
227
|
+
private async compile_svg():Promise<string> {
|
|
228
|
+
this.assert_renderer('SVG')
|
|
229
|
+
const result = await this.compiler.compile({
|
|
230
|
+
mainFilePath: '/cover.typ',
|
|
231
|
+
format: CompileFormatEnum.vector,
|
|
232
|
+
})
|
|
233
|
+
if (!result.result) {
|
|
234
|
+
throw_compile_error(result.diagnostics)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const svg = await this.renderer!.renderSvg({
|
|
238
|
+
artifactContent: result.result!,
|
|
239
|
+
format: 'vector',
|
|
240
|
+
data_selection: {js: false, css: true, body: true, defs: true},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Patch unitless width/height to add `pt` suffix (Typst SVG output uses pt) so
|
|
244
|
+
// browsers render at 96 CSS px per inch, matching the split SVGs from split_svg()
|
|
245
|
+
return svg.replace(/<svg([^>]*)>/, (_match, attrs:string) => {
|
|
246
|
+
const patched = attrs
|
|
247
|
+
.replace(/width="([\d.]+)"/, 'width="$1pt"')
|
|
248
|
+
.replace(/height="([\d.]+)"/, 'height="$1pt"')
|
|
249
|
+
return `<svg${patched}>`
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Compile to vector format, then render as PNG via offscreen canvas */
|
|
254
|
+
private async compile_png(ppi:number):Promise<Uint8Array> {
|
|
255
|
+
this.assert_renderer('PNG')
|
|
256
|
+
const result = await this.compiler.compile({
|
|
257
|
+
mainFilePath: '/cover.typ',
|
|
258
|
+
format: CompileFormatEnum.vector,
|
|
259
|
+
})
|
|
260
|
+
if (!result.result) {
|
|
261
|
+
throw_compile_error(result.diagnostics)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Render to a canvas at the desired PPI (typst uses 72pt/inch)
|
|
265
|
+
const scale = ppi / 72
|
|
266
|
+
const canvas = document.createElement('canvas')
|
|
267
|
+
|
|
268
|
+
await this.renderer!.renderCanvas({
|
|
269
|
+
artifactContent: result.result!,
|
|
270
|
+
format: 'vector',
|
|
271
|
+
canvas,
|
|
272
|
+
pageOffset: 0,
|
|
273
|
+
pixelPerPt: scale,
|
|
274
|
+
backgroundColor: '#ffffff',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// Extract PNG from canvas
|
|
278
|
+
const blob = await new Promise<Blob>((resolve) => {
|
|
279
|
+
canvas.toBlob((b) => resolve(b!), 'image/png')
|
|
280
|
+
})
|
|
281
|
+
return new Uint8Array(await blob.arrayBuffer())
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Throw if the renderer was not initialised */
|
|
285
|
+
private assert_renderer(format:string):void {
|
|
286
|
+
if (!this.renderer) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`[generator-web] ${format} output requires renderer_wasm_url in init() options`
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Generate a book cover using this instance's compiler and renderer */
|
|
294
|
+
async generate(options:GenerateOptions):Promise<GenerateResult> {
|
|
295
|
+
const format = options.format ?? 'pdf'
|
|
296
|
+
// Default to 144 PPI (2x typographic resolution, 2 × 72pt/in)
|
|
297
|
+
const ppi = options.ppi ?? 144
|
|
298
|
+
const split = options.split ?? false
|
|
299
|
+
|
|
300
|
+
// Parse schema and get dimensions from printing-services
|
|
301
|
+
const parsed = cover_schema.parse(options.schema)
|
|
302
|
+
const assets = this.opts.assets_prefix ?? '/assets/'
|
|
303
|
+
|
|
304
|
+
// (Re)initialise the compiler when the set of required fonts changes — including on
|
|
305
|
+
// the very first generate, since the compiler is created lazily ('' never matches).
|
|
306
|
+
// Noto Serif (base font) is always included — it's not bundled in the WASM assets.
|
|
307
|
+
// collect_all_fonts also includes every Noto fallback family needed to cover non-Latin
|
|
308
|
+
// scripts detected in the schema's text, so only those specific families get fetched.
|
|
309
|
+
const needed_fonts = collect_all_fonts(parsed)
|
|
310
|
+
const custom_fonts_id = options.custom_fonts
|
|
311
|
+
? options.custom_fonts.map(b => String(this.custom_font_id(b))).join(':')
|
|
312
|
+
: ''
|
|
313
|
+
const cache_key = needed_fonts.join(',') + '|' + custom_fonts_id
|
|
314
|
+
if (cache_key !== this.active_fonts) {
|
|
315
|
+
await this.reinit_compiler(this.font_urls_for(needed_fonts), options.custom_fonts)
|
|
316
|
+
this.active_fonts = cache_key
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Convert Blob to ImageInput (Uint8Array + extension) for the core builder
|
|
320
|
+
let image_input:{data:Uint8Array, ext:string} | undefined
|
|
321
|
+
if (options.image) {
|
|
322
|
+
const blob = options.image
|
|
323
|
+
const ext = '.' + (blob.type.split('/')[1] ?? 'jpg').replace('jpeg', 'jpg')
|
|
324
|
+
image_input = {data: new Uint8Array(await blob.arrayBuffer()), ext}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Load typst templates from assets
|
|
328
|
+
const [cover_typ, helpers_typ] = await Promise.all([
|
|
329
|
+
fetch(asset_path(assets, TYPST_DIR, TEMPLATE_FILES.cover)).then(r => r.text()),
|
|
330
|
+
fetch(asset_path(assets, TYPST_DIR, TEMPLATE_FILES.helpers)).then(r => r.text()),
|
|
331
|
+
])
|
|
332
|
+
const templates:Templates = {cover: cover_typ, helpers: helpers_typ}
|
|
333
|
+
|
|
334
|
+
// Load the frame PNG from assets when the schema uses painted coverage
|
|
335
|
+
let frame_blob:Blob | undefined
|
|
336
|
+
if (parsed.bg_image_coverage === 'painted') {
|
|
337
|
+
const url = frame_asset_path(assets, 'painted')
|
|
338
|
+
const resp = await fetch(url)
|
|
339
|
+
frame_blob = await resp.blob()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const {files, dims} = await build(templates, parsed, image_input, frame_image, frame_blob)
|
|
343
|
+
this.load_files(files)
|
|
344
|
+
|
|
345
|
+
// Compile to the requested format
|
|
346
|
+
let data:Uint8Array | string
|
|
347
|
+
|
|
348
|
+
if (format === 'pdf') {
|
|
349
|
+
data = await this.compile_pdf()
|
|
350
|
+
}
|
|
351
|
+
else if (format === 'svg') {
|
|
352
|
+
data = await this.compile_svg()
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
data = await this.compile_png(ppi)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const result:GenerateResult = {data}
|
|
359
|
+
|
|
360
|
+
// Split into front/back/spine panels
|
|
361
|
+
if (split) {
|
|
362
|
+
if (format === 'pdf') {
|
|
363
|
+
result.split = split_pdf(data as Uint8Array, dims)
|
|
364
|
+
}
|
|
365
|
+
else if (format === 'svg') {
|
|
366
|
+
result.split = split_svg(data as string, dims)
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
result.split = await split_png(data as Uint8Array, dims, ppi, canvas_crop)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return result
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Revoke blob URLs and drop cached font bytes to free memory */
|
|
377
|
+
destroy():void {
|
|
378
|
+
revoke_blob_urls(this.font_blob_urls)
|
|
379
|
+
this.font_blob_urls = []
|
|
380
|
+
this.font_bytes.clear()
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Create a CoverGenerator instance with an optional initialised WASM renderer.
|
|
386
|
+
* The compiler itself is created lazily by the first generate() call, once the schema's
|
|
387
|
+
* font set is known. Each instance is independent — multiple generators can run concurrently.
|
|
388
|
+
*/
|
|
389
|
+
export async function init(options:InitOptions):Promise<CoverGenerator> {
|
|
390
|
+
// Load the curated font manifest before anything else below resolves a font family
|
|
391
|
+
await load_fonts_prefix(options.fonts_prefix ?? '/fonts/')
|
|
392
|
+
|
|
393
|
+
// Renderer only needs base fonts (glyph shapes are embedded in compiled vector data)
|
|
394
|
+
let renderer:TypstRenderer | null = null
|
|
395
|
+
if (options.renderer_wasm_url) {
|
|
396
|
+
const renderer_font_opts = loadFonts([])
|
|
397
|
+
const r = createTypstRenderer()
|
|
398
|
+
await r.init({
|
|
399
|
+
getModule: () => ({module_or_path: options.renderer_wasm_url!}),
|
|
400
|
+
beforeBuild: [renderer_font_opts],
|
|
401
|
+
})
|
|
402
|
+
renderer = r
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const gen = new CoverGenerator(options, renderer)
|
|
406
|
+
|
|
407
|
+
// Warm the font byte cache with the always-needed base font while the rest of the app
|
|
408
|
+
// finishes loading — a failure here is fine, the first generate simply refetches
|
|
409
|
+
gen.prefetch_fonts([base_font()]).catch(() => {})
|
|
410
|
+
|
|
411
|
+
return gen
|
|
412
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"lib": ["ES2022", "DOM"],
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"skipLibCheck": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts"],"version":"5.9.3"}
|