bookcover-web 0.3.0 → 0.4.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/package.json +5 -2
- package/src/embed_types.ts +0 -42
- package/src/index.ts +0 -421
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bookcover-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT-0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "dist/index.js",
|
|
14
14
|
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/"
|
|
17
|
+
],
|
|
15
18
|
"scripts": {
|
|
16
19
|
"build": "tsc",
|
|
17
20
|
"dev": "tsc --watch"
|
|
@@ -20,7 +23,7 @@
|
|
|
20
23
|
"@myriaddreamin/typst-ts-renderer": "^0.7.0",
|
|
21
24
|
"@myriaddreamin/typst-ts-web-compiler": "^0.7.0",
|
|
22
25
|
"@myriaddreamin/typst.ts": "^0.7.0",
|
|
23
|
-
"bookcover-core": "^0.
|
|
26
|
+
"bookcover-core": "^0.4.0",
|
|
24
27
|
"typst-fonts": "^0.1.0"
|
|
25
28
|
}
|
|
26
29
|
}
|
package/src/embed_types.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
// Protocol types for embedding the cover-editor widget in an iframe. The host sends one
|
|
3
|
-
// InitMessage in reply to the widget's 'ready' message; the widget reports edits back with
|
|
4
|
-
// WidgetMessage. Binaries (background image, custom font bytes) always travel as separate
|
|
5
|
-
// structured-clone fields beside the pure-JSON form values, so hosts can persist `data`
|
|
6
|
-
// as-is (e.g. to Firestore) and store the binaries elsewhere.
|
|
7
|
-
|
|
8
|
-
import type {EmbedFormState} from 'bookcover-core'
|
|
9
|
-
import type {CustomFont} from 'typst-fonts'
|
|
10
|
-
|
|
11
|
-
/** Locales the widget UI supports. Lives in this published package, so adding a widget
|
|
12
|
-
* locale requires a bookcover-web release. */
|
|
13
|
-
export type AppLocale = 'eng' | 'vie'
|
|
14
|
-
|
|
15
|
-
/** Host -> widget, answered to the widget's 'ready' message */
|
|
16
|
-
export interface InitMessage {
|
|
17
|
-
type: 'init'
|
|
18
|
-
// Form values to preset — pure JSON, no binaries inside
|
|
19
|
-
preset?: Partial<EmbedFormState>
|
|
20
|
-
// Background image to restore into the editor (File structured-clones as a cheap blob ref)
|
|
21
|
-
bg_image?: File | null
|
|
22
|
-
// Previously uploaded font families to restore into the editor
|
|
23
|
-
custom_fonts?: CustomFont[]
|
|
24
|
-
// Swap the export button for a "Finished" signal, and show a Cancel button
|
|
25
|
-
finished_mode?: boolean
|
|
26
|
-
// Hide the Book Size sidebar section entirely
|
|
27
|
-
hide_size_section?: boolean
|
|
28
|
-
locale?: AppLocale
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Widget -> host. `schema` is the renderable generator schema derived from `data` (also
|
|
32
|
-
* derivable host-side via build_schema). `bg_image` rides on every data/finished message
|
|
33
|
-
* (cloning a File is cheap); `custom_fonts` byte arrays are expensive to clone, so on 'data'
|
|
34
|
-
* messages the field is only present when the font set changed since the last message
|
|
35
|
-
* (absent = unchanged), while 'finished' always carries the complete array. */
|
|
36
|
-
export type WidgetMessage =
|
|
37
|
-
| {type: 'ready'}
|
|
38
|
-
| {type: 'data', data: EmbedFormState, schema: Record<string, unknown>,
|
|
39
|
-
bg_image: File | null, custom_fonts?: CustomFont[]}
|
|
40
|
-
| {type: 'finished', data: EmbedFormState, schema: Record<string, unknown>,
|
|
41
|
-
bg_image: File | null, custom_fonts: CustomFont[]}
|
|
42
|
-
| {type: 'cancelled'}
|
package/src/index.ts
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
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, DOCS_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, CustomFont} 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
|
-
// Form state + form->schema conversion, so hosts can derive the renderable schema themselves
|
|
24
|
-
export {make_blank_form_values, build_schema, curly_quotes, parse_font_family, find_pattern,
|
|
25
|
-
derive_colors, hex_override_to_hsl, hex_to_hsl, is_dark_color} from 'bookcover-core'
|
|
26
|
-
export type {FormState, EmbedFormState, CustomFontStyle, DerivedColors} from 'bookcover-core'
|
|
27
|
-
|
|
28
|
-
// Embed protocol types for iframing the widget
|
|
29
|
-
export type {InitMessage, WidgetMessage, AppLocale} from './embed_types.js'
|
|
30
|
-
|
|
31
|
-
const decoder = new TextDecoder()
|
|
32
|
-
|
|
33
|
-
export interface InitOptions {
|
|
34
|
-
// URL or path to typst_ts_web_compiler_bg.wasm — required
|
|
35
|
-
wasm_url:string
|
|
36
|
-
// URL or path to typst_ts_renderer_bg.wasm — required for SVG/PNG output
|
|
37
|
-
renderer_wasm_url?:string
|
|
38
|
-
// URL prefix for the static assets tree (e.g. '/generator_assets/' in dev,
|
|
39
|
-
// 'https://assets.paper.bible/' in production). Used to load typst templates (docs/),
|
|
40
|
-
// frames, backgrounds, etc.
|
|
41
|
-
assets_prefix?:string
|
|
42
|
-
// URL prefix for fonts — curated and Noto fallback alike (e.g. '/generator_assets/fonts'
|
|
43
|
-
// in dev, 'https://assets.paper.bible/fonts' in production). Kept separate from
|
|
44
|
-
// assets_prefix since the fonts tree is published separately from the app (see the repo's
|
|
45
|
-
// .bin/download_fonts / .bin/deploy_fonts).
|
|
46
|
-
fonts_prefix?:string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface GenerateOptions {
|
|
50
|
-
schema:unknown
|
|
51
|
-
// Background image as a Blob (content type included)
|
|
52
|
-
image?:Blob
|
|
53
|
-
// Output format: 'pdf' (default), 'svg', or 'png'
|
|
54
|
-
format?:OutputFormat
|
|
55
|
-
// PPI for PNG output (default 144)
|
|
56
|
-
ppi?:number
|
|
57
|
-
// Whether to split the result into front/back/spine panels
|
|
58
|
-
split?:boolean
|
|
59
|
-
// Raw TTF bytes for user-uploaded custom fonts
|
|
60
|
-
custom_fonts?:Uint8Array[]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface GenerateResult {
|
|
64
|
-
// The full cover data (PDF/PNG bytes, or SVG string)
|
|
65
|
-
data:Uint8Array | string
|
|
66
|
-
// Split panel data (only present when split is true and format is svg/png)
|
|
67
|
-
split?:SplitResult<Uint8Array> | SplitResult<string>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/** Format compilation diagnostics into a readable error */
|
|
72
|
-
function throw_compile_error(diagnostics:unknown):never {
|
|
73
|
-
const diag = Array.isArray(diagnostics)
|
|
74
|
-
? diagnostics.map((d) => (typeof d === 'string' ? d : JSON.stringify(d))).join('\n')
|
|
75
|
-
: 'unknown error'
|
|
76
|
-
throw new Error(`[generator-web] Typst compilation failed:\n${diag}`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Create a 2D canvas — a DOM element on the main thread, OffscreenCanvas inside a worker */
|
|
80
|
-
function make_canvas(width:number, height:number):HTMLCanvasElement | OffscreenCanvas {
|
|
81
|
-
if (typeof document === 'undefined') {
|
|
82
|
-
return new OffscreenCanvas(width, height)
|
|
83
|
-
}
|
|
84
|
-
const canvas = document.createElement('canvas')
|
|
85
|
-
canvas.width = width
|
|
86
|
-
canvas.height = height
|
|
87
|
-
return canvas
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Encode a canvas' contents as PNG bytes (handles both canvas types) */
|
|
91
|
-
async function canvas_png_bytes(canvas:HTMLCanvasElement | OffscreenCanvas):Promise<Uint8Array> {
|
|
92
|
-
const blob = 'convertToBlob' in canvas
|
|
93
|
-
? await canvas.convertToBlob({type: 'image/png'})
|
|
94
|
-
: await new Promise<Blob>((resolve) => {
|
|
95
|
-
canvas.toBlob((b) => resolve(b!), 'image/png')
|
|
96
|
-
})
|
|
97
|
-
return new Uint8Array(await blob.arrayBuffer())
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Crop a region from a PNG using the Canvas API — used as PngCropFn */
|
|
101
|
-
async function canvas_crop(
|
|
102
|
-
data:Uint8Array,
|
|
103
|
-
x:number,
|
|
104
|
-
y:number,
|
|
105
|
-
w:number,
|
|
106
|
-
h:number,
|
|
107
|
-
):Promise<Uint8Array> {
|
|
108
|
-
const blob = new Blob([data as BlobPart], {type: 'image/png'})
|
|
109
|
-
const bitmap = await createImageBitmap(blob)
|
|
110
|
-
|
|
111
|
-
const canvas = make_canvas(w, h)
|
|
112
|
-
const ctx = (canvas as HTMLCanvasElement).getContext('2d')!
|
|
113
|
-
ctx.drawImage(bitmap, x, y, w, h, 0, 0, w, h)
|
|
114
|
-
bitmap.close()
|
|
115
|
-
|
|
116
|
-
return canvas_png_bytes(canvas)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Stateful cover generator — each instance owns its own compiler and renderer */
|
|
120
|
-
export class CoverGenerator {
|
|
121
|
-
private opts:InitOptions
|
|
122
|
-
// Created lazily on first generate() — the font set isn't known until a schema arrives,
|
|
123
|
-
// so creating one earlier would just instantiate WASM with fonts that get replaced
|
|
124
|
-
private compiler!:TypstCompiler
|
|
125
|
-
private renderer:TypstRenderer | null
|
|
126
|
-
|
|
127
|
-
// Cache key of the font set the current compiler was init'd with ('' = no compiler yet)
|
|
128
|
-
private active_fonts = ''
|
|
129
|
-
// Fetched font bytes keyed by URL, so a compiler reinit (any font-set change) never
|
|
130
|
-
// refetches families already seen this session
|
|
131
|
-
private font_bytes = new Map<string, Uint8Array>()
|
|
132
|
-
// Blob URLs handed to the current compiler (bundled + custom) — revoked on reinit
|
|
133
|
-
private font_blob_urls:string[] = []
|
|
134
|
-
// Identity-based ids for custom font byte arrays (see custom_font_id)
|
|
135
|
-
private custom_font_ids = new WeakMap<Uint8Array, number>()
|
|
136
|
-
private next_custom_font_id = 1
|
|
137
|
-
|
|
138
|
-
constructor(opts:InitOptions, renderer:TypstRenderer | null) {
|
|
139
|
-
this.opts = opts
|
|
140
|
-
this.renderer = renderer
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Build fetch URLs for the given font families against fonts_prefix — curated families
|
|
144
|
-
* live at <prefix>/<family>/, Noto fallback families at <prefix>/_noto/<family>/,
|
|
145
|
-
* mirroring the top-level fonts/ directory's own layout exactly */
|
|
146
|
-
private font_urls_for(families:string[]):string[] {
|
|
147
|
-
return build_font_urls(this.opts.fonts_prefix ?? '/fonts/', families)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Fetch a font file's bytes, memoised in font_bytes (failures aren't cached, so a
|
|
151
|
-
* later generate retries the fetch) */
|
|
152
|
-
private async fetch_font(url:string):Promise<Uint8Array> {
|
|
153
|
-
const cached = this.font_bytes.get(url)
|
|
154
|
-
if (cached) {
|
|
155
|
-
return cached
|
|
156
|
-
}
|
|
157
|
-
const bytes = await fetch_font_bytes(url)
|
|
158
|
-
this.font_bytes.set(url, bytes)
|
|
159
|
-
return bytes
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Pre-fetch the given font families into the in-memory byte cache */
|
|
163
|
-
async prefetch_fonts(families:string[]):Promise<void> {
|
|
164
|
-
await Promise.all(this.font_urls_for(families).map(url => this.fetch_font(url)))
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Stable id for a custom font's byte array. The widget passes the same Uint8Array
|
|
168
|
-
* references across generates, so object identity distinguishes fonts without hashing
|
|
169
|
-
* their bytes (byteLength alone could collide between two different fonts). */
|
|
170
|
-
private custom_font_id(data:Uint8Array):number {
|
|
171
|
-
let id = this.custom_font_ids.get(data)
|
|
172
|
-
if (id === undefined) {
|
|
173
|
-
id = this.next_custom_font_id
|
|
174
|
-
this.next_custom_font_id += 1
|
|
175
|
-
this.custom_font_ids.set(data, id)
|
|
176
|
-
}
|
|
177
|
-
return id
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** (Re)initialise the compiler with a different font set. Bundled font bytes come from
|
|
181
|
-
* the in-memory cache (fetched at most once per session) and everything is handed to
|
|
182
|
-
* loadFonts() as blob URLs, keeping the network out of the reinit path. The renderer
|
|
183
|
-
* never needs reinitialisation because it reads glyph outlines from compiled vector
|
|
184
|
-
* data. */
|
|
185
|
-
private async reinit_compiler(font_urls:string[], custom_fonts?:Uint8Array[]):Promise<void> {
|
|
186
|
-
// Fetch any fonts not yet cached, in parallel
|
|
187
|
-
const bundled_bytes = await Promise.all(font_urls.map(url => this.fetch_font(url)))
|
|
188
|
-
|
|
189
|
-
// Revoke the previous compiler's blob URLs to avoid memory leaks
|
|
190
|
-
revoke_blob_urls(this.font_blob_urls)
|
|
191
|
-
|
|
192
|
-
// One blob URL per font file (bundled + custom) for loadFonts() to read —
|
|
193
|
-
// mapShadow only adds to the virtual filesystem, not the font book
|
|
194
|
-
this.font_blob_urls = fonts_to_blob_urls([...bundled_bytes, ...(custom_fonts ?? [])])
|
|
195
|
-
|
|
196
|
-
// assets:false stops typst.ts silently appending its default text fonts (NewCM,
|
|
197
|
-
// Libertinus, DejaVu... fetched from jsdelivr) — all fonts we need are passed explicitly
|
|
198
|
-
const compiler_font_opts = loadFonts(this.font_blob_urls, {assets: false})
|
|
199
|
-
|
|
200
|
-
const c = createTypstCompiler()
|
|
201
|
-
await c.init({
|
|
202
|
-
getModule: () => ({module_or_path: this.opts.wasm_url}),
|
|
203
|
-
beforeBuild: [compiler_font_opts],
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
this.compiler = c
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Load the typst file map into the compiler's shadow filesystem */
|
|
210
|
-
private load_files(files:Map<string, Uint8Array>):void {
|
|
211
|
-
this.compiler.resetShadow()
|
|
212
|
-
for (const [filename, bytes] of files) {
|
|
213
|
-
const vpath = `/${filename}`
|
|
214
|
-
if (filename.endsWith('.typ')) {
|
|
215
|
-
this.compiler.addSource(vpath, decoder.decode(bytes))
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
this.compiler.mapShadow(vpath, bytes)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Compile to PDF and return raw bytes */
|
|
224
|
-
private async compile_pdf():Promise<Uint8Array> {
|
|
225
|
-
const result = await this.compiler.compile({
|
|
226
|
-
mainFilePath: '/cover.typ',
|
|
227
|
-
format: CompileFormatEnum.pdf,
|
|
228
|
-
})
|
|
229
|
-
if (!result.result) {
|
|
230
|
-
throw_compile_error(result.diagnostics)
|
|
231
|
-
}
|
|
232
|
-
return result.result!
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Compile to vector format, then render as SVG string covering the full canvas */
|
|
236
|
-
private async compile_svg():Promise<string> {
|
|
237
|
-
this.assert_renderer('SVG')
|
|
238
|
-
const result = await this.compiler.compile({
|
|
239
|
-
mainFilePath: '/cover.typ',
|
|
240
|
-
format: CompileFormatEnum.vector,
|
|
241
|
-
})
|
|
242
|
-
if (!result.result) {
|
|
243
|
-
throw_compile_error(result.diagnostics)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const svg = await this.renderer!.renderSvg({
|
|
247
|
-
artifactContent: result.result!,
|
|
248
|
-
format: 'vector',
|
|
249
|
-
data_selection: {js: false, css: true, body: true, defs: true},
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
// Patch unitless width/height to add `pt` suffix (Typst SVG output uses pt) so
|
|
253
|
-
// browsers render at 96 CSS px per inch, matching the split SVGs from split_svg()
|
|
254
|
-
return svg.replace(/<svg([^>]*)>/, (_match, attrs:string) => {
|
|
255
|
-
const patched = attrs
|
|
256
|
-
.replace(/width="([\d.]+)"/, 'width="$1pt"')
|
|
257
|
-
.replace(/height="([\d.]+)"/, 'height="$1pt"')
|
|
258
|
-
return `<svg${patched}>`
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Compile to vector format, then render as PNG via offscreen canvas */
|
|
263
|
-
private async compile_png(ppi:number):Promise<Uint8Array> {
|
|
264
|
-
this.assert_renderer('PNG')
|
|
265
|
-
const result = await this.compiler.compile({
|
|
266
|
-
mainFilePath: '/cover.typ',
|
|
267
|
-
format: CompileFormatEnum.vector,
|
|
268
|
-
})
|
|
269
|
-
if (!result.result) {
|
|
270
|
-
throw_compile_error(result.diagnostics)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Render to a canvas at the desired PPI (typst uses 72pt/inch)
|
|
274
|
-
const scale = ppi / 72
|
|
275
|
-
const canvas = document.createElement('canvas')
|
|
276
|
-
|
|
277
|
-
await this.renderer!.renderCanvas({
|
|
278
|
-
artifactContent: result.result!,
|
|
279
|
-
format: 'vector',
|
|
280
|
-
canvas,
|
|
281
|
-
pageOffset: 0,
|
|
282
|
-
pixelPerPt: scale,
|
|
283
|
-
backgroundColor: '#ffffff',
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
// Extract PNG from canvas
|
|
287
|
-
const blob = await new Promise<Blob>((resolve) => {
|
|
288
|
-
canvas.toBlob((b) => resolve(b!), 'image/png')
|
|
289
|
-
})
|
|
290
|
-
return new Uint8Array(await blob.arrayBuffer())
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Throw if the renderer was not initialised */
|
|
294
|
-
private assert_renderer(format:string):void {
|
|
295
|
-
if (!this.renderer) {
|
|
296
|
-
throw new Error(
|
|
297
|
-
`[generator-web] ${format} output requires renderer_wasm_url in init() options`
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Generate a book cover using this instance's compiler and renderer */
|
|
303
|
-
async generate(options:GenerateOptions):Promise<GenerateResult> {
|
|
304
|
-
const format = options.format ?? 'pdf'
|
|
305
|
-
// Default to 144 PPI (2x typographic resolution, 2 × 72pt/in)
|
|
306
|
-
const ppi = options.ppi ?? 144
|
|
307
|
-
const split = options.split ?? false
|
|
308
|
-
|
|
309
|
-
// Parse schema and get dimensions from printing-services
|
|
310
|
-
const parsed = cover_schema.parse(options.schema)
|
|
311
|
-
const assets = this.opts.assets_prefix ?? '/assets/'
|
|
312
|
-
|
|
313
|
-
// (Re)initialise the compiler when the set of required fonts changes — including on
|
|
314
|
-
// the very first generate, since the compiler is created lazily ('' never matches).
|
|
315
|
-
// Noto Serif (base font) is always included — it's not bundled in the WASM assets.
|
|
316
|
-
// collect_all_fonts also includes every Noto fallback family needed to cover non-Latin
|
|
317
|
-
// scripts detected in the schema's text, so only those specific families get fetched.
|
|
318
|
-
const needed_fonts = collect_all_fonts(parsed)
|
|
319
|
-
const custom_fonts_id = options.custom_fonts
|
|
320
|
-
? options.custom_fonts.map(b => String(this.custom_font_id(b))).join(':')
|
|
321
|
-
: ''
|
|
322
|
-
const cache_key = needed_fonts.join(',') + '|' + custom_fonts_id
|
|
323
|
-
if (cache_key !== this.active_fonts) {
|
|
324
|
-
await this.reinit_compiler(this.font_urls_for(needed_fonts), options.custom_fonts)
|
|
325
|
-
this.active_fonts = cache_key
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Convert Blob to ImageInput (Uint8Array + extension) for the core builder
|
|
329
|
-
let image_input:{data:Uint8Array, ext:string} | undefined
|
|
330
|
-
if (options.image) {
|
|
331
|
-
const blob = options.image
|
|
332
|
-
const ext = '.' + (blob.type.split('/')[1] ?? 'jpg').replace('jpeg', 'jpg')
|
|
333
|
-
image_input = {data: new Uint8Array(await blob.arrayBuffer()), ext}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Load typst templates from assets (docs/ subdir)
|
|
337
|
-
const [cover_typ, helpers_typ] = await Promise.all([
|
|
338
|
-
fetch(asset_path(assets, DOCS_DIR, TEMPLATE_FILES.cover)).then(r => r.text()),
|
|
339
|
-
fetch(asset_path(assets, DOCS_DIR, TEMPLATE_FILES.helpers)).then(r => r.text()),
|
|
340
|
-
])
|
|
341
|
-
const templates:Templates = {cover: cover_typ, helpers: helpers_typ}
|
|
342
|
-
|
|
343
|
-
// Load the frame PNG from assets when the schema uses painted coverage
|
|
344
|
-
let frame_blob:Blob | undefined
|
|
345
|
-
if (parsed.bg_image_coverage === 'painted') {
|
|
346
|
-
const url = frame_asset_path(assets, 'painted')
|
|
347
|
-
const resp = await fetch(url)
|
|
348
|
-
frame_blob = await resp.blob()
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const {files, dims} = await build(templates, parsed, image_input, frame_image, frame_blob)
|
|
352
|
-
this.load_files(files)
|
|
353
|
-
|
|
354
|
-
// Compile to the requested format
|
|
355
|
-
let data:Uint8Array | string
|
|
356
|
-
|
|
357
|
-
if (format === 'pdf') {
|
|
358
|
-
data = await this.compile_pdf()
|
|
359
|
-
}
|
|
360
|
-
else if (format === 'svg') {
|
|
361
|
-
data = await this.compile_svg()
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
data = await this.compile_png(ppi)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const result:GenerateResult = {data}
|
|
368
|
-
|
|
369
|
-
// Split into front/back/spine panels
|
|
370
|
-
if (split) {
|
|
371
|
-
if (format === 'pdf') {
|
|
372
|
-
result.split = split_pdf(data as Uint8Array, dims)
|
|
373
|
-
}
|
|
374
|
-
else if (format === 'svg') {
|
|
375
|
-
result.split = split_svg(data as string, dims)
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
result.split = await split_png(data as Uint8Array, dims, ppi, canvas_crop)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return result
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** Revoke blob URLs and drop cached font bytes to free memory */
|
|
386
|
-
destroy():void {
|
|
387
|
-
revoke_blob_urls(this.font_blob_urls)
|
|
388
|
-
this.font_blob_urls = []
|
|
389
|
-
this.font_bytes.clear()
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Create a CoverGenerator instance with an optional initialised WASM renderer.
|
|
395
|
-
* The compiler itself is created lazily by the first generate() call, once the schema's
|
|
396
|
-
* font set is known. Each instance is independent — multiple generators can run concurrently.
|
|
397
|
-
*/
|
|
398
|
-
export async function init(options:InitOptions):Promise<CoverGenerator> {
|
|
399
|
-
// Load the curated font manifest before anything else below resolves a font family
|
|
400
|
-
await load_fonts_prefix(options.fonts_prefix ?? '/fonts/')
|
|
401
|
-
|
|
402
|
-
// Renderer only needs base fonts (glyph shapes are embedded in compiled vector data)
|
|
403
|
-
let renderer:TypstRenderer | null = null
|
|
404
|
-
if (options.renderer_wasm_url) {
|
|
405
|
-
const renderer_font_opts = loadFonts([])
|
|
406
|
-
const r = createTypstRenderer()
|
|
407
|
-
await r.init({
|
|
408
|
-
getModule: () => ({module_or_path: options.renderer_wasm_url!}),
|
|
409
|
-
beforeBuild: [renderer_font_opts],
|
|
410
|
-
})
|
|
411
|
-
renderer = r
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const gen = new CoverGenerator(options, renderer)
|
|
415
|
-
|
|
416
|
-
// Warm the font byte cache with the always-needed base font while the rest of the app
|
|
417
|
-
// finishes loading — a failure here is fine, the first generate simply refetches
|
|
418
|
-
gen.prefetch_fonts([base_font()]).catch(() => {})
|
|
419
|
-
|
|
420
|
-
return gen
|
|
421
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/index.ts"],"version":"5.9.3"}
|