summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-03-14 19:54:38 +0100
committerLaurenz <laurmaedje@gmail.com>2019-03-14 19:54:38 +0100
commit83dd762f67c6513e4073387c5f287fd2ce9ae767 (patch)
tree691bb772fe6a49353f173282024e217513151a9b
parent22ea09d9c1fd342dcee13d153fedaf49a62db044 (diff)
Font providers 🚀+ better docs 📜
-rw-r--r--src/doc.rs22
-rw-r--r--src/engine.rs103
-rw-r--r--src/font.rs134
-rw-r--r--src/lib.rs111
-rw-r--r--src/pdf.rs2
5 files changed, 310 insertions, 62 deletions
diff --git a/src/doc.rs b/src/doc.rs
index a3bdfeb6..5d3c1e14 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<String>,
+ pub font_families: Vec<FontFamily>,
/// 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 5a6e27b0..1d249172 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<Font>,
@@ -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<Document> {
// 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<T> = std::result::Result<T, TypesetError>;
/// 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<io::Error> for TypesetError {
+ #[inline]
+ fn from(err: io::Error) -> TypesetError {
+ TypesetError::Io(err)
+ }
+}
+
+impl From<FontError> for TypesetError {
+ #[inline]
+ fn from(err: FontError) -> TypesetError {
+ TypesetError::Font(err)
+ }
+}
diff --git a/src/font.rs b/src/font.rs
index 40fa2102..df8f912f 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<T> TakeInvalid<T> for Option<T> {
}
}
+///////////////////////////////////////////////////////////////////////////////
+
+/// 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<Box<dyn FontSource>>;
+}
+
+/// 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<T> 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<FontFamily>,
+ /// If some, matches only italic/non-italic fonts, otherwise any.
+ pub italic: Option<bool>,
+ /// If some, matches only bold/non-bold fonts, otherwise any.
+ pub bold: Option<bool>,
+}
+
+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<FontFamily>) -> 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<FileFontDescriptor<'a>>,
+}
+
+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<P: 'a, I>(root: P, specs: I) -> FileFontProvider<'a>
+ where
+ I: IntoIterator<Item=FileFontDescriptor<'a>>,
+ P: Into<PathBuf>
+ {
+ 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<FontFamily>,
+ /// 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<Box<dyn FontSource>> {
+ 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<FontSource>
+ })
+ }
+}
+
type FontResult<T> = Result<T, FontError>;
/// The error type for font operations.
diff --git a/src/lib.rs b/src/lib.rs
index 6f2aaa53..59a6a279 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 {
+pub struct Compiler<'p> {
+ context: Context<'p>,
+}
+
+struct Context<'p> {
/// Style for typesetting.
style: Style,
+ /// Font providers.
+ font_providers: Vec<Box<dyn FontProvider + 'p>>,
}
-impl Compiler {
+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<P: 'p>(&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<Document, Error> {
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<PdfError> 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 b188ab91..ad193f18 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<T> = std::result::Result<T, PdfError>;
/// The error type for _PDF_ creation.