diff --git a/src/engine/mod.rs b/src/engine/mod.rs index c588e5c32..d9d169656 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,8 +1,11 @@ //! Core typesetting engine. +use std::cell::{RefCell, Ref}; +use std::collections::HashMap; +use std::mem::swap; use crate::syntax::{SyntaxTree, Node}; use crate::doc::{Document, Page, Text, TextCommand}; -use crate::font::{Font, FontFamily, FontFilter, FontError}; +use crate::font::{Font, FontFamily, FontInfo, FontError}; use crate::Context; mod size; @@ -11,16 +14,21 @@ pub use size::Size; /// The core typesetting engine, transforming an abstract syntax tree into a document. pub struct Engine<'t> { - // Immutable + // Input tree: &'t SyntaxTree<'t>, ctx: &'t Context<'t>, - // Mutable - fonts: Vec, - active_font: usize, + // Internal + font_loader: FontLoader<'t>, + + // Output text_commands: Vec, - current_line: String, - current_width: Size, + + // Intermediates + active_font: usize, + current_text: String, + current_line_width: Size, + current_max_vertical_move: Size, } impl<'t> Engine<'t> { @@ -29,121 +37,287 @@ impl<'t> Engine<'t> { Engine { tree, ctx: context, - fonts: vec![], - active_font: 0, + font_loader: FontLoader::new(context), text_commands: vec![], - current_line: String::new(), - current_width: Size::zero(), + active_font: std::usize::MAX, + current_text: String::new(), + current_line_width: Size::zero(), + current_max_vertical_move: Size::zero(), } } /// Generate the abstract document. pub(crate) fn typeset(mut self) -> TypeResult { - // Load font defined by style - let mut font = None; - let filter = FontFilter::new(&self.ctx.style.font_families); - for provider in &self.ctx.font_providers { - let available = provider.available(); - for info in available { - if filter.matches(info) { - if let Some(mut source) = provider.get(info) { - let mut program = Vec::new(); - source.read_to_end(&mut program)?; - font = Some(Font::new(program)?); - break; - } - } - } - } - - let font = match font { - Some(font) => font, - None => return Err(TypesetError::MissingFont), - }; - - self.fonts.push(font); - self.active_font = 0; - - // Move cursor to top-left position - self.text_commands.push(TextCommand::Move( - self.ctx.style.margin_left, - self.ctx.style.height - self.ctx.style.margin_top - )); - - // Set the current font - self.text_commands.push(TextCommand::SetFont(0, self.ctx.style.font_size)); + // Start by moving to a suitable position. + self.move_start(); // Iterate through the documents nodes. for node in &self.tree.nodes { match node { - Node::Word(word) => self.write_word(word), - - Node::Space => self.write_space(), + Node::Word(word) => self.write_word(word)?, + Node::Space => self.write_space()?, Node::Newline => (), - Node::ToggleItalics | Node::ToggleBold | Node::ToggleMath => unimplemented!(), Node::Func(_) => unimplemented!(), } } - // Create a page from the contents. - let page = Page { - width: self.ctx.style.width, - height: self.ctx.style.height, - text: vec![Text { - commands: self.text_commands, - }], - }; + // Flush the text buffer. + self.write_buffered_text(); + let fonts = self.font_loader.into_fonts(); + + println!("fonts: {:?}", fonts.len()); + + // Create a document with one page from the contents. Ok(Document { - pages: vec![page], - fonts: self.fonts, + pages: vec![Page { + width: self.ctx.style.width, + height: self.ctx.style.height, + text: vec![Text { + commands: self.text_commands, + }], + }], + fonts, }) } - fn write_word(&mut self, word: &str) { - let font = &self.fonts[self.active_font]; + /// Move to the starting position defined by the style. + fn move_start(&mut self) { + // Move cursor to top-left position + self.text_commands.push(TextCommand::Move( + self.ctx.style.margin_left, + self.ctx.style.height - self.ctx.style.margin_top + )); + } - let width = self.width(word); - if self.would_overflow(width) { - let vertical_move = - self.ctx.style.font_size + /// Move to a new line. + fn move_newline(&mut self) { + let vertical_move = - if self.current_max_vertical_move == Size::zero() { + // If max vertical move is still zero, the line is empty and we take the + // font size from the previous line. + self.ctx.style.font_size * self.ctx.style.line_spacing - * font.metrics.ascender; - self.text_commands.push(TextCommand::Move(Size::zero(), vertical_move)); + * self.font_loader.get_at(self.active_font).metrics.ascender + } else { + self.current_max_vertical_move + }; - self.current_line.clear(); - self.current_width = Size::zero(); - } - - self.text_commands.push(TextCommand::Text(word.to_owned())); - self.current_line.push_str(word); - self.current_width += width; + self.text_commands.push(TextCommand::Move(Size::zero(), vertical_move)); + self.current_max_vertical_move = Size::zero(); + self.current_line_width = Size::zero(); } - fn write_space(&mut self) { - let space_width = self.width(" "); + /// Set the current font. + fn set_font(&mut self, index: usize) { + self.text_commands.push(TextCommand::SetFont(index, self.ctx.style.font_size)); + self.active_font = index; + } - if !self.would_overflow(space_width) && !self.current_line.is_empty() { - self.text_commands.push(TextCommand::Text(" ".to_owned())); - self.current_line.push_str(" "); - self.current_width += space_width; + /// Write a word. + fn write_word(&mut self, word: &str) -> TypeResult<()> { + let width = self.width(word)?; + + // If this would overflow, we move to a new line and finally write the previous one. + if self.would_overflow(width) { + self.write_buffered_text(); + self.move_newline(); + } + + for c in word.chars() { + let (index, _) = self.get_font_for(c)?; + if index != self.active_font { + self.write_buffered_text(); + self.set_font(index); + } + self.current_text.push(c); + let char_width = self.char_width(c).unwrap(); + self.current_line_width += char_width; + } + + Ok(()) + } + + /// Write the space character: `' '`. + fn write_space(&mut self) -> TypeResult<()> { + let space_width = self.char_width(' ')?; + + if !self.would_overflow(space_width) && self.current_line_width > Size::zero() { + self.write_word(" ")?; + } + + Ok(()) + } + + /// Write a text command with the buffered text. + fn write_buffered_text(&mut self) { + if !self.current_text.is_empty() { + let mut current_text = String::new(); + swap(&mut self.current_text, &mut current_text); + self.text_commands.push(TextCommand::Text(current_text)); } } - fn width(&self, word: &str) -> Size { - let font = &self.fonts[self.active_font]; - word.chars() - .map(|c| font.widths[font.map(c) as usize] * self.ctx.style.font_size) - .sum() - } - + /// Whether the current line plus the extra `width` would overflow the line. fn would_overflow(&self, width: Size) -> bool { let max_width = self.ctx.style.width - - self.ctx.style.margin_left - - self.ctx.style.margin_right; - - self.current_width + width > max_width + - self.ctx.style.margin_left - self.ctx.style.margin_right; + self.current_line_width + width > max_width } + + /// The width of a word when printed out. + fn width(&self, word: &str) -> TypeResult { + let mut width = Size::zero(); + for c in word.chars() { + width += self.char_width(c)?; + } + Ok(width) + } + + /// The width of a char when printed out. + fn char_width(&self, character: char) -> TypeResult { + let font = self.get_font_for(character)?.1; + Ok(font.widths[font.map(character) as usize] * self.ctx.style.font_size) + } + + /// Load a font that has the character we need. + fn get_font_for(&self, character: char) -> TypeResult<(usize, Ref)> { + let res = self.font_loader.get(FontQuery { + families: &self.ctx.style.font_families, + italic: false, + bold: false, + character, + }).ok_or_else(|| TypesetError::MissingFont)?; + Ok(res) + } +} + +/// Serves matching fonts given a query. +struct FontLoader<'t> { + /// The context containing the used font providers. + context: &'t Context<'t>, + /// All available fonts indexed by provider. + availables: Vec<&'t [FontInfo]>, + /// Allows to lookup fonts by their infos. + indices: RefCell>, + /// Allows to retrieve cached results for queries. + matches: RefCell, usize>>, + /// All loaded fonts. + loaded: RefCell>, + /// Indexed by outside and indices maps to internal indices. + external: RefCell>, +} + +impl<'t> FontLoader<'t> { + /// Create a new font loader. + pub fn new(context: &'t Context<'t>) -> FontLoader { + let availables = context.font_providers.iter() + .map(|prov| prov.available()).collect(); + + FontLoader { + context, + availables, + indices: RefCell::new(HashMap::new()), + matches: RefCell::new(HashMap::new()), + loaded: RefCell::new(vec![]), + external: RefCell::new(vec![]), + } + } + + /// Return the list of fonts. + pub fn into_fonts(self) -> Vec { + // FIXME: Don't clone here. + let fonts = self.loaded.into_inner(); + self.external.into_inner().into_iter().map(|index| fonts[index].clone()).collect() + } + + /// Return the best matching font and it's index (if there is any) given the query. + pub fn get(&self, query: FontQuery<'t>) -> Option<(usize, Ref)> { + if let Some(index) = self.matches.borrow().get(&query) { + let external = self.external.borrow().iter().position(|i| i == index).unwrap(); + return Some((external, self.get_at_internal(*index))); + } + + // Go through all available fonts and try to find one. + for family in query.families { + for (p, available) in self.availables.iter().enumerate() { + for info in available.iter() { + if Self::matches(query, &family, info) { + if let Some((index, font)) = self.try_load(info, p) { + if font.mapping.contains_key(&query.character) { + self.matches.borrow_mut().insert(query, index); + + let pos = self.external.borrow().iter().position(|&i| i == index); + let external = pos.unwrap_or_else(|| { + let external = self.external.borrow().len(); + self.external.borrow_mut().push(index); + external + }); + + return Some((external, font)); + } + } + } + } + } + } + + None + } + + /// Return a loaded font at an index. Panics if the index is out of bounds. + pub fn get_at(&self, index: usize) -> Ref { + let internal = self.external.borrow()[index]; + self.get_at_internal(internal) + } + + /// Try to load the font with the given info from the provider. + fn try_load(&self, info: &FontInfo, provider: usize) -> Option<(usize, Ref)> { + if let Some(index) = self.indices.borrow().get(info) { + return Some((*index, self.get_at_internal(*index))); + } + + if let Some(mut source) = self.context.font_providers[provider].get(info) { + let mut program = Vec::new(); + source.read_to_end(&mut program).ok()?; + + let font = Font::new(program).ok()?; + + let index = self.loaded.borrow().len(); + println!("loading at interal index: {}", index); + self.loaded.borrow_mut().push(font); + self.indices.borrow_mut().insert(info.clone(), index); + + Some((index, self.get_at_internal(index))) + } else { + None + } + } + + /// Return a loaded font at an internal index. Panics if the index is out of bounds. + fn get_at_internal(&self, index: usize) -> Ref { + Ref::map(self.loaded.borrow(), |loaded| &loaded[index]) + } + + /// Check whether the query and the current family match the info. + fn matches(query: FontQuery, family: &FontFamily, info: &FontInfo) -> bool { + info.families.contains(family) + && info.italic == query.italic && info.bold == query.bold + } +} + +/// A query for a font with specific properties. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct FontQuery<'a> { + /// A fallback list of font families to accept. The first family in this list, that also + /// satisfies the other conditions, shall be returned. + families: &'a [FontFamily], + /// Whether the font shall be in italics. + italic: bool, + /// Whether the font shall be in boldface. + bold: bool, + /// Which character we need. + character: char, } /// Default styles for typesetting. diff --git a/src/export/pdf.rs b/src/export/pdf.rs index b466f225d..4b42f5c2b 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -58,7 +58,7 @@ impl<'d, W: Write> PdfEngine<'d, W> { let pages = (page_tree + 1, page_tree + doc.pages.len() as Ref); let content_count = doc.pages.iter().flat_map(|p| p.text.iter()).count() as Ref; let contents = (pages.1 + 1, pages.1 + content_count); - let fonts = (contents.1 + 1, contents.1 + 4 * doc.fonts.len() as Ref); + let fonts = (contents.1 + 1, contents.1 + 5 * doc.fonts.len() as Ref); let offsets = Offsets { catalog, page_tree, pages, contents, fonts }; // Create a subsetted PDF font for each font in the document. @@ -109,10 +109,14 @@ impl<'d, W: Write> PdfEngine<'d, W> { // The document catalog. self.writer.write_obj(self.offsets.catalog, &Catalog::new(self.offsets.page_tree))?; + // The font resources. + let fonts = (0 .. self.fonts.len()) + .map(|i| Resource::Font((i + 1) as u32, self.offsets.fonts.0 + 5 * i as u32)); + // The root page tree. self.writer.write_obj(self.offsets.page_tree, PageTree::new() .kids(ids(self.offsets.pages)) - .resource(Resource::Font(1, self.offsets.fonts.0)) + .resources(fonts) )?; // The page objects. diff --git a/src/font.rs b/src/font.rs index f596af0f8..53d31a41d 100644 --- a/src/font.rs +++ b/src/font.rs @@ -172,7 +172,7 @@ pub struct FontMetrics { pub weight_class: u16, } -/// A type that provides fonts matching given criteria. +/// A type that provides fonts. pub trait FontProvider { /// Returns the font with the given info if this provider has it. fn get(&self, info: &FontInfo) -> Option>; @@ -194,7 +194,7 @@ impl FontData for T where T: Read + Seek {} /// Describes a font. /// /// Can be constructed conventiently with the [`font_info`] macro. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct FontInfo { /// The font families this font is part of. pub families: Vec, @@ -274,50 +274,8 @@ macro_rules! font_info { (@__gen Monospace) => { $crate::font::FontFamily::Monospace }; } -/// Criteria to filter fonts. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct FontFilter<'a> { - /// A fallback list of font families to accept. The first family in this list, that also - /// satisfies the other conditions, shall be returned. - pub families: &'a [FontFamily], - /// If some, matches only italic/non-italic fonts, otherwise any. - pub italic: Option, - /// If some, matches only bold/non-bold fonts, otherwise any. - pub bold: Option, -} - -impl<'a> FontFilter<'a> { - /// Create a new font config with the given families. - /// - /// All other fields are set to [`None`] and match anything. - pub fn new(families: &'a [FontFamily]) -> FontFilter<'a> { - FontFilter { - families, - italic: None, - bold: None, - } - } - - /// Set the italic value to something. - pub fn italic(&mut self, italic: bool) -> &mut Self { - self.italic = Some(italic); self - } - - /// Set the bold value to something. - pub fn bold(&mut self, bold: bool) -> &mut Self { - self.bold = Some(bold); self - } - - /// Whether this filter matches the given info. - pub fn matches(&self, info: &FontInfo) -> bool { - self.italic.map(|i| i == info.italic).unwrap_or(true) - && self.bold.map(|i| i == info.bold).unwrap_or(true) - && self.families.iter().any(|family| info.families.contains(family)) - } -} - /// A family of fonts (either generic or named). -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum FontFamily { SansSerif, Serif, diff --git a/src/lib.rs b/src/lib.rs index 65955d487..f8c0c59be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! use typeset::export::pdf::PdfExporter; //! //! // Simple example source code. -//! let src = "Hello World from Typeset!"; +//! let src = "Hello World from Typeset! 🌍"; //! //! // Create a compiler with a font provider that provides three fonts //! // (the default sans-serif fonts and a fallback for the emoji). @@ -145,6 +145,7 @@ error_type! { #[cfg(test)] mod test { use std::fs::File; + use std::io::BufWriter; use crate::Compiler; use crate::export::pdf::PdfExporter; use crate::font::FileSystemFontProvider; @@ -167,7 +168,7 @@ mod test { // Write to file let path = format!("../target/typeset-pdf-{}.pdf", name); - let file = File::create(path).unwrap(); + let file = BufWriter::new(File::create(path).unwrap()); let exporter = PdfExporter::new(); exporter.export(&document, file).unwrap(); } @@ -193,6 +194,11 @@ mod test { test("composite-glyph", "Composite character‼"); } + #[test] + fn mixed_emoji() { + test("mixed-emoji", "Hello World 🌍!") + } + #[test] fn long_wikipedia() { test("wikipedia", r#"