bookcover-web 0.17.0 → 0.19.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 CHANGED
@@ -64,7 +64,7 @@ To self-host instead, serve the same layout under any prefix on your origin:
64
64
  for the renderer). Without a bundler, copy them from `node_modules` into your `public/` dir.
65
65
  - **`<assets_prefix>/frames/`** — `painted.png` and `torn.png`, copied from the repo's
66
66
  `assets/frames/`. (`backgrounds/` is only needed if your UI offers the suggested
67
- background images.)
67
+ background images — see below.)
68
68
  - **`<fonts_prefix>/`** — the fonts tree (`manifest.json`, one dir per family, Noto fallbacks
69
69
  under `_noto/`). Generate it with the repo's font config:
70
70
 
@@ -74,12 +74,34 @@ To self-host instead, serve the same layout under any prefix on your origin:
74
74
  ```
75
75
 
76
76
 
77
+ ### Built-in backgrounds
78
+
79
+ A built-in background's filename (e.g. `beach.jpg`) is its ID — the embed protocol sends a built-in
80
+ as `bg_image_builtin` with no bytes. Join it onto any of these to fetch a copy:
81
+
82
+ - `https://assets.paper.bible/backgrounds/<name>` — the original (2.8MB on average), for final
83
+ output
84
+ - `https://assets.paper.bible/backgrounds/previews_2700/<name>` (`BG_PREVIEW_DIR`) — ~380KB,
85
+ enough for on-screen previews of a 6x9" cover
86
+ - `https://assets.paper.bible/backgrounds/previews_800/<name>` — large thumbnails
87
+ - `https://assets.paper.bible/backgrounds/thumbnails/<name>` — 160x120 picker tiles
88
+
89
+ Pass the filename to `generate()` as `image_builtin` whichever copy you render, so the automatic
90
+ text colors come from its baked color data and a preview matches the final output exactly.
91
+ `get_builtin_bg_regions(name)` returns the same data, and `get_builtin_bg(name)` adds the
92
+ original's pixel size (for resolution checks without downloading it).
93
+
94
+
77
95
  ## `CoverGenerator.generate(options): Promise<GenerateResult>`
78
96
 
79
97
  ```ts
80
98
  interface GenerateOptions {
81
99
  schema: unknown // raw schema (see above)
82
100
  image?: Blob // background image
101
+ // Filename of the built-in background `image` is a copy of — colors come from its baked data
102
+ image_builtin?: string
103
+ // Shrink `image` to at most this many pixels per inch of cover before compiling (previews)
104
+ image_max_dpi?: number
83
105
  format?: 'pdf'|'svg'|'png' // default: 'pdf'
84
106
  ppi?: number // PNG resolution, default: 144
85
107
  split?: boolean // include split panels in result
@@ -85,7 +85,8 @@ export type HostMessage = InitMessage | {
85
85
  id: string;
86
86
  bg_image: File | null;
87
87
  };
88
- /** Widget -> host. `bg_image` rides on every data/finished message (cloning a File is cheap);
88
+ /** Widget -> host. `bg_image` and `bg_image_builtin` ride on every data/finished message
89
+ * (cloning a File is cheap), at most one of them non-null;
89
90
  * `custom_fonts` byte arrays are expensive to clone, so on 'data' messages the field is only
90
91
  * present when the font set changed since the last message (absent = unchanged), while
91
92
  * 'finished' always carries the complete array.
@@ -98,17 +99,18 @@ export type HostMessage = InitMessage | {
98
99
  * InitMessage.custom_fonts for the rule, and font_families_in_form in bookcover-core for the
99
100
  * applied set).
100
101
  *
101
- * `bg_image_builtin` is ADVISORY background identity, so a host doesn't have to recover it by
102
- * hashing returned bytes: when the user picks one of the widget's built-in backgrounds it
103
- * carries that image's published filename, and it is null for a user upload or no image at all.
104
- * The filename IS the id — hosts join it onto their own assets path to get the bytes and slice
105
- * its extension off for the MIME type, so it must stay a filename (see the contract on
106
- * BACKGROUNDS in the widget's services/backgrounds.ts). It exists purely so the host can store a
107
- * reference instead of a private copy of a shipped image. Treat it as untrusted input — map it
108
- * to a built-in only after checking it against your own allowlist, and fall back to the upload
109
- * path otherwise. Nothing in bookcover ever resolves bytes from this field.
102
+ * `bg_image_builtin` IS the background when the user picked one of the widget's built-ins: it
103
+ * carries that image's published filename and `bg_image` is null. It is null for an upload or
104
+ * no image at all. The filename IS the id — hosts join it onto their own assets path to get the
105
+ * bytes (`backgrounds/<name>` for the original, BG_PREVIEW_DIR for a preview-sized copy) and
106
+ * slice its extension off for the MIME type, so it must stay a filename (see the contract on
107
+ * BACKGROUNDS in the widget's services/backgrounds.ts). Pass it to generate() as image_builtin
108
+ * so colors come from the baked table whichever copy is rendered. Treat it as untrusted input
109
+ * — map it to a built-in only after checking it against your own allowlist. A name that fails
110
+ * that check comes with no bytes to fall back on, so keep the cover's existing background
111
+ * rather than treating the message as clearing it.
110
112
  *
111
- * The `bg_image` File is the one the host supplied, byte-for-byte — see the round-trip
113
+ * A non-null `bg_image` File is the one the host supplied, byte-for-byte — see the round-trip
112
114
  * guarantee at the top of this file.
113
115
  *
114
116
  * 'bg_suggestion_selected' asks the host for the bytes behind one of the BgSuggestions it sent
@@ -8,7 +8,13 @@
8
8
  // Messages carry no derived schema: hosts call build_schema(form) from bookcover-core, so there
9
9
  // is exactly one place that form → schema conversion happens.
10
10
  //
11
- // BACKGROUND IMAGE BYTES ROUND-TRIP UNMODIFIED. Whatever File the host hands in comes back out
11
+ // A BACKGROUND IMAGE IS AN ID OR BYTES, NEVER BOTH. A built-in background travels as its
12
+ // filename in `bg_image_builtin` with `bg_image: null` — no bytes, in either direction, so
13
+ // neither side has to download a multi-megabyte original just to open or report the editor. An
14
+ // uploaded image (a user upload, or a host-supplied one) travels as `bg_image` bytes with
15
+ // `bg_image_builtin: null`.
16
+ //
17
+ // UPLOADED IMAGE BYTES ROUND-TRIP UNMODIFIED. Whatever File the host hands in comes back out
12
18
  // byte-for-byte — never re-encoded, resized or stripped of metadata — so a host can hash the
13
19
  // bytes to recognise an image it already stores. See FormState.bg_image in bookcover-core.
14
20
  export {};
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@ export { font_file_url } from 'typst-fonts/web';
6
6
  export { get_fonts, get_bundled_font } from 'typst-fonts';
7
7
  export { list_patterns, collect_fonts, collect_all_fonts, default_spine_title, list_vector_backgrounds, find_vector_background, list_builtin_icons, find_builtin_icon, icon_categories, suggested_icons, generate_palette, cover_schema } from 'bookcover-core';
8
8
  export type { IconCategory } from 'bookcover-core';
9
- export { tinted_contrast_text, pick_vivid_tint, synthesize_fill, blend_regions, region_hex, all_image_regions, analyze_pixel_regions, get_builtin_bg_regions, VECTOR_BG_AUTO_COLOR, resolve_colors, darken_hsl } from 'bookcover-core';
10
- export type { RegionStats, ImageRegions, ResolvedColors } from 'bookcover-core';
11
- export { make_blank_form_values, build_schema, curly_quotes, parse_font_family, find_pattern, derive_colors, hex_override_to_hsl, hex_to_hsl, is_dark_color } from 'bookcover-core';
9
+ export { tinted_contrast_text, pick_vivid_tint, synthesize_fill, blend_regions, region_hex, all_image_regions, analyze_pixel_regions, get_builtin_bg, get_builtin_bg_regions, BG_PREVIEW_DIR, VECTOR_BG_AUTO_COLOR, resolve_colors, darken_hsl } from 'bookcover-core';
10
+ export type { RegionStats, ImageRegions, ResolvedColors, BuiltinBg } from 'bookcover-core';
11
+ export { make_blank_form_values, build_schema, curly_quotes, parse_font_family, find_pattern, derive_colors, hex_override_to_hsl, hex_to_hsl, is_dark_color, warn_unknown } from 'bookcover-core';
12
12
  export type { FormState, EmbedFormState, CustomFontStyle, DerivedColors } from 'bookcover-core';
13
13
  export { resolve_dimensions } from 'bookcover-core';
14
14
  export type { GetDimensionsResult, DimensionInputs } from 'bookcover-core';
@@ -22,6 +22,8 @@ export interface InitOptions {
22
22
  export interface GenerateOptions {
23
23
  schema: unknown;
24
24
  image?: Blob;
25
+ image_builtin?: string;
26
+ image_max_dpi?: number;
25
27
  format?: OutputFormat;
26
28
  ppi?: number;
27
29
  split?: boolean;
@@ -34,12 +36,11 @@ export interface GenerateResult {
34
36
  }
35
37
  /**
36
38
  * Sample a background image's dominant colors, under both interpretations at once — see
37
- * analyze_pixel_regions for what `dims` does. Skips decoding entirely when the image matches a
38
- * known builtin background by filename + byte size (see get_builtin_bg_regions) — that fast
39
- * path needs a File (or any Blob with a `name`); a plain nameless Blob always gets a live
40
- * decode via the Canvas API. Exported so hosts can sample ahead of generate() (e.g. the widget
41
- * caches this on a debounced watcher, independent of when generate() itself runs, then passes
42
- * the result back in via GenerateOptions.image_regions).
39
+ * analyze_pixel_regions for what `dims` does. Always decodes via the Canvas API — a builtin
40
+ * background's baked regions come from get_builtin_bg_regions() by its ID instead. Exported so
41
+ * hosts can sample ahead of generate() (e.g. the widget caches this on a debounced watcher,
42
+ * independent of when generate() itself runs, then passes the result back in via
43
+ * GenerateOptions.image_regions).
43
44
  */
44
45
  export declare function analyze_image_regions(image: Blob, dims: GetDimensionsResult | null): Promise<ImageRegions>;
45
46
  /** Stateful cover generator — each instance owns its own compiler and renderer */
@@ -99,6 +100,8 @@ export declare class CoverGenerator {
99
100
  private compile_png;
100
101
  /** Throw if the renderer was not initialised */
101
102
  private assert_renderer;
103
+ /** Run the compiler (and, for svg/png, the renderer) for the requested output format */
104
+ private compile_format;
102
105
  /** Generate a book cover using this instance's compiler and renderer */
103
106
  generate(options: GenerateOptions): Promise<GenerateResult>;
104
107
  /** Revoke blob URLs and drop cached font bytes to free memory */
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ import { load_fonts_prefix, font_urls_for as build_font_urls, fetch_font_bytes,
8
8
  export { font_file_url } from 'typst-fonts/web';
9
9
  export { get_fonts, get_bundled_font } from 'typst-fonts';
10
10
  export { list_patterns, collect_fonts, collect_all_fonts, default_spine_title, list_vector_backgrounds, find_vector_background, list_builtin_icons, find_builtin_icon, icon_categories, suggested_icons, generate_palette, cover_schema } from 'bookcover-core';
11
- export { tinted_contrast_text, pick_vivid_tint, synthesize_fill, blend_regions, region_hex, all_image_regions, analyze_pixel_regions, get_builtin_bg_regions, VECTOR_BG_AUTO_COLOR, resolve_colors, darken_hsl } from 'bookcover-core';
11
+ export { tinted_contrast_text, pick_vivid_tint, synthesize_fill, blend_regions, region_hex, all_image_regions, analyze_pixel_regions, get_builtin_bg, get_builtin_bg_regions, BG_PREVIEW_DIR, VECTOR_BG_AUTO_COLOR, resolve_colors, darken_hsl } from 'bookcover-core';
12
12
  // Form state + form->schema conversion, so hosts can derive the renderable schema themselves
13
- export { make_blank_form_values, build_schema, curly_quotes, parse_font_family, find_pattern, derive_colors, hex_override_to_hsl, hex_to_hsl, is_dark_color } from 'bookcover-core';
13
+ export { make_blank_form_values, build_schema, curly_quotes, parse_font_family, find_pattern, derive_colors, hex_override_to_hsl, hex_to_hsl, is_dark_color, warn_unknown } from 'bookcover-core';
14
14
  // Dimension resolution, so hosts can locate cover panels (e.g. for background-image analysis)
15
15
  export { resolve_dimensions } from 'bookcover-core';
16
16
  const decoder = new TextDecoder();
@@ -31,13 +31,17 @@ function make_canvas(width, height) {
31
31
  canvas.height = height;
32
32
  return canvas;
33
33
  }
34
+ /** Encode a canvas' contents as an image Blob of the given type (handles both canvas types) */
35
+ async function canvas_blob(canvas, type, quality) {
36
+ if ('convertToBlob' in canvas)
37
+ return canvas.convertToBlob({ type, quality });
38
+ return new Promise((resolve) => {
39
+ canvas.toBlob((b) => resolve(b), type, quality);
40
+ });
41
+ }
34
42
  /** Encode a canvas' contents as PNG bytes (handles both canvas types) */
35
43
  async function canvas_png_bytes(canvas) {
36
- const blob = 'convertToBlob' in canvas
37
- ? await canvas.convertToBlob({ type: 'image/png' })
38
- : await new Promise((resolve) => {
39
- canvas.toBlob((b) => resolve(b), 'image/png');
40
- });
44
+ const blob = await canvas_blob(canvas, 'image/png');
41
45
  return new Uint8Array(await blob.arrayBuffer());
42
46
  }
43
47
  /** Crop a region from a PNG using the Canvas API — used as PngCropFn */
@@ -53,23 +57,78 @@ async function canvas_crop(data, x, y, w, h) {
53
57
  // Downscale target (longest edge, px) for a live pixel decode — plenty of resolution for a
54
58
  // dominant-color read while keeping the pixel scan cheap
55
59
  const IMAGE_REGIONS_MAX_DIM = 400;
60
+ // Resizing is skipped when it would shrink the image by less than this fraction of its size —
61
+ // a near-target image (e.g. a built-in's preview copy, sized for a 6x9 cover) gains nothing from
62
+ // another decode and lossy re-encode
63
+ const MIN_DOWNSCALE = 0.85;
64
+ // The most recent downscaled copy (see downscale_image), so repeat previews of one image at one
65
+ // size skip the decode + re-encode. Just one entry: the target size shifts with every page count
66
+ // or trim edit, and older copies would otherwise pile up
67
+ let downscaled = null;
68
+ /** Identify an image across structured clones. A File posted to a worker arrives as a new object
69
+ * on every message, so identity alone would never match; its name, size, type and modified time
70
+ * together do. A plain Blob carries none of that beyond size/type, so it's matched by identity */
71
+ function image_key(image) {
72
+ if (!(image instanceof File))
73
+ return null;
74
+ return `${image.name}:${image.size}:${image.type}:${image.lastModified}`;
75
+ }
76
+ /** Shrink an image to the smallest size that still covers target_w x target_h — the templates
77
+ * place background images with fit "cover", so the LARGER of the two scale factors is the one
78
+ * that keeps every covered pixel. Never enlarges, and returns the image itself when it's already
79
+ * small enough or near enough (see MIN_DOWNSCALE). PNGs stay PNG so transparency survives;
80
+ * anything else is re-encoded as JPEG */
81
+ async function resize_to_cover(image, target_w, target_h) {
82
+ const bitmap = await createImageBitmap(image);
83
+ const scale = Math.max(target_w / bitmap.width, target_h / bitmap.height);
84
+ if (scale >= MIN_DOWNSCALE) {
85
+ bitmap.close();
86
+ return image;
87
+ }
88
+ const w = Math.max(1, Math.round(bitmap.width * scale));
89
+ const h = Math.max(1, Math.round(bitmap.height * scale));
90
+ const canvas = make_canvas(w, h);
91
+ const ctx = canvas.getContext('2d');
92
+ if (!ctx)
93
+ throw new Error('[generator-web] 2D canvas context unavailable');
94
+ ctx.drawImage(bitmap, 0, 0, w, h);
95
+ bitmap.close();
96
+ const type = image.type === 'image/png' ? 'image/png' : 'image/jpeg';
97
+ return canvas_blob(canvas, type, 0.85);
98
+ }
99
+ /** A copy of a background image with at most max_dpi pixels per inch across the full cover
100
+ * spread (see GenerateOptions.image_max_dpi), reusing the last one when nothing changed */
101
+ function downscale_image(image, dims, max_dpi) {
102
+ const target_w = Math.round(dims.cover_total_width.toNumber() / 25.4 * max_dpi);
103
+ const target_h = Math.round(dims.cover_total_height.toNumber() / 25.4 * max_dpi);
104
+ const size_key = `${target_w}x${target_h}`;
105
+ const key = image_key(image) ?? '';
106
+ const same_image = downscaled !== null
107
+ && (downscaled.source === image || (key !== '' && downscaled.image_key === key));
108
+ if (downscaled && same_image && downscaled.size_key === size_key)
109
+ return downscaled.blob;
110
+ // Cache the promise so concurrent calls share one resize, but drop it if the resize fails
111
+ const blob = resize_to_cover(image, target_w, target_h);
112
+ const entry = { image_key: key, source: image, size_key, blob };
113
+ downscaled = entry;
114
+ blob.catch(() => {
115
+ if (downscaled === entry)
116
+ downscaled = null;
117
+ });
118
+ return blob;
119
+ }
56
120
  // How many generate() calls to allow between forced memory resets, as a fallback for whatever
57
121
  // slow growth an image-identity change alone wouldn't catch — see reset_memory_if_needed
58
122
  const MEMORY_RESET_INTERVAL = 25;
59
123
  /**
60
124
  * Sample a background image's dominant colors, under both interpretations at once — see
61
- * analyze_pixel_regions for what `dims` does. Skips decoding entirely when the image matches a
62
- * known builtin background by filename + byte size (see get_builtin_bg_regions) — that fast
63
- * path needs a File (or any Blob with a `name`); a plain nameless Blob always gets a live
64
- * decode via the Canvas API. Exported so hosts can sample ahead of generate() (e.g. the widget
65
- * caches this on a debounced watcher, independent of when generate() itself runs, then passes
66
- * the result back in via GenerateOptions.image_regions).
125
+ * analyze_pixel_regions for what `dims` does. Always decodes via the Canvas API — a builtin
126
+ * background's baked regions come from get_builtin_bg_regions() by its ID instead. Exported so
127
+ * hosts can sample ahead of generate() (e.g. the widget caches this on a debounced watcher,
128
+ * independent of when generate() itself runs, then passes the result back in via
129
+ * GenerateOptions.image_regions).
67
130
  */
68
131
  export async function analyze_image_regions(image, dims) {
69
- const filename = 'name' in image ? image.name : undefined;
70
- const builtin = filename ? get_builtin_bg_regions(filename, image.size) : null;
71
- if (builtin)
72
- return builtin;
73
132
  const bitmap = await createImageBitmap(image);
74
133
  const scale = Math.min(1, IMAGE_REGIONS_MAX_DIM / Math.max(bitmap.width, bitmap.height));
75
134
  const w = Math.max(1, Math.round(bitmap.width * scale));
@@ -281,6 +340,14 @@ export class CoverGenerator {
281
340
  throw new Error(`[generator-web] ${format} output requires renderer_wasm_url in init() options`);
282
341
  }
283
342
  }
343
+ /** Run the compiler (and, for svg/png, the renderer) for the requested output format */
344
+ async compile_format(format, ppi) {
345
+ if (format === 'pdf')
346
+ return this.compile_pdf();
347
+ if (format === 'svg')
348
+ return this.compile_svg();
349
+ return this.compile_png(ppi);
350
+ }
284
351
  /** Generate a book cover using this instance's compiler and renderer */
285
352
  async generate(options) {
286
353
  const format = options.format ?? 'pdf';
@@ -306,10 +373,16 @@ export class CoverGenerator {
306
373
  }
307
374
  // Bound the compiler/renderer's WASM memory growth — see reset_memory_if_needed
308
375
  await this.reset_memory_if_needed(options.image);
376
+ // The image actually compiled — a downscaled copy for previews (see image_max_dpi), which
377
+ // only ever feeds build(); colors and the memory-reset check above use the original
378
+ const cover_dims = resolve_dimensions(parsed);
379
+ const render_image = options.image && options.image_max_dpi
380
+ ? await downscale_image(options.image, cover_dims, options.image_max_dpi)
381
+ : options.image;
309
382
  // Convert Blob to ImageInput (Uint8Array + extension) for the core builder
310
383
  let image_input;
311
- if (options.image) {
312
- const blob = options.image;
384
+ if (render_image) {
385
+ const blob = render_image;
313
386
  const ext = '.' + (blob.type.split('/')[1] ?? 'jpg').replace('jpeg', 'jpg');
314
387
  image_input = { data: new Uint8Array(await blob.arrayBuffer()), ext };
315
388
  }
@@ -321,16 +394,21 @@ export class CoverGenerator {
321
394
  frame_blob = await resp.blob();
322
395
  }
323
396
  // Colors sampled from the background image, for auto text/blurb/spine coloring. A
324
- // caller-supplied value (e.g. the widget's own debounced cache) is used as-is; otherwise
325
- // this samples `image` itself (see analyze_image_regions) — best-effort, since a decode
326
- // failure here would otherwise take down an unrelated generate() call.
397
+ // caller-supplied value (e.g. the widget's own debounced cache) is used as-is, then a
398
+ // named builtin's baked regions; otherwise this samples the original `image` itself (see
399
+ // analyze_image_regions) — best-effort, since a decode failure here would otherwise take
400
+ // down an unrelated generate() call.
401
+ const builtin_regions = options.image_builtin
402
+ ? get_builtin_bg_regions(options.image_builtin) : null;
327
403
  let image_regions;
328
404
  if (options.image_regions !== undefined) {
329
405
  image_regions = options.image_regions;
330
406
  }
407
+ else if (options.image && builtin_regions) {
408
+ image_regions = builtin_regions;
409
+ }
331
410
  else if (options.image) {
332
- image_regions = await analyze_image_regions(options.image, resolve_dimensions(parsed))
333
- .catch(() => null);
411
+ image_regions = await analyze_image_regions(options.image, cover_dims).catch(() => null);
334
412
  }
335
413
  else {
336
414
  image_regions = null;
@@ -339,16 +417,25 @@ export class CoverGenerator {
339
417
  // bookcover-core — see its generated/templates_data.ts)
340
418
  const { files, dims } = await build(parsed, image_input, frame_image, frame_blob, undefined, image_regions);
341
419
  this.load_files(files);
342
- // Compile to the requested format
420
+ // Compile to the requested format. iOS Safari/WebKit's WASM call stack inside a Worker
421
+ // is shallow enough that the Typst compiler (or, for svg/png, the renderer) can throw a
422
+ // RangeError stack overflow on a compile that succeeds on a freshly created WASM
423
+ // instance — some internal state survives compiler.reset()/the periodic renderer
424
+ // rebuild (see reset_memory_if_needed) as more generate() calls accumulate, regardless
425
+ // of the schema's own complexity. A hard reinit plus a single retry recovers
426
+ // transparently instead of hard-failing the whole session.
343
427
  let data;
344
- if (format === 'pdf') {
345
- data = await this.compile_pdf();
346
- }
347
- else if (format === 'svg') {
348
- data = await this.compile_svg();
428
+ try {
429
+ data = await this.compile_format(format, ppi);
349
430
  }
350
- else {
351
- data = await this.compile_png(ppi);
431
+ catch (error) {
432
+ if (!(error instanceof RangeError))
433
+ throw error;
434
+ await this.reinit_compiler(this.font_urls_for(needed_fonts), options.custom_fonts);
435
+ if (format !== 'pdf')
436
+ await this.reinit_renderer();
437
+ this.load_files(files);
438
+ data = await this.compile_format(format, ppi);
352
439
  }
353
440
  const result = { data };
354
441
  // Split into front/back/spine panels
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookcover-web",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "license": "MIT-0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "@myriaddreamin/typst-ts-renderer": "0.7.0",
40
40
  "@myriaddreamin/typst-ts-web-compiler": "0.7.0",
41
41
  "@myriaddreamin/typst.ts": "0.7.0",
42
- "bookcover-core": "^0.17.0",
42
+ "bookcover-core": "^0.19.0",
43
43
  "typst-fonts": "^0.1.0"
44
44
  },
45
45
  "sideEffects": false