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 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
+ [![Example cover](../sample.svg)](../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
+ ```
@@ -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":"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"}
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"}