diff --git a/src/bin/main.rs b/src/bin/main.rs index b548214a1..9e1403b2b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -5,7 +5,7 @@ use std::process; use std::io::Read; use std::path::{Path, PathBuf}; -use typeset::Compiler; +use typeset::Typesetter; use typeset::{font::FileSystemFontProvider, font_info}; use typeset::export::pdf::PdfExporter; @@ -33,10 +33,10 @@ fn run() -> Result<(), Box> { let mut src = String::new(); file.read_to_string(&mut src).map_err(|_| "failed to read from source file")?; - // Create a compiler with a font provider that provides three fonts + // Create a typesetter with a font provider that provides three fonts // (two sans-serif fonts and a fallback for the emoji). - let mut compiler = Compiler::new(); - compiler.add_font_provider(FileSystemFontProvider::new("fonts", vec![ + let mut typesetter = Typesetter::new(); + typesetter.add_font_provider(FileSystemFontProvider::new("fonts", vec![ ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])), ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)), ("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)), @@ -45,9 +45,8 @@ fn run() -> Result<(), Box> { ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), ])); - // Compile the source code with the compiler. - let document = compiler.compile(&src)?; - + // Typeset the source code. + let document = typesetter.typeset(&src)?; // Export the document into a PDF file. let exporter = PdfExporter::new(); diff --git a/src/doc.rs b/src/doc.rs index 83a3b300b..51583bdc8 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -1,7 +1,7 @@ //! Representation of typesetted documents. use crate::font::Font; -use crate::engine::Size; +use crate::layout::Size; /// A complete typesetted document, which can be exported. diff --git a/src/engine/loader.rs b/src/engine/loader.rs deleted file mode 100644 index bae5f51a3..000000000 --- a/src/engine/loader.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Loading of fonts by queries. - -use std::fmt::{self, Debug, Formatter}; -use std::cell::{RefCell, Ref}; -use std::collections::HashMap; -use crate::font::{Font, FontProvider, FontFamily, FontInfo}; - - -/// Serves matching fonts given a query. -pub struct FontLoader<'p> { - /// The font providers. - providers: &'p [Box], - /// All available fonts indexed by provider. - provider_fonts: Vec<&'p [FontInfo]>, - /// The internal state. - state: RefCell>, -} - -/// Internal state of the font loader (wrapped in a RefCell). -struct FontLoaderState<'p> { - /// The loaded fonts along with their external indices. - fonts: Vec<(Option, Font)>, - /// Allows to retrieve cached results for queries. - query_cache: HashMap, usize>, - /// Allows to lookup fonts by their infos. - info_cache: HashMap<&'p FontInfo, usize>, - /// Indexed by outside and indices maps to internal indices. - inner_index: Vec, -} - -impl<'p> FontLoader<'p> { - /// Create a new font loader. - pub fn new(providers: &'p [Box]) -> FontLoader { - let provider_fonts = providers.iter() - .map(|prov| prov.available()).collect(); - - FontLoader { - providers, - provider_fonts, - state: RefCell::new(FontLoaderState { - query_cache: HashMap::new(), - info_cache: HashMap::new(), - inner_index: vec![], - fonts: vec![], - }), - } - } - - /// Return the best matching font and it's index (if there is any) given the query. - pub fn get(&self, query: FontQuery<'p>) -> Option<(usize, Ref)> { - // Check if we had the exact same query before. - let state = self.state.borrow(); - if let Some(&index) = state.query_cache.get(&query) { - // That this is the query cache means it must has an index as we've served it before. - let extern_index = state.fonts[index].0.unwrap(); - let font = Ref::map(state, |s| &s.fonts[index].1); - - return Some((extern_index, font)); - } - drop(state); - - // Go over all font infos from all font providers that match the query. - for family in query.families { - for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) { - for info in infos.iter() { - // Check whether this info matches the query. - if Self::matches(query, family, info) { - let mut state = self.state.borrow_mut(); - - // Check if we have already loaded this font before. - // Otherwise we'll fetch the font from the provider. - let index = if let Some(&index) = state.info_cache.get(info) { - index - } else if let Some(mut source) = provider.get(info) { - // Read the font program into a vec. - let mut program = Vec::new(); - source.read_to_end(&mut program).ok()?; - - // Create a font from it. - let font = Font::new(program).ok()?; - - // Insert it into the storage. - let index = state.fonts.len(); - state.info_cache.insert(info, index); - state.fonts.push((None, font)); - - index - } else { - continue; - }; - - // Check whether this font has the character we need. - let has_char = state.fonts[index].1.mapping.contains_key(&query.character); - if has_char { - // We can take this font, so we store the query. - state.query_cache.insert(query, index); - - // Now we have to find out the external index of it, or assign a new - // one if it has not already one. - let maybe_extern_index = state.fonts[index].0; - let extern_index = maybe_extern_index.unwrap_or_else(|| { - // We have to assign an external index before serving. - let extern_index = state.inner_index.len(); - state.inner_index.push(index); - state.fonts[index].0 = Some(extern_index); - extern_index - }); - - // Release the mutable borrow and borrow immutably. - drop(state); - let font = Ref::map(self.state.borrow(), |s| &s.fonts[index].1); - - // Finally we can return it. - return Some((extern_index, font)); - } - } - } - } - } - - None - } - - /// Return a loaded font at an index. Panics if the index is out of bounds. - pub fn get_with_index(&self, index: usize) -> Ref { - let state = self.state.borrow(); - let internal = state.inner_index[index]; - Ref::map(state, |s| &s.fonts[internal].1) - } - - /// Return the list of fonts. - pub fn into_fonts(self) -> Vec { - // Sort the fonts by external key so that they are in the correct order. - let mut fonts = self.state.into_inner().fonts; - fonts.sort_by_key(|&(maybe_index, _)| match maybe_index { - Some(index) => index as isize, - None => -1, - }); - - // Remove the fonts that are not used from the outside - fonts.into_iter().filter_map(|(maybe_index, font)| { - maybe_index.map(|_| font) - }).collect() - } - - /// 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 - } -} - -impl Debug for FontLoader<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let state = self.state.borrow(); - f.debug_struct("FontLoader") - .field("providers", &self.providers.len()) - .field("provider_fonts", &self.provider_fonts) - .field("fonts", &state.fonts) - .field("query_cache", &state.query_cache) - .field("info_cache", &state.info_cache) - .field("inner_index", &state.inner_index) - .finish() - } -} - -/// A query for a font with specific properties. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FontQuery<'p> { - /// 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: &'p [FontFamily], - /// Whether the font shall be in italics. - pub italic: bool, - /// Whether the font shall be in boldface. - pub bold: bool, - /// Which character we need. - pub character: char, -} diff --git a/src/engine/mod.rs b/src/engine/mod.rs deleted file mode 100644 index a396ed3aa..000000000 --- a/src/engine/mod.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! Core typesetting engine. - -use std::cell::Ref; -use std::mem::swap; - -use smallvec::SmallVec; - -use crate::doc::{Document, Page, Text, TextCommand}; -use crate::font::{Font, FontFamily, FontProvider, FontError}; -use crate::syntax::{SyntaxTree, Node}; -use loader::{FontLoader, FontQuery}; - -mod size; -mod loader; -pub use size::Size; - - -/// Typeset a parsed syntax tree. -pub fn typeset<'p>(tree: &SyntaxTree, style: &Style, font_providers: &[Box]) - -> TypesetResult { - Engine::new(tree, style, font_providers).typeset() -} - - -/// The core typesetting engine, transforming an abstract syntax tree into a document. -struct Engine<'a> { - // Input - tree: &'a SyntaxTree, - style: &'a Style, - - // Internal - font_loader: FontLoader<'a>, - - // Output - text_commands: Vec, - - // Intermediates - active_font: usize, - current_text: String, - current_line_width: Size, - current_max_vertical_move: Size, - bold: bool, - italic: bool, -} - -impl<'a> Engine<'a> { - /// Create a new generator from a syntax tree. - fn new( - tree: &'a SyntaxTree, - style: &'a Style, - font_providers: &'a [Box] - ) -> Engine<'a> { - Engine { - tree, - style, - font_loader: FontLoader::new(font_providers), - text_commands: vec![], - active_font: std::usize::MAX, - current_text: String::new(), - current_line_width: Size::zero(), - current_max_vertical_move: Size::zero(), - italic: false, - bold: false, - } - } - - /// Generate the abstract document. - fn typeset(mut self) -> TypesetResult { - // Start by moving to a suitable position. - self.move_start(); - - // Iterate through the documents nodes. - for node in &self.tree.nodes { - match node { - Node::Text(text) => self.write_word(text)?, - Node::Space => self.write_space()?, - Node::Newline => { - self.write_buffered_text(); - self.move_newline(self.style.paragraph_spacing); - }, - - Node::ToggleItalics => self.italic = !self.italic, - Node::ToggleBold => self.bold = !self.bold, - - Node::ToggleMath => unimplemented!(), - Node::Func(_) => unimplemented!(), - } - } - - // Flush the text buffer. - self.write_buffered_text(); - - // Create a document with one page from the contents. - Ok(Document { - pages: vec![Page { - width: self.style.width, - height: self.style.height, - text: vec![Text { - commands: self.text_commands, - }], - }], - fonts: self.font_loader.into_fonts(), - }) - } - - /// Write a word. - fn write_word(&mut self, word: &str) -> TypesetResult<()> { - // Contains pairs of (characters, font_index, char_width). - let mut chars_with_widths = SmallVec::<[(char, usize, Size); 12]>::new(); - - // Find out which font to use for each character in the word and meanwhile - // calculate the width of the word. - let mut word_width = Size::zero(); - for c in word.chars() { - let (index, font) = self.get_font_for(c)?; - let width = self.char_width(c, &font); - word_width += width; - chars_with_widths.push((c, index, width)); - } - - // If this would overflow, we move to a new line and finally write the previous one. - if self.would_overflow(word_width) { - self.write_buffered_text(); - self.move_newline(1.0); - } - - // Finally write the word. - for (c, index, width) in chars_with_widths { - if index != self.active_font { - // If we will change the font, first write the remaining things. - self.write_buffered_text(); - self.set_font(index); - } - - self.current_text.push(c); - self.current_line_width += width; - } - - Ok(()) - } - - /// Write the space character: `' '`. - fn write_space(&mut self) -> TypesetResult<()> { - let space_width = self.char_width(' ', &self.get_font_for(' ')?.1); - 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)); - } - } - - /// 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.style.margin_left, - self.style.height - self.style.margin_top - )); - } - - /// Move to a new line. - fn move_newline(&mut self, factor: f32) { - if self.active_font == std::usize::MAX { - return; - } - - 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.style.font_size - * self.style.line_spacing - * self.get_font_at(self.active_font).metrics.ascender - * factor - } else { - self.current_max_vertical_move - }; - - self.text_commands.push(TextCommand::Move(Size::zero(), -vertical_move)); - self.current_max_vertical_move = Size::zero(); - self.current_line_width = Size::zero(); - } - - /// Set the current font. - fn set_font(&mut self, index: usize) { - self.text_commands.push(TextCommand::SetFont(index, self.style.font_size)); - self.active_font = index; - } - - /// Whether the current line plus the extra `width` would overflow the line. - fn would_overflow(&self, width: Size) -> bool { - let max_width = self.style.width - - self.style.margin_left - self.style.margin_right; - self.current_line_width + width > max_width - } - - /// Load a font that has the character we need. - fn get_font_for(&self, character: char) -> TypesetResult<(usize, Ref)> { - self.font_loader.get(FontQuery { - families: &self.style.font_families, - italic: self.italic, - bold: self.bold, - character, - }).ok_or_else(|| TypesetError::MissingFont) - } - - /// Load a font at an index. - fn get_font_at(&self, index: usize) -> Ref { - self.font_loader.get_with_index(index) - } - - /// The width of a char in a specific font. - fn char_width(&self, character: char, font: &Font) -> Size { - font.widths[font.map(character) as usize] * self.style.font_size - } -} - -/// The context for typesetting a function. -#[derive(Debug)] -pub struct TypesetContext {} - -/// Default styles for typesetting. -#[derive(Debug, Clone, PartialEq)] -pub struct Style { - /// The width of the paper. - pub width: Size, - /// The height of the paper. - pub height: Size, - - /// The left margin of the paper. - pub margin_left: Size, - /// The top margin of the paper. - pub margin_top: Size, - /// The right margin of the paper. - pub margin_right: Size, - /// The bottom margin of the paper. - pub margin_bottom: Size, - - /// A fallback list of font families to use. - pub font_families: Vec, - /// The font size. - pub font_size: f32, - /// The line spacing (as a multiple of the font size). - pub line_spacing: f32, - /// The spacing for paragraphs (as a multiple of the line spacing). - pub paragraph_spacing: f32, -} - -impl Default for Style { - fn default() -> Style { - use FontFamily::*; - Style { - // A4 paper. - width: Size::from_mm(210.0), - height: Size::from_mm(297.0), - - // Margins. A bit more on top and bottom. - margin_left: Size::from_cm(3.0), - margin_top: Size::from_cm(3.0), - margin_right: Size::from_cm(3.0), - margin_bottom: Size::from_cm(3.0), - - // Default font family, font size and line spacing. - font_families: vec![SansSerif, Serif, Monospace], - font_size: 11.0, - line_spacing: 1.25, - paragraph_spacing: 1.5, - } - } -} - -/// The error type for typesetting. -pub enum TypesetError { - /// There was no suitable font. - MissingFont, - /// An error occured while gathering font data. - Font(FontError), -} - -/// The result type for typesetting. -pub type TypesetResult = Result; - -error_type! { - err: TypesetError, - show: f => match err { - TypesetError::MissingFont => write!(f, "missing font"), - TypesetError::Font(err) => write!(f, "font error: {}", err), - }, - source: match err { - TypesetError::Font(err) => Some(err), - _ => None, - }, - from: (std::io::Error, TypesetError::Font(FontError::Io(err))), - from: (FontError, TypesetError::Font(err)), -} diff --git a/src/export/pdf.rs b/src/export/pdf.rs index ee7ce1ef6..55e00abbb 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -10,7 +10,7 @@ use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream}; use crate::doc::{Document, Text as DocText, TextCommand}; use crate::font::{Font, FontError}; -use crate::engine::Size; +use crate::layout::Size; /// Exports documents into _PDFs_. diff --git a/src/font.rs b/src/font.rs index 19b09b311..173312343 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,16 +1,18 @@ //! Font loading and transforming. //! //! # Font handling -//! To do the typesetting, the compiler needs font data. To be highly portable the compiler assumes -//! nothing about the environment. To still work with fonts, the consumer of this library has to -//! add _font providers_ to their compiler instance. These can be queried for font data given -//! flexible font filters specifying required font families and styles. A font provider is a type -//! implementing the [`FontProvider`](crate::font::FontProvider) trait. +//! To do the typesetting, the typesetting engine needs font data. To be highly portable the engine +//! itself assumes nothing about the environment. To still work with fonts, the consumer of this +//! library has to add _font providers_ to their typesetting instance. These can be queried for font +//! data given flexible font filters specifying required font families and styles. A font provider +//! is a type implementing the [`FontProvider`](crate::font::FontProvider) trait. //! -//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves -//! fonts from a folder on the file system. +//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves fonts +//! from a folder on the file system. +use std::cell::{RefCell, Ref}; use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; use std::fs::File; use std::io::{self, Cursor, Read, Seek, SeekFrom, BufReader}; use std::path::PathBuf; @@ -20,7 +22,7 @@ use opentype::{Error as OpentypeError, OpenTypeReader, Outlines, TableRecord, Ta use opentype::tables::{Header, Name, CharMap, MaximumProfile, HorizontalMetrics, Post, OS2}; use opentype::global::{MacStyleFlags, NameEntry}; -use crate::engine::Size; +use crate::layout::Size; /// A loaded font, containing relevant information for typesetting. @@ -174,25 +176,6 @@ pub struct FontMetrics { pub weight_class: u16, } -/// 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>; - - /// The available fonts this provider can serve. While these should generally be retrievable - /// through the `get` method, it is not guaranteed that a font info that is contained here - /// yields a `Some` value when passed into `get`. - fn available<'a>(&'a self) -> &'a [FontInfo]; -} - -/// A wrapper trait around `Read + Seek`. -/// -/// This type is needed because currently you can't make a trait object -/// with two traits, like `Box`. -/// Automatically implemented for all types that are [`Read`] and [`Seek`]. -pub trait FontData: Read + Seek {} -impl FontData for T where T: Read + Seek {} - /// Describes a font. /// /// Can be constructed conveniently with the [`font_info`] macro. @@ -285,6 +268,25 @@ pub enum FontFamily { Named(String), } +/// 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>; + + /// The available fonts this provider can serve. While these should generally be retrievable + /// through the `get` method, it is not guaranteed that a font info that is contained here + /// yields a `Some` value when passed into `get`. + fn available<'a>(&'a self) -> &'a [FontInfo]; +} + +/// A wrapper trait around `Read + Seek`. +/// +/// This type is needed because currently you can't make a trait object +/// with two traits, like `Box`. +/// Automatically implemented for all types that are [`Read`] and [`Seek`]. +pub trait FontData: Read + Seek {} +impl FontData for T where T: Read + Seek {} + /// A font provider serving fonts from a folder on the local file system. pub struct FileSystemFontProvider { base: PathBuf, @@ -349,10 +351,182 @@ impl FontProvider for FileSystemFontProvider { } } -struct Subsetter<'d> { +/// Serves matching fonts given a query. +pub struct FontLoader<'p> { + /// The font providers. + providers: Vec<&'p (dyn FontProvider + 'p)>, + /// All available fonts indexed by provider. + provider_fonts: Vec<&'p [FontInfo]>, + /// The internal state. + state: RefCell>, +} + +/// Internal state of the font loader (wrapped in a RefCell). +struct FontLoaderState<'p> { + /// The loaded fonts along with their external indices. + fonts: Vec<(Option, Font)>, + /// Allows to retrieve cached results for queries. + query_cache: HashMap, usize>, + /// Allows to lookup fonts by their infos. + info_cache: HashMap<&'p FontInfo, usize>, + /// Indexed by outside and indices maps to internal indices. + inner_index: Vec, +} + +impl<'p> FontLoader<'p> { + /// Create a new font loader. + pub fn new(providers: &'p [P]) -> FontLoader<'p> where P: AsRef { + let providers: Vec<_> = providers.iter().map(|p| p.as_ref()).collect(); + let provider_fonts = providers.iter().map(|prov| prov.available()).collect(); + + FontLoader { + providers, + provider_fonts, + state: RefCell::new(FontLoaderState { + query_cache: HashMap::new(), + info_cache: HashMap::new(), + inner_index: vec![], + fonts: vec![], + }), + } + } + + /// Return the best matching font and it's index (if there is any) given the query. + pub fn get(&self, query: FontQuery<'p>) -> Option<(usize, Ref)> { + // Check if we had the exact same query before. + let state = self.state.borrow(); + if let Some(&index) = state.query_cache.get(&query) { + // That this is the query cache means it must has an index as we've served it before. + let extern_index = state.fonts[index].0.unwrap(); + let font = Ref::map(state, |s| &s.fonts[index].1); + + return Some((extern_index, font)); + } + drop(state); + + // Go over all font infos from all font providers that match the query. + for family in query.families { + for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) { + for info in infos.iter() { + // Check whether this info matches the query. + if Self::matches(query, family, info) { + let mut state = self.state.borrow_mut(); + + // Check if we have already loaded this font before. + // Otherwise we'll fetch the font from the provider. + let index = if let Some(&index) = state.info_cache.get(info) { + index + } else if let Some(mut source) = provider.get(info) { + // Read the font program into a vec. + let mut program = Vec::new(); + source.read_to_end(&mut program).ok()?; + + // Create a font from it. + let font = Font::new(program).ok()?; + + // Insert it into the storage. + let index = state.fonts.len(); + state.info_cache.insert(info, index); + state.fonts.push((None, font)); + + index + } else { + continue; + }; + + // Check whether this font has the character we need. + let has_char = state.fonts[index].1.mapping.contains_key(&query.character); + if has_char { + // We can take this font, so we store the query. + state.query_cache.insert(query, index); + + // Now we have to find out the external index of it, or assign a new + // one if it has not already one. + let maybe_extern_index = state.fonts[index].0; + let extern_index = maybe_extern_index.unwrap_or_else(|| { + // We have to assign an external index before serving. + let extern_index = state.inner_index.len(); + state.inner_index.push(index); + state.fonts[index].0 = Some(extern_index); + extern_index + }); + + // Release the mutable borrow and borrow immutably. + drop(state); + let font = Ref::map(self.state.borrow(), |s| &s.fonts[index].1); + + // Finally we can return it. + return Some((extern_index, font)); + } + } + } + } + } + + None + } + + /// Return a loaded font at an index. Panics if the index is out of bounds. + pub fn get_with_index(&self, index: usize) -> Ref { + let state = self.state.borrow(); + let internal = state.inner_index[index]; + Ref::map(state, |s| &s.fonts[internal].1) + } + + /// Return the list of fonts. + pub fn into_fonts(self) -> Vec { + // Sort the fonts by external key so that they are in the correct order. + let mut fonts = self.state.into_inner().fonts; + fonts.sort_by_key(|&(maybe_index, _)| match maybe_index { + Some(index) => index as isize, + None => -1, + }); + + // Remove the fonts that are not used from the outside + fonts.into_iter().filter_map(|(maybe_index, font)| { + maybe_index.map(|_| font) + }).collect() + } + + /// 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 + } +} + +impl Debug for FontLoader<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let state = self.state.borrow(); + f.debug_struct("FontLoader") + .field("providers", &self.providers.len()) + .field("provider_fonts", &self.provider_fonts) + .field("fonts", &state.fonts) + .field("query_cache", &state.query_cache) + .field("info_cache", &state.info_cache) + .field("inner_index", &state.inner_index) + .finish() + } +} + +/// A query for a font with specific properties. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub 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. + pub families: &'a [FontFamily], + /// Whether the font shall be in italics. + pub italic: bool, + /// Whether the font shall be in boldface. + pub bold: bool, + /// Which character we need. + pub character: char, +} + +struct Subsetter<'a> { // Original font - font: &'d Font, - reader: OpenTypeReader>, + font: &'a Font, + reader: OpenTypeReader>, outlines: Outlines, tables: Vec, cmap: Option, @@ -366,7 +540,7 @@ struct Subsetter<'d> { body: Vec, } -impl<'d> Subsetter<'d> { +impl<'a> Subsetter<'a> { fn subset(mut self, needed_tables: I, optional_tables: I) -> FontResult where I: IntoIterator, S: AsRef { // Find out which glyphs to include based on which characters we want @@ -695,7 +869,7 @@ impl<'d> Subsetter<'d> { })) } - fn get_table_data(&self, tag: Tag) -> FontResult<&'d [u8]> { + fn get_table_data(&self, tag: Tag) -> FontResult<&'a [u8]> { let record = match self.tables.binary_search_by_key(&tag, |r| r.tag) { Ok(index) => &self.tables[index], Err(_) => return Err(FontError::MissingTable(tag.to_string())), diff --git a/src/func.rs b/src/func.rs index c90e87ad3..ef5120dfa 100644 --- a/src/func.rs +++ b/src/func.rs @@ -6,7 +6,7 @@ use std::fmt::{self, Debug, Formatter}; use crate::syntax::FuncHeader; use crate::parsing::{ParseContext, ParseResult}; -use crate::engine::{TypesetContext, TypesetResult}; +use crate::layout::{Layout, LayoutContext, LayoutResult}; /// Types that act as functions. @@ -17,12 +17,16 @@ use crate::engine::{TypesetContext, TypesetResult}; /// The trait `FunctionBounds` is automatically implemented for types which can be /// used as functions, that is they fulfill the bounds `Debug + PartialEq + 'static`. pub trait Function: FunctionBounds { - /// Parse the tokens of the context with the given header and scope into self. + /// Parse the header and body into this function given this context. fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext) -> ParseResult where Self: Sized; - /// Execute the function and optionally yield a return value. - fn typeset(&self, ctx: &TypesetContext) -> TypesetResult<()>; + /// Layout this function given a context. + /// + /// Returns optionally the resulting layout and a if changes to the context + /// should be made new context. + fn layout(&self, ctx: &LayoutContext) + -> LayoutResult<(Option, Option)>; } impl PartialEq for dyn Function { diff --git a/src/layout/mod.rs b/src/layout/mod.rs new file mode 100644 index 000000000..7bc62bd3f --- /dev/null +++ b/src/layout/mod.rs @@ -0,0 +1,118 @@ +//! Layouting engine. + +use crate::doc::Document; +use crate::font::{Font, FontLoader, FontFamily, FontError}; +use crate::syntax::SyntaxTree; + +mod size; +pub use size::Size; + + +/// Layout a syntax tree given a context. +#[allow(unused_variables)] +pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult { + Ok(Layout {}) +} + +/// A collection of layouted content. +pub struct Layout {} + +impl Layout { + /// Convert this layout into a document given the list of fonts referenced by it. + pub fn into_document(self, fonts: Vec) -> Document { + Document { + pages: vec![], + fonts, + } + } +} + +/// The context for layouting. +pub struct LayoutContext<'a, 'p> { + pub loader: &'a FontLoader<'p>, +} + +/// Default styles for pages. +#[derive(Debug, Clone, PartialEq)] +pub struct PageStyle { + /// The width of the paper. + pub width: Size, + /// The height of the paper. + pub height: Size, + + /// The left margin of the paper. + pub margin_left: Size, + /// The top margin of the paper. + pub margin_top: Size, + /// The right margin of the paper. + pub margin_right: Size, + /// The bottom margin of the paper. + pub margin_bottom: Size, +} + +impl Default for PageStyle { + fn default() -> PageStyle { + PageStyle { + // A4 paper. + width: Size::from_mm(210.0), + height: Size::from_mm(297.0), + + // Margins. A bit more on top and bottom. + margin_left: Size::from_cm(3.0), + margin_top: Size::from_cm(3.0), + margin_right: Size::from_cm(3.0), + margin_bottom: Size::from_cm(3.0), + } + } +} + +/// Default styles for texts. +#[derive(Debug, Clone, PartialEq)] +pub struct TextStyle { + /// A fallback list of font families to use. + pub font_families: Vec, + /// The font size. + pub font_size: f32, + /// The line spacing (as a multiple of the font size). + pub line_spacing: f32, + /// The spacing for paragraphs (as a multiple of the line spacing). + pub paragraph_spacing: f32, +} + +impl Default for TextStyle { + fn default() -> TextStyle { + use FontFamily::*; + TextStyle { + // Default font family, font size and line spacing. + font_families: vec![SansSerif, Serif, Monospace], + font_size: 11.0, + line_spacing: 1.25, + paragraph_spacing: 1.5, + } + } +} + +/// The error type for layouting. +pub enum LayoutError { + /// There was no suitable font. + MissingFont, + /// An error occured while gathering font data. + Font(FontError), +} + +/// The result type for layouting. +pub type LayoutResult = Result; + +error_type! { + err: LayoutError, + show: f => match err { + LayoutError::MissingFont => write!(f, "missing font"), + LayoutError::Font(err) => write!(f, "font error: {}", err), + }, + source: match err { + LayoutError::Font(err) => Some(err), + _ => None, + }, + from: (std::io::Error, LayoutError::Font(FontError::Io(err))), + from: (FontError, LayoutError::Font(err)), +} diff --git a/src/engine/size.rs b/src/layout/size.rs similarity index 100% rename from src/engine/size.rs rename to src/layout/size.rs diff --git a/src/lib.rs b/src/lib.rs index aa017cad9..9a1082c6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,9 @@ //! [iterator of tokens](crate::parsing::Tokens). Then the [parser](crate::parsing::Parser) //! operates on that to construct a syntax tree. The structures describing the tree can be found //! in the [syntax] module. -//! - **Typesetting:** The next step is to transform the syntax tree into a portable representation -//! of the typesetted document. Types for these can be found in the [doc] module. This -//! representation contains already the finished layout. +//! - **Layouting:** The next step is to transform the syntax tree into a portable representation +//! of the typesetted document. Types for these can be found in the [doc] and [layout] modules. +//! This representation contains already the finished layout. //! - **Exporting:** The finished document can then be exported into supported formats. Submodules //! for the supported formats are located in the [export] module. Currently the only supported //! format is _PDF_. @@ -15,24 +15,24 @@ //! # Example //! ``` //! use std::fs::File; -//! use typeset::Compiler; +//! use typeset::Typesetter; //! use typeset::{font::FileSystemFontProvider, font_info}; //! use typeset::export::pdf::PdfExporter; //! //! // Simple example source code. //! let src = "Hello World from __Typeset__! 🌍"; //! -//! // Create a compiler with a font provider that provides three fonts +//! // Create a typesetter with a font provider that provides three fonts //! // (two sans-serif fonts and a fallback for the emoji). -//! let mut compiler = Compiler::new(); -//! compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ +//! let mut typesetter = Typesetter::new(); +//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ //! ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])), //! ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)), //! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), //! ])); //! -//! // Compile the source code into a document with the compiler. -//! let document = compiler.compile(src).unwrap(); +//! // Typeset the source code into a document. +//! let document = typesetter.typeset(src).unwrap(); //! //! // Export the document into a PDF file. //! # /* @@ -44,51 +44,61 @@ //! ``` use crate::doc::Document; -use crate::engine::{typeset, Style, TypesetResult, TypesetError}; use crate::func::Scope; -use crate::font::FontProvider; -use crate::parsing::{parse, ParseResult, ParseError}; +use crate::font::{Font, FontLoader, FontProvider}; +use crate::layout::{layout, Layout, LayoutContext, LayoutResult, LayoutError}; +use crate::layout::{PageStyle, TextStyle}; +use crate::parsing::{parse, ParseContext, ParseResult, ParseError}; use crate::syntax::SyntaxTree; #[macro_use] mod error; pub mod doc; -pub mod engine; pub mod export; #[macro_use] pub mod font; pub mod func; +pub mod layout; pub mod parsing; pub mod syntax; /// Transforms source code into typesetted documents. /// -/// Holds the compilation context, which can be configured through various methods. -pub struct Compiler<'p> { - /// Style for typesetting. - style: Style, +/// Holds the typesetting context, which can be configured through various methods. +pub struct Typesetter<'p> { + /// The default page style. + base_page_style: PageStyle, + /// The default text style. + base_text_style: TextStyle, /// Font providers. font_providers: Vec>, } -impl<'p> Compiler<'p> { - /// Create a new compiler. +impl<'p> Typesetter<'p> { + /// Create a new typesetter. #[inline] - pub fn new() -> Compiler<'p> { - Compiler { - style: Style::default(), + pub fn new() -> Typesetter<'p> { + Typesetter { + base_page_style: PageStyle::default(), + base_text_style: TextStyle::default(), font_providers: vec![], } } - /// Set the default style for the document. + /// Set the default page style for the document. #[inline] - pub fn set_style(&mut self, style: Style) { - self.style = style; + pub fn set_page_style(&mut self, style: PageStyle) { + self.base_page_style = style; } - /// Add a font provider to the context of this compiler. + /// Set the default text style for the document. + #[inline] + pub fn set_text_style(&mut self, style: TextStyle) { + self.base_text_style = style; + } + + /// Add a font provider to the context of this typesetter. #[inline] pub fn add_font_provider(&mut self, provider: P) where P: FontProvider { self.font_providers.push(Box::new(provider)); @@ -98,45 +108,50 @@ impl<'p> Compiler<'p> { #[inline] pub fn parse(&self, src: &str) -> ParseResult { let scope = Scope::with_std(); - parse(src, &scope) + let ctx = ParseContext { scope: &scope }; + parse(src, &ctx) } - /// Typeset a parsed syntax tree into a document. + /// Layout a parsed syntax tree and return the layout and the referenced font list. #[inline] - pub fn typeset(&self, tree: &SyntaxTree) -> TypesetResult { - typeset(&tree, &self.style, &self.font_providers).map_err(Into::into) + pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(Layout, Vec)> { + let loader = FontLoader::new(&self.font_providers); + let ctx = LayoutContext { loader: &loader }; + let layout = layout(&tree, &ctx)?; + Ok((layout, loader.into_fonts())) } - /// Compile a portable typesetted document from source code. + /// Typeset a portable document from source code. #[inline] - pub fn compile(&self, src: &str) -> Result { + pub fn typeset(&self, src: &str) -> Result { let tree = self.parse(src)?; - let document = self.typeset(&tree)?; + let (layout, fonts) = self.layout(&tree)?; + let document = layout.into_document(fonts); Ok(document) } } -/// The general error type for compilation. -pub enum CompileError { +/// The general error type for typesetting. +pub enum TypesetError { /// An error that occured while transforming source code into /// an abstract syntax tree. - ParseErr(ParseError), - /// An error that occured while typesetting into an abstract document. - TypesetErr(TypesetError), + Parse(ParseError), + /// An error that occured while layouting. + Layout(LayoutError), } error_type! { - err: CompileError, + err: TypesetError, show: f => match err { - CompileError::ParseErr(e) => write!(f, "parse error: {}", e), - CompileError::TypesetErr(e) => write!(f, "typeset error: {}", e), + TypesetError::Parse(e) => write!(f, "parse error: {}", e), + TypesetError::Layout(e) => write!(f, "layout error: {}", e), }, source: match err { - CompileError::ParseErr(e) => Some(e), - CompileError::TypesetErr(e) => Some(e), + TypesetError::Parse(e) => Some(e), + TypesetError::Layout(e) => Some(e), }, - from: (ParseError, CompileError::ParseErr(err)), - from: (TypesetError, CompileError::TypesetErr(err)), + from: (ParseError, TypesetError::Parse(err)), + from: (LayoutError, TypesetError::Layout(err)), } @@ -144,15 +159,14 @@ error_type! { mod test { use std::fs::File; use std::io::BufWriter; - use crate::Compiler; + use crate::Typesetter; use crate::export::pdf::PdfExporter; use crate::font::FileSystemFontProvider; /// Create a pdf with a name from the source code. fn test(name: &str, src: &str) { - // Create compiler - let mut compiler = Compiler::new(); - compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ + let mut typesetter = Typesetter::new(); + typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![ ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])), ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)), ("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)), @@ -161,8 +175,8 @@ mod test { ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])), ])); - // Compile into document - let document = compiler.compile(src).unwrap(); + // Typeset into document + let document = typesetter.typeset(src).unwrap(); // Write to file let path = format!("../target/typeset-unit-{}.pdf", name); diff --git a/src/parsing.rs b/src/parsing.rs index f9ebe49a6..594077f76 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -324,18 +324,25 @@ impl Iterator for PeekableChars<'_> { } } -/// Parses source code into a syntax tree using function definitions from a scope. +/// Parses source code into a syntax tree given a context. #[inline] -pub fn parse(src: &str, scope: &Scope) -> ParseResult { - Parser::new(src, scope).parse() +pub fn parse(src: &str, ctx: &ParseContext) -> ParseResult { + Parser::new(src, ctx).parse() +} + +/// The context for parsing. +#[derive(Debug)] +pub struct ParseContext<'a> { + /// The scope containing function definitions. + pub scope: &'a Scope, } /// Transforms token streams to syntax trees. struct Parser<'s> { src: &'s str, tokens: PeekableTokens<'s>, - scope: &'s Scope, state: ParserState, + ctx: &'s ParseContext<'s>, tree: SyntaxTree, } @@ -352,11 +359,11 @@ enum ParserState { impl<'s> Parser<'s> { /// Create a new parser from a stream of tokens and a scope of functions. - fn new(src: &'s str, scope: &'s Scope) -> Parser<'s> { + fn new(src: &'s str, ctx: &'s ParseContext) -> Parser<'s> { Parser { src, tokens: PeekableTokens::new(tokenize(src)), - scope, + ctx, state: ParserState::Body, tree: SyntaxTree::new(), } @@ -454,13 +461,9 @@ impl<'s> Parser<'s> { } // Now we want to parse this function dynamically. - let parser = self.scope.get_parser(&header.name) + let parser = self.ctx.scope.get_parser(&header.name) .ok_or_else(|| ParseError::new(format!("unknown function: '{}'", &header.name)))?; - let parse_context = ParseContext { - scope: &self.scope, - }; - // Do the parsing dependent on whether the function has a body. Ok(if has_body { // Find out the string which makes the body of this function. @@ -471,7 +474,7 @@ impl<'s> Parser<'s> { // Parse the body. let body_string = &self.src[start .. end]; - let body = parser(&header, Some(body_string), &parse_context)?; + let body = parser(&header, Some(body_string), self.ctx)?; // Skip to the end of the function in the token stream. self.tokens.goto(end); @@ -481,7 +484,7 @@ impl<'s> Parser<'s> { body } else { - parser(&header, None, &parse_context)? + parser(&header, None, self.ctx)? }) } @@ -627,13 +630,6 @@ impl<'s> Iterator for PeekableTokens<'s> { } } -/// The context for parsing a function. -#[derive(Debug)] -pub struct ParseContext<'s> { - /// The scope containing function definitions. - pub scope: &'s Scope, -} - /// Whether this word is a valid unicode identifier. fn is_identifier(string: &str) -> bool { let mut chars = string.chars(); @@ -801,7 +797,7 @@ mod token_tests { mod parse_tests { use super::*; use crate::func::{Function, Scope}; - use crate::engine::{TypesetContext, TypesetResult}; + use crate::layout::{LayoutContext, LayoutResult, Layout}; use Node::{Space as S, Newline as N, Func as F}; use funcs::*; @@ -818,13 +814,14 @@ mod parse_tests { fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext) -> ParseResult where Self: Sized { if let Some(src) = body { - parse(src, ctx.scope).map(|tree| TreeFn(tree)) + parse(src, ctx).map(|tree| TreeFn(tree)) } else { Err(ParseError::new("expected body for tree fn")) } } - fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) } + fn layout(&self, _: &LayoutContext) + -> LayoutResult<(Option, Option)> { Ok((None, None)) } } /// A testing function without a body. @@ -841,28 +838,33 @@ mod parse_tests { } } - fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) } + fn layout(&self, _: &LayoutContext) + -> LayoutResult<(Option, Option)> { Ok((None, None)) } } } /// Test if the source code parses into the syntax tree. fn test(src: &str, tree: SyntaxTree) { - assert_eq!(parse(src, &Scope::new()).unwrap(), tree); + let ctx = ParseContext { scope: &Scope::new() }; + assert_eq!(parse(src, &ctx).unwrap(), tree); } /// Test with a scope containing function definitions. fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) { - assert_eq!(parse(src, &scope).unwrap(), tree); + let ctx = ParseContext { scope }; + assert_eq!(parse(src, &ctx).unwrap(), tree); } /// Test if the source parses into the error. fn test_err(src: &str, err: &str) { - assert_eq!(parse(src, &Scope::new()).unwrap_err().to_string(), err); + let ctx = ParseContext { scope: &Scope::new() }; + assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err); } /// Test with a scope if the source parses into the error. fn test_err_scoped(scope: &Scope, src: &str, err: &str) { - assert_eq!(parse(src, &scope).unwrap_err().to_string(), err); + let ctx = ParseContext { scope }; + assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err); } /// Create a text node.