diff --git a/src/doc.rs b/src/doc.rs index a3bdfeb66..5d3c1e14b 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -30,30 +30,38 @@ pub struct Style { pub margin_bottom: Size, /// A fallback list of font families to use. - pub font_families: Vec, + 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, } +/// A family of fonts (either generic or named). +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FontFamily { + SansSerif, + Serif, + Monospace, + Named(String), +} + impl Default for Style { fn default() -> Style { + use FontFamily::*; Style { - // A4 paper + // A4 paper. width: Size::from_mm(210.0), height: Size::from_mm(297.0), - // A bit more on top and bottom + // Margins. A bit more on top and bottom. margin_left: Size::from_cm(2.5), margin_top: Size::from_cm(3.0), margin_right: Size::from_cm(2.5), margin_bottom: Size::from_cm(3.0), - // Default font family - font_families: (&[ - "NotoSans", "NotoSansMath" - ]).iter().map(ToString::to_string).collect(), + // Default font family, font size and line spacing. + font_families: vec![SansSerif, Serif, Monospace], font_size: 12.0, line_spacing: 1.25, } diff --git a/src/engine.rs b/src/engine.rs index 5a6e27b06..1d249172a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,18 +1,19 @@ //! Core typesetting engine. +use std::io; use std::error; use std::fmt; use crate::syntax::{SyntaxTree, Node}; -use crate::doc::{Document, Style, Size, Page, Text, TextCommand}; -use crate::font::Font; +use crate::doc::{Document, Size, Page, Text, TextCommand}; +use crate::font::{Font, FontConfig, FontError}; +use crate::Context; /// The core typesetting engine, transforming an abstract syntax tree into a document. -#[derive(Debug, Clone)] -pub struct Engine<'s> { +pub(crate) struct Engine<'a> { // Immutable - tree: &'s SyntaxTree<'s>, - style: Style, + tree: &'a SyntaxTree<'a>, + ctx: &'a Context<'a>, // Mutable fonts: Vec, @@ -22,12 +23,12 @@ pub struct Engine<'s> { current_width: Size, } -impl<'s> Engine<'s> { +impl<'a> Engine<'a> { /// Create a new generator from a syntax tree. - pub fn new(tree: &'s SyntaxTree<'s>, style: Style) -> Engine<'s> { + pub fn new(tree: &'a SyntaxTree<'a>, context: &'a Context<'a>) -> Engine<'a> { Engine { - style, tree, + ctx: context, fonts: Vec::new(), active_font: 0, text_commands: Vec::new(), @@ -39,21 +40,33 @@ impl<'s> Engine<'s> { /// Generate the abstract document. pub fn typeset(mut self) -> TypeResult { // Load font defined by style - let font_family = self.style.font_families.first().unwrap(); - let program = std::fs::read(format!("../fonts/{}-Regular.ttf", font_family)).unwrap(); - let font = Font::new(program).unwrap(); + let mut font = None; + let config = FontConfig::new(self.ctx.style.font_families.clone()); + for provider in &self.ctx.font_providers { + if let Some(mut source) = provider.provide(&config) { + 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.style.margin_left, - self.style.height - self.style.margin_top + 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.style.font_size)); + self.text_commands.push(TextCommand::SetFont(0, self.ctx.style.font_size)); // Iterate through the documents nodes. for node in &self.tree.nodes { @@ -70,8 +83,8 @@ impl<'s> Engine<'s> { // Create a page from the contents. let page = Page { - width: self.style.width, - height: self.style.height, + width: self.ctx.style.width, + height: self.ctx.style.height, text: vec![Text { commands: self.text_commands, }], @@ -88,8 +101,8 @@ impl<'s> Engine<'s> { let width = self.width(word); if self.would_overflow(width) { - let vertical_move = - self.style.font_size - * self.style.line_spacing + let vertical_move = - self.ctx.style.font_size + * self.ctx.style.line_spacing * font.metrics.ascender; self.text_commands.push(TextCommand::Move(Size::zero(), vertical_move)); @@ -115,31 +128,51 @@ impl<'s> Engine<'s> { 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.style.font_size) + .map(|c| font.widths[font.map(c) as usize] * self.ctx.style.font_size) .sum() } fn would_overflow(&self, width: Size) -> bool { - let max_width = self.style.width - - self.style.margin_left - - self.style.margin_right; + let max_width = self.ctx.style.width + - self.ctx.style.margin_left + - self.ctx.style.margin_right; self.current_width + width > max_width } } -/// Result type used for parsing. +/// Result type used for typesetting. type TypeResult = std::result::Result; /// The error type for typesetting. -pub enum TypesetError {} +pub enum TypesetError { + /// There was no suitable font. + MissingFont, + /// An error occured while gathering font data. + Font(FontError), + /// An I/O Error on occured while reading a font. + Io(io::Error), +} -impl error::Error for TypesetError {} +impl error::Error for TypesetError { + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + TypesetError::Font(err) => Some(err), + TypesetError::Io(err) => Some(err), + _ => None, + } + } +} impl fmt::Display for TypesetError { #[inline] - fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { - Ok(()) + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TypesetError::MissingFont => write!(f, "missing font"), + TypesetError::Font(err) => write!(f, "font error: {}", err), + TypesetError::Io(err) => write!(f, "io error: {}", err), + } } } @@ -149,3 +182,17 @@ impl fmt::Debug for TypesetError { fmt::Display::fmt(self, f) } } + +impl From for TypesetError { + #[inline] + fn from(err: io::Error) -> TypesetError { + TypesetError::Io(err) + } +} + +impl From for TypesetError { + #[inline] + fn from(err: FontError) -> TypesetError { + TypesetError::Font(err) + } +} diff --git a/src/font.rs b/src/font.rs index 40fa21026..df8f912fa 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,14 +1,17 @@ //! Font loading, utility and subsetting. +#![macro_use] + use std::collections::HashMap; use std::error; use std::fmt; -use std::io::{self, Cursor, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::io::{self, Cursor, Read, Seek, SeekFrom}; use byteorder::{BE, ReadBytesExt, WriteBytesExt}; use opentype::{Error as OpentypeError, OpenTypeReader, Outlines, TableRecord, Tag}; use opentype::tables::{Header, Name, CharMap, MaximumProfile, HorizontalMetrics, Post, OS2}; use opentype::tables::{MacStyleFlags, NameEntry}; -use crate::doc::Size; +use crate::doc::{Size, FontFamily}; /// An font wrapper which allows to subset a font. @@ -586,6 +589,133 @@ impl TakeInvalid for Option { } } +/////////////////////////////////////////////////////////////////////////////// + +/// A type that provides fonts matching given criteria. +pub trait FontProvider { + /// Returns a font matching the configuration + /// if this provider has a matching font. + fn provide(&self, config: &FontConfig) -> Option>; +} + +/// A wrapper trait around `Read + Seek` to allow for making a trait object. +/// +/// Automatically implemented for all types that are [`Read`] and [`Seek`]. +pub trait FontSource: Read + Seek {} +impl FontSource for T where T: Read + Seek {} + +/// Criteria to filter fonts. +#[derive(Debug, Clone, PartialEq)] +pub struct FontConfig { + /// The font families we are looking for. + pub families: Vec, + /// 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 FontConfig { + /// Create a new font config with the given families. + /// + /// All other fields are set to [`None`] and match anything. + pub fn new(families: Vec) -> FontConfig { + FontConfig { + 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 + } +} + +/// A font provider that works on font files on the local file system. +pub struct FileFontProvider<'a> { + root: PathBuf, + specs: Vec>, +} + +impl<'a> FileFontProvider<'a> { + /// Create a new file font provider. The folder relative to which the `specs` + /// contains the file paths, is given as `root`. + pub fn new(root: P, specs: I) -> FileFontProvider<'a> + where + I: IntoIterator>, + P: Into + { + FileFontProvider { + root: root.into(), + specs: specs.into_iter().collect() + } + } +} + +/// A type describing a font on the file system. +/// +/// Can be constructed conventiently with the [`file_font`] macro. +pub struct FileFontDescriptor<'a> { + /// The path to the font relative to the root. + pub path: &'a Path, + /// The font families this font is part of. + pub families: Vec, + /// Whether the font is in italics. + pub italic: bool, + /// Whether the font is bold. + pub bold: bool, +} + +impl FileFontDescriptor<'_> { + fn matches(&self, config: &FontConfig) -> bool { + config.italic.map(|i| i == self.italic).unwrap_or(true) + && config.bold.map(|i| i == self.bold).unwrap_or(true) + && config.families.iter().any(|family| self.families.contains(family)) + } +} + +/// Helper macro to create [file font descriptors](crate::font::FileFontDescriptor). +/// +/// ``` +/// # use typeset::file_font; +/// file_font!( +/// "NotoSans", // Font family name +/// [SansSerif], // Generic families +/// "NotoSans-Regular.ttf", // Font file +/// false, false // Bold & Italic +/// ); +/// ``` +#[macro_export] +macro_rules! file_font { + ($family:expr, [$($generic:ident),*], $path:expr, $bold:expr, $italic:expr) => {{ + let mut families = vec![$crate::doc::FontFamily::Named($family.to_string())]; + families.extend([$($crate::doc::FontFamily::$generic),*].iter().cloned()); + $crate::font::FileFontDescriptor { + path: std::path::Path::new($path), + families, + italic: $italic, bold: $bold, + } + }}; +} + +impl FontProvider for FileFontProvider<'_> { + fn provide(&self, config: &FontConfig) -> Option> { + self.specs.iter().find(|spec| spec.matches(&config)).map(|spec| { + let file = std::fs::File::open(self.root.join(spec.path)).unwrap(); + Box::new(file) as Box + }) + } +} + type FontResult = Result; /// The error type for font operations. diff --git a/src/lib.rs b/src/lib.rs index 6f2aaa538..59a6a2794 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,50 @@ -//! Typeset is a library for compiling documents written in the -//! corresponding typesetting language into a typesetted document in an -//! output format like _PDF_. +//! The compiler for the _Typeset_ typesetting language 📜. +//! +//! # Compilation +//! - **Parsing:** The parsing step first transforms a plain string into an +//! [iterator of tokens](Tokens). Then the parser operates on that to +//! construct an abstract syntax tree. The structures describing the tree +//! can be found in the [`syntax`](syntax) module. +//! - **Typesetting:** The next step is to transform the syntax tree into an +//! abstract document representation. Types for these can be found in the +//! [`doc`](doc) module. This representation contains already the finished +//! layout, but is still portable. +//! - **Exporting:** The abstract document can then be exported into supported +//! formats. Currently the only supported format is _PDF_. In this step +//! the text is finally encoded into glyph indices and font data is +//! subsetted. +//! +//! # Fonts +//! 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 a flexible font configuration +//! specifying font families and styles. A font provider is a type implementing the +//! [`FontProvider`](crate::font::FontProvider) trait. For convenience there exists +//! the [`FileFontProvider`](crate::font::FileFontProvider) to serve fonts from a +//! local folder. //! //! # Example -//! This is an example of compiling a really simple document into _PDF_. //! ``` -//! use typeset::Compiler; +//! use std::fs::File; +//! use typeset::{Compiler, font::FileFontProvider, file_font}; //! -//! // Minimal source code for our document. -//! let src = "Hello World from Typeset!"; +//! // Simple example source code. +//! let source = "Hello World from Typeset!"; //! -//! // Create an output file. +//! // Create a compiler with a font provider that provides one font. +//! let mut compiler = Compiler::new(); +//! compiler.add_font_provider(FileFontProvider::new("../fonts", vec![ +//! // Font family name, generic families, file, bold, italic +//! file_font!("NotoSans", [SansSerif], "NotoSans-Regular.ttf", false, false), +//! ])); +//! +//! // Open an output file, compile and write to the file. //! # /* -//! let mut file = std::fs::File::create("hello-typeset.pdf").unwrap(); +//! let mut file = File::create("hello-typeset.pdf").unwrap(); //! # */ -//! # let mut file = std::fs::File::create("../target/typeset-hello.pdf").unwrap(); -//! -//! // Create a compiler and write the document into a file as a PDF. -//! let compiler = Compiler::new(); -//! compiler.write_pdf(src, &mut file).unwrap(); +//! # let mut file = File::create("../target/typeset-hello.pdf").unwrap(); +//! compiler.write_pdf(source, &mut file).unwrap(); //! ``` pub mod syntax; @@ -36,33 +62,51 @@ pub use crate::pdf::PdfError; use std::error; use std::fmt; use std::io::Write; -use crate::parsing::Parser; use crate::syntax::SyntaxTree; -use crate::engine::Engine; +use crate::parsing::Parser; use crate::doc::{Document, Style}; +use crate::font::FontProvider; +use crate::engine::Engine; use crate::pdf::PdfCreator; /// Compiles source code into typesetted documents allowing to /// retrieve results at various stages. -pub struct Compiler { - /// Style for typesetting. - style: Style, +pub struct Compiler<'p> { + context: Context<'p>, } -impl Compiler { +struct Context<'p> { + /// Style for typesetting. + style: Style, + /// Font providers. + font_providers: Vec>, +} + +impl<'p> Compiler<'p> { /// Create a new compiler from a document. #[inline] - pub fn new() -> Compiler { + pub fn new() -> Compiler<'p> { Compiler { - style: Style::default(), + context: Context { + style: Style::default(), + font_providers: Vec::new(), + } } } /// Set the default style for typesetting. #[inline] pub fn style(&mut self, style: Style) -> &mut Self { - self.style = style; + self.context.style = style; + self + } + + /// Add a font provider. + #[inline] + pub fn add_font_provider(&mut self, provider: P) -> &mut Self + where P: FontProvider { + self.context.font_providers.push(Box::new(provider)); self } @@ -82,7 +126,7 @@ impl Compiler { #[inline] pub fn typeset(&self, source: &str) -> Result { let tree = self.parse(source)?; - Engine::new(&tree, self.style.clone()).typeset().map_err(Into::into) + Engine::new(&tree, &self.context).typeset().map_err(Into::into) } /// Write the document as a _PDF_, returning how many bytes were written. @@ -156,13 +200,32 @@ impl From for Error { #[cfg(test)] mod test { + use std::fs::File; use crate::Compiler; + use crate::font::FileFontProvider; /// Create a pdf with a name from the source code. fn test(name: &str, src: &str) { + // Create compiler + let mut compiler = Compiler::new(); + let provider = FileFontProvider::new("../fonts", vec![ + // Font family name, generic families, file, bold, italic + file_font!("NotoSans", [SansSerif], "NotoSans-Regular.ttf", false, false), + file_font!("NotoSans", [SansSerif], "NotoSans-Bold.ttf", true, false), + file_font!("NotoSans", [SansSerif], "NotoSans-Italic.ttf", false, true), + file_font!("NotoSans", [SansSerif], "NotoSans-BoldItalic.ttf", true, true), + file_font!("NotoSansMath", [SansSerif], "NotoSansMath-Regular.ttf", false, false), + file_font!("NotoEmoji", [SansSerif, Serif, Monospace], + "NotoEmoji-Regular.ttf", false, false), + ]); + compiler.add_font_provider(provider); + + // Open output file; let path = format!("../target/typeset-pdf-{}.pdf", name); - let mut file = std::fs::File::create(path).unwrap(); - Compiler::new().write_pdf(src, &mut file).unwrap(); + let mut file = File::create(path).unwrap(); + + // Compile and output + compiler.write_pdf(src, &mut file).unwrap(); } #[test] diff --git a/src/pdf.rs b/src/pdf.rs index b188ab91c..ad193f185 100644 --- a/src/pdf.rs +++ b/src/pdf.rs @@ -278,7 +278,7 @@ impl std::ops::Deref for PdfFont { } } -/// Result type used for parsing. +/// Result type for _PDF_ creation. type PdfResult = std::result::Result; /// The error type for _PDF_ creation.