diff --git a/src-tauri/src/pdf.rs b/src-tauri/src/pdf.rs index 36a9293..ecc456a 100644 --- a/src-tauri/src/pdf.rs +++ b/src-tauri/src/pdf.rs @@ -10,6 +10,29 @@ static LITERATA: &[u8] = include_bytes!("../../public/fonts/Literata-VF.ttf"); static LITERATA_ITALIC: &[u8] = include_bytes!("../../public/fonts/Literata-Italic-VF.ttf"); static HANKEN: &[u8] = include_bytes!("../../public/fonts/HankenGrotesk-VF.ttf"); static HANKEN_ITALIC: &[u8] = include_bytes!("../../public/fonts/HankenGrotesk-Italic-VF.ttf"); +static EB_GARAMOND: &[u8] = include_bytes!("../../public/fonts/EBGaramond-VF.ttf"); +static EB_GARAMOND_ITALIC: &[u8] = include_bytes!("../../public/fonts/EBGaramond-Italic-VF.ttf"); +static LORA: &[u8] = include_bytes!("../../public/fonts/Lora-VF.ttf"); +static LORA_ITALIC: &[u8] = include_bytes!("../../public/fonts/Lora-Italic-VF.ttf"); +static SOURCE_SERIF: &[u8] = include_bytes!("../../public/fonts/SourceSerif4-VF.ttf"); +static SOURCE_SERIF_ITALIC: &[u8] = include_bytes!("../../public/fonts/SourceSerif4-Italic-VF.ttf"); +static FRAUNCES: &[u8] = include_bytes!("../../public/fonts/Fraunces-VF.ttf"); +static FRAUNCES_ITALIC: &[u8] = include_bytes!("../../public/fonts/Fraunces-Italic-VF.ttf"); + +static EB_GARAMOND_FONTS: [&[u8]; 2] = [EB_GARAMOND, EB_GARAMOND_ITALIC]; +static LORA_FONTS: [&[u8]; 2] = [LORA, LORA_ITALIC]; +static SOURCE_SERIF_FONTS: [&[u8]; 2] = [SOURCE_SERIF, SOURCE_SERIF_ITALIC]; +static FRAUNCES_FONTS: [&[u8]; 2] = [FRAUNCES, FRAUNCES_ITALIC]; + +fn bundled_font_bytes(id: &str) -> &'static [&'static [u8]] { + match id { + "eb-garamond" => &EB_GARAMOND_FONTS, + "lora" => &LORA_FONTS, + "source-serif" => &SOURCE_SERIF_FONTS, + "fraunces" => &FRAUNCES_FONTS, + _ => &[], + } +} #[derive(Deserialize)] pub struct ImageInput { @@ -17,7 +40,12 @@ pub struct ImageInput { data: String, } -fn compile(source: String, images: &[ImageInput]) -> Result<(Vec, String), String> { +fn compile( + source: String, + images: &[ImageInput], + bundled_fonts: &[String], + system_fonts: &[String], +) -> Result<(Vec, String), String> { let mut binaries: Vec<(&str, Vec)> = Vec::with_capacity(images.len()); for image in images { let bytes = STANDARD @@ -26,9 +54,24 @@ fn compile(source: String, images: &[ImageInput]) -> Result<(Vec, String), S binaries.push((image.path.as_str(), bytes)); } + let mut fonts: Vec> = vec![ + LITERATA.to_vec(), + LITERATA_ITALIC.to_vec(), + HANKEN.to_vec(), + HANKEN_ITALIC.to_vec(), + ]; + for id in bundled_fonts { + for bytes in bundled_font_bytes(id) { + fonts.push(bytes.to_vec()); + } + } + for family in system_fonts { + fonts.extend(crate::fonts::system_font_bytes(family)); + } + let engine = TypstEngine::builder() .main_file(source) - .fonts([LITERATA, LITERATA_ITALIC, HANKEN, HANKEN_ITALIC]) + .fonts(fonts) .with_static_file_resolver(binaries) .build(); @@ -50,8 +93,10 @@ pub fn compile_pdf( source: String, images: Vec, emit_warnings: bool, + bundled_fonts: Vec, + system_fonts: Vec, ) -> Result { - let (bytes, warnings) = compile(source, &images)?; + let (bytes, warnings) = compile(source, &images, &bundled_fonts, &system_fonts)?; if emit_warnings && !warnings.is_empty() { app.emit("pdf-warnings", warnings).ok(); } diff --git a/src/components/Dock.tsx b/src/components/Dock.tsx index 700306f..3c125d5 100644 --- a/src/components/Dock.tsx +++ b/src/components/Dock.tsx @@ -107,8 +107,8 @@ function PdfDock() { clearTimeout(timer.current); setBusy(true); timer.current = setTimeout(() => { - const { source, images } = coverActive ? coverToPdfInputs(book) : chapterToPdfInputs(book, idx); - compilePdf(source, images) + const { source, images, fonts } = coverActive ? coverToPdfInputs(book) : chapterToPdfInputs(book, idx); + compilePdf(source, images, false, fonts) .then((bytes) => { setPdf(bytes); setError(null); diff --git a/src/export/exporters.ts b/src/export/exporters.ts index fbefae8..e3e04a0 100644 --- a/src/export/exporters.ts +++ b/src/export/exporters.ts @@ -8,8 +8,8 @@ const PDF = [{ name: "PDF", extensions: ["pdf"] }]; const EPUB = [{ name: "EPUB", extensions: ["epub"] }]; export async function exportPdf(book: Book): Promise { - const { source, images } = bookToPdfInputs(book); - const bytes = await compilePdf(source, images, true); + const { source, images, fonts } = bookToPdfInputs(book); + const bytes = await compilePdf(source, images, true, fonts); await saveBytes(bytes, `${book.metadata.title || "Untitled"}.pdf`, PDF); } diff --git a/src/export/typst.ts b/src/export/typst.ts index db65fc6..542f72d 100644 --- a/src/export/typst.ts +++ b/src/export/typst.ts @@ -1,7 +1,18 @@ import type { JSONContent } from "@tiptap/core"; import { type Book, type FigurePlacement, type TrimSize, FIGURE_WIDTH, bodyNumber, chapterKind, isResizablePlacement } from "../model/book"; +import { fontFamilyName, fontsUsed } from "../model/fonts"; import type { ImageInput } from "../ipc"; +export interface PdfFonts { + bundled: string[]; + system: string[]; +} + +function pdfFonts(book: Book): PdfFonts { + const used = fontsUsed(book.settings.fonts); + return { bundled: used.bundled.map((f) => f.id), system: used.system }; +} + const TRIM: Record = { "6x9": { w: "6in", h: "9in" }, "5.5x8.5": { w: "5.5in", h: "8.5in" }, @@ -104,6 +115,8 @@ function preamble(book: Book): string { const trim = TRIM[book.settings.trim]; const meta = book.metadata; const bleed = book.settings.bleed ? "0.125in" : "0in"; + const body = fontFamilyName(book.settings.fonts.body); + const heading = fontFamilyName(book.settings.fonts.heading); return `#set document(title: ${str(meta.title || "Untitled")}, author: ${str(meta.author)}) #set page( width: ${trim.w} + ${bleed}, @@ -111,9 +124,9 @@ function preamble(book: Book): string { margin: (inside: 0.875in, outside: 0.625in + ${bleed}, top: 0.8in + ${bleed}, bottom: 0.8in + ${bleed}), binding: left, ) -#set text(font: "Literata", size: 11pt, lang: ${str(meta.language || "en")}, hyphenate: true) +#set text(font: ${str(body)}, size: 11pt, lang: ${str(meta.language || "en")}, hyphenate: true) #set par(justify: true, leading: 0.72em, spacing: 0.72em) -#show heading: set text(font: "Literata", weight: "medium") +#show heading: set text(font: ${str(heading)}, weight: "medium") #show heading.where(level: 1): set text(size: 22pt) #show heading.where(level: 2): set block(above: 1.4em, below: 0.6em) #show heading.where(level: 3): set block(above: 1.2em, below: 0.5em) @@ -125,10 +138,10 @@ function preamble(book: Book): string { #let titlepage(title, subtitle, author) = { v(2.4in) align(center)[ - #text(font: "Literata", size: 30pt, weight: "medium")[#title] + #text(font: ${str(heading)}, size: 30pt, weight: "medium")[#title] #if subtitle != "" { v(0.6em) - text(font: "Literata", size: 15pt, style: "italic", fill: rgb("#6b6458"))[#subtitle] + text(font: ${str(body)}, size: 15pt, style: "italic", fill: rgb("#6b6458"))[#subtitle] } #if author != "" { v(1.7em) @@ -164,7 +177,7 @@ function preamble(book: Book): string { set par(justify: false, first-line-indent: 0pt) block(below: 2em)[ #show heading: set block(above: 0pt, below: 0pt) - #heading(level: 1, numbering: none, outlined: false, bookmarked: true)[#text(font: "Literata", size: 24pt, weight: "medium")[Contents]] + #heading(level: 1, numbering: none, outlined: false, bookmarked: true)[#text(font: ${str(heading)}, size: 24pt, weight: "medium")[Contents]] ] context { let items = query().filter(it => it.value.toc != false) @@ -200,13 +213,15 @@ function imageExtension(dataUrl: string): string { function coverBlock(book: Book, coverPath?: string): string { const c = book.cover; const meta = book.metadata; + const body = fontFamilyName(book.settings.fonts.body); + const heading = fontFamilyName(book.settings.fonts.heading); if (c.kind === "image" && coverPath) { return `#page(margin: 0pt, numbering: none)[#image(${str(coverPath)}, width: 100%, height: 100%, fit: "cover")]`; } const subtitle = meta.subtitle - ? `#v(0.55em)\n #text(font: "Literata", size: 14pt, style: "italic")[${esc(meta.subtitle)}]` + ? `#v(0.55em)\n #text(font: ${str(body)}, size: 14pt, style: "italic")[${esc(meta.subtitle)}]` : ""; const author = meta.author ? `#v(1fr)\n #text(font: "Hanken Grotesk", size: 11pt, weight: "semibold", tracking: 0.24em)[#upper[${esc(meta.author)}]]\n #v(0.55in)` @@ -216,7 +231,7 @@ function coverBlock(book: Book, coverPath?: string): string { #set align(center) #set text(fill: rgb(${str(c.ink)})) #v(1fr) - #text(font: "Literata", size: 30pt, weight: "medium")[${esc(meta.title || "Untitled")}] + #text(font: ${str(heading)}, size: 30pt, weight: "medium")[${esc(meta.title || "Untitled")}] #v(0.5em) #line(length: 16%, stroke: 0.6pt + rgb(${str(c.ink)})) ${subtitle} @@ -334,16 +349,16 @@ export function bookToTypst(book: Book, paths: Map = new Map(), return `${preamble(book)}\n\n${cover}\n\n${front}\n\n${body}\n`; } -export function bookToPdfInputs(book: Book): { source: string; images: ImageInput[] } { +export function bookToPdfInputs(book: Book): { source: string; images: ImageInput[]; fonts: PdfFonts } { const { images, paths } = extractImages(book); const coverPath = pushCoverImage(book, images); - return { source: bookToTypst(book, paths, coverPath), images }; + return { source: bookToTypst(book, paths, coverPath), images, fonts: pdfFonts(book) }; } -export function coverToPdfInputs(book: Book): { source: string; images: ImageInput[] } { +export function coverToPdfInputs(book: Book): { source: string; images: ImageInput[]; fonts: PdfFonts } { const images: ImageInput[] = []; const coverPath = pushCoverImage(book, images); - return { source: `${preamble(book)}\n\n${coverBlock(book, coverPath)}\n`, images }; + return { source: `${preamble(book)}\n\n${coverBlock(book, coverPath)}\n`, images, fonts: pdfFonts(book) }; } function chapterToTypst(book: Book, index: number, paths: Map): string { @@ -358,8 +373,8 @@ ${chapterBody(book.chapters[index].content, paths)} `; } -export function chapterToPdfInputs(book: Book, index: number): { source: string; images: ImageInput[] } { +export function chapterToPdfInputs(book: Book, index: number): { source: string; images: ImageInput[]; fonts: PdfFonts } { const chapter = book.chapters[index]; const { images, paths } = collectImages([chapter.content]); - return { source: chapterToTypst(book, index, paths), images }; + return { source: chapterToTypst(book, index, paths), images, fonts: pdfFonts(book) }; } diff --git a/src/ipc.ts b/src/ipc.ts index 32cdc48..1adeb0e 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -7,12 +7,24 @@ export interface ImageInput { data: string; } +export interface PdfFonts { + bundled: string[]; + system: string[]; +} + export async function compilePdf( source: string, images: ImageInput[] = [], emitWarnings = false, + fonts: PdfFonts = { bundled: [], system: [] }, ): Promise { - const buffer = await invoke("compile_pdf", { source, images, emitWarnings }); + const buffer = await invoke("compile_pdf", { + source, + images, + emitWarnings, + bundledFonts: fonts.bundled, + systemFonts: fonts.system, + }); return new Uint8Array(buffer); }