Reuse font loader across compilations 🔋

This commit is contained in:
Laurenz Mädje 2019-07-28 22:27:09 +02:00
parent 51faad45ad
commit b96a7e0cf3
9 changed files with 106 additions and 96 deletions

View File

@ -1,19 +1,48 @@
use bencher::Bencher; use bencher::Bencher;
use typeset::Typesetter; use typeset::Typesetter;
use typeset::font::FileSystemFontProvider; use typeset::font::FileSystemFontProvider;
use typeset::export::pdf::PdfExporter;
fn typesetting(b: &mut Bencher) { fn prepare<'p>() -> (Typesetter<'p>, &'static str) {
let src = include_str!("../test/shakespeare.tps"); let src = include_str!("../test/shakespeare.tps");
let mut typesetter = Typesetter::new(); let mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap(); let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
typesetter.add_font_provider(provider); typesetter.add_font_provider(provider);
(typesetter, src)
}
/// Benchmarks only the parsing step.
fn parsing(b: &mut Bencher) {
let (typesetter, src) = prepare();
b.iter(|| { typesetter.parse(src).unwrap(); });
}
/// Benchmarks only the layouting step.
fn layouting(b: &mut Bencher) {
let (typesetter, src) = prepare();
let tree = typesetter.parse(src).unwrap();
b.iter(|| { typesetter.layout(&tree).unwrap(); });
}
/// Benchmarks the full typesetting step.
fn typesetting(b: &mut Bencher) {
let (typesetter, src) = prepare();
b.iter(|| { typesetter.typeset(src).unwrap(); });
}
/// Benchmarks only the exporting step.
fn exporting(b: &mut Bencher) {
let (typesetter, src) = prepare();
let doc = typesetter.typeset(src).unwrap();
let exporter = PdfExporter::new();
b.iter(|| { b.iter(|| {
let _document = typesetter.typeset(&src).unwrap(); let mut buf = Vec::new();
exporter.export(&doc, &typesetter.loader(), &mut buf).unwrap();
}); });
} }
bencher::benchmark_group!(benches, typesetting); bencher::benchmark_group!(benches, parsing, layouting, typesetting, exporting);
bencher::benchmark_main!(benches); bencher::benchmark_main!(benches);

View File

@ -3,10 +3,11 @@ use typeset::font::{*, FontClass::*};
use typeset::style::TextStyle; use typeset::style::TextStyle;
/// Benchmarks just the char-by-char font loading.
fn font_loading(b: &mut Bencher) { fn font_loading(b: &mut Bencher) {
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap(); let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
let providers = vec![Box::new(provider) as Box<dyn FontProvider>]; let mut font_loader = FontLoader::new();
let font_loader = FontLoader::new(&providers); font_loader.add_font_provider(provider);
let text = include_str!("../test/shakespeare.tps"); let text = include_str!("../test/shakespeare.tps");
@ -28,7 +29,7 @@ fn font_loading(b: &mut Bencher) {
match character { match character {
'_' => style.toggle_class(Italic), '_' => style.toggle_class(Italic),
'*' => style.toggle_class(Bold), '*' => style.toggle_class(Bold),
'\n' => {}, '\n' | '[' | ']' => {},
_ => { _ => {
let _font = font_loader.get(FontQuery { let _font = font_loader.get(FontQuery {
character, character,

View File

@ -54,7 +54,7 @@ fn run() -> Result<(), Box<dyn Error>> {
// Export the document into a PDF file. // Export the document into a PDF file.
let exporter = PdfExporter::new(); let exporter = PdfExporter::new();
let dest_file = File::create(&dest_path)?; let dest_file = File::create(&dest_path)?;
exporter.export(&document, BufWriter::new(dest_file))?; exporter.export(&document, typesetter.loader(), BufWriter::new(dest_file))?;
Ok(()) Ok(())
} }

View File

@ -1,6 +1,5 @@
//! Representation of typesetted documents. //! Representation of typesetted documents.
use crate::font::Font;
use crate::size::{Size, Size2D}; use crate::size::{Size, Size2D};
@ -9,8 +8,6 @@ use crate::size::{Size, Size2D};
pub struct Document { pub struct Document {
/// The pages of the document. /// The pages of the document.
pub pages: Vec<Page>, pub pages: Vec<Page>,
/// The fonts used (the page contents index into this).
pub fonts: Vec<Font>,
} }
/// A page of a document. /// A page of a document.

View File

@ -1,6 +1,6 @@
//! Exporting into _PDF_ documents. //! Exporting into _PDF_ documents.
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::io::{self, Write}; use std::io::{self, Write};
use pdf::{PdfWriter, Ref, Rect, Version, Trailer, Content}; use pdf::{PdfWriter, Ref, Rect, Version, Trailer, Content};
@ -9,7 +9,7 @@ use pdf::font::{Type0Font, CIDFont, CIDFontType, CIDSystemInfo, FontDescriptor,
use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream}; use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream};
use crate::doc::{Document, Page as DocPage, LayoutAction}; use crate::doc::{Document, Page as DocPage, LayoutAction};
use crate::font::{Font, FontError}; use crate::font::{Font, FontLoader, FontError};
use crate::size::{Size, Size2D}; use crate::size::{Size, Size2D};
@ -26,8 +26,9 @@ impl PdfExporter {
/// Export a typesetted document into a writer. Returns how many bytes were written. /// Export a typesetted document into a writer. Returns how many bytes were written.
#[inline] #[inline]
pub fn export<W: Write>(&self, document: &Document, target: W) -> PdfResult<usize> { pub fn export<W: Write>(&self, document: &Document, loader: &FontLoader, target: W)
let mut engine = PdfEngine::new(document, target)?; -> PdfResult<usize> {
let mut engine = PdfEngine::new(document, loader, target)?;
engine.write() engine.write()
} }
} }
@ -38,6 +39,7 @@ struct PdfEngine<'d, W: Write> {
writer: PdfWriter<W>, writer: PdfWriter<W>,
doc: &'d Document, doc: &'d Document,
offsets: Offsets, offsets: Offsets,
font_remap: HashMap<usize, usize>,
fonts: Vec<PdfFont>, fonts: Vec<PdfFont>,
} }
@ -53,41 +55,53 @@ struct Offsets {
impl<'d, W: Write> PdfEngine<'d, W> { impl<'d, W: Write> PdfEngine<'d, W> {
/// Create a new _PDF_ engine. /// Create a new _PDF_ engine.
fn new(doc: &'d Document, target: W) -> PdfResult<PdfEngine<'d, W>> { fn new(doc: &'d Document, loader: &FontLoader, target: W) -> PdfResult<PdfEngine<'d, W>> {
// Calculate a unique id for all objects that will be written.
let catalog = 1;
let page_tree = catalog + 1;
let pages = (page_tree + 1, page_tree + doc.pages.len() as Ref);
let contents = (pages.1 + 1, pages.1 + doc.pages.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. // Create a subsetted PDF font for each font in the document.
let mut font_remap = HashMap::new();
let fonts = { let fonts = {
let mut font = 0usize; let mut font = 0usize;
let mut chars = vec![HashSet::new(); doc.fonts.len()]; let mut chars = HashMap::new();
// Find out which characters are used for each font. // Find out which characters are used for each font.
for page in &doc.pages { for page in &doc.pages {
for action in &page.actions { for action in &page.actions {
match action { match action {
LayoutAction::WriteText(string) => chars[font].extend(string.chars()), LayoutAction::WriteText(string) => {
LayoutAction::SetFont(id, _) => font = *id, chars.entry(font)
.or_insert_with(HashSet::new)
.extend(string.chars())
},
LayoutAction::SetFont(id, _) => {
font = *id;
let new_id = font_remap.len();
font_remap.entry(font).or_insert(new_id);
},
_ => {}, _ => {},
} }
} }
} }
doc.fonts.iter() // Collect the fonts into a vector in the order of the values in the remapping.
.enumerate() let mut order = font_remap.iter().map(|(&old, &new)| (old, new)).collect::<Vec<_>>();
.map(|(i, font)| PdfFont::new(font, &chars[i])) order.sort_by_key(|&(_, new)| new);
order.into_iter()
.map(|(old, _)| PdfFont::new(&loader.get_with_index(old), &chars[&old]))
.collect::<PdfResult<Vec<_>>>()? .collect::<PdfResult<Vec<_>>>()?
}; };
// Calculate a unique id for all objects that will be written.
let catalog = 1;
let page_tree = catalog + 1;
let pages = (page_tree + 1, page_tree + doc.pages.len() as Ref);
let contents = (pages.1 + 1, pages.1 + doc.pages.len() as Ref);
let font_offsets = (contents.1 + 1, contents.1 + 5 * fonts.len() as Ref);
let offsets = Offsets { catalog, page_tree, pages, contents, fonts: font_offsets };
Ok(PdfEngine { Ok(PdfEngine {
writer: PdfWriter::new(target), writer: PdfWriter::new(target),
doc, doc,
offsets, offsets,
font_remap,
fonts, fonts,
}) })
} }
@ -151,7 +165,7 @@ impl<'d, W: Write> PdfEngine<'d, W> {
for action in &page.actions { for action in &page.actions {
match action { match action {
LayoutAction::MoveAbsolute(pos) => next_pos = Some(*pos), LayoutAction::MoveAbsolute(pos) => next_pos = Some(*pos),
LayoutAction::SetFont(id, size) => next_font = Some((*id, *size)), LayoutAction::SetFont(id, size) => next_font = Some((self.font_remap[id], *size)),
LayoutAction::WriteText(string) => { LayoutAction::WriteText(string) => {
// Flush the font if it is different from the current. // Flush the font if it is different from the current.
if let Some((id, size)) = next_font { if let Some((id, size)) = next_font {

View File

@ -10,16 +10,14 @@ use super::{Font, FontInfo, FontClass, FontProvider};
/// Serves fonts matching queries. /// Serves fonts matching queries.
pub struct FontLoader<'p> { pub struct FontLoader<'p> {
/// The font providers. /// The font providers.
providers: Vec<&'p (dyn FontProvider + 'p)>, providers: Vec<Box<dyn FontProvider + 'p>>,
/// The fonts available from each provider (indexed like `providers`).
infos: Vec<&'p [FontInfo]>,
/// The internal state. Uses interior mutability because the loader works behind /// The internal state. Uses interior mutability because the loader works behind
/// an immutable reference to ease usage. /// an immutable reference to ease usage.
state: RefCell<FontLoaderState<'p>>, state: RefCell<FontLoaderState>,
} }
/// Internal state of the font loader (seperated to wrap it in a `RefCell`). /// Internal state of the font loader (seperated to wrap it in a `RefCell`).
struct FontLoaderState<'p> { struct FontLoaderState {
/// The loaded fonts alongside their external indices. Some fonts may not /// The loaded fonts alongside their external indices. Some fonts may not
/// have external indices because they were loaded but did not contain the /// have external indices because they were loaded but did not contain the
/// required character. However, these are still stored because they may /// required character. However, these are still stored because they may
@ -28,21 +26,17 @@ struct FontLoaderState<'p> {
/// Allows to retrieve a font (index) quickly if a query was submitted before. /// Allows to retrieve a font (index) quickly if a query was submitted before.
query_cache: HashMap<FontQuery, usize>, query_cache: HashMap<FontQuery, usize>,
/// Allows to re-retrieve loaded fonts by their info instead of loading them again. /// Allows to re-retrieve loaded fonts by their info instead of loading them again.
info_cache: HashMap<&'p FontInfo, usize>, info_cache: HashMap<FontInfo, usize>,
/// Indexed by external indices (the ones inside the tuples in the `fonts` vector) /// Indexed by external indices (the ones inside the tuples in the `fonts` vector)
/// and maps to internal indices (the actual indices into the vector). /// and maps to internal indices (the actual indices into the vector).
inner_index: Vec<usize>, inner_index: Vec<usize>,
} }
impl<'p> FontLoader<'p> { impl<'p> FontLoader<'p> {
/// Create a new font loader using a set of providers. /// Create a new font loader.
pub fn new<P: 'p>(providers: &'p [P]) -> FontLoader<'p> where P: AsRef<dyn FontProvider + 'p> { pub fn new() -> FontLoader<'p> {
let providers: Vec<_> = providers.iter().map(|p| p.as_ref()).collect();
let infos = providers.iter().map(|prov| prov.available()).collect();
FontLoader { FontLoader {
providers, providers: vec![],
infos,
state: RefCell::new(FontLoaderState { state: RefCell::new(FontLoaderState {
query_cache: HashMap::new(), query_cache: HashMap::new(),
info_cache: HashMap::new(), info_cache: HashMap::new(),
@ -52,6 +46,11 @@ impl<'p> FontLoader<'p> {
} }
} }
/// Add a font provider to this loader.
pub fn add_font_provider<P: FontProvider + 'p>(&mut self, provider: P) {
self.providers.push(Box::new(provider));
}
/// Returns the font (and its index) best matching the query, if there is any. /// Returns the font (and its index) best matching the query, if there is any.
pub fn get(&self, query: FontQuery) -> Option<(usize, Ref<Font>)> { pub fn get(&self, query: FontQuery) -> Option<(usize, Ref<Font>)> {
// Load results from the cache, if we had the exact same query before. // Load results from the cache, if we had the exact same query before.
@ -70,8 +69,8 @@ impl<'p> FontLoader<'p> {
// font that matches the first possible class. // font that matches the first possible class.
for class in &query.fallback { for class in &query.fallback {
// For each class now go over all fonts from all font providers. // For each class now go over all fonts from all font providers.
for (provider, infos) in self.providers.iter().zip(&self.infos) { for provider in &self.providers {
for info in infos.iter() { for info in provider.available().iter() {
let viable = info.classes.contains(class); let viable = info.classes.contains(class);
let matches = viable && query.classes.iter() let matches = viable && query.classes.iter()
.all(|class| info.classes.contains(class)); .all(|class| info.classes.contains(class));
@ -90,7 +89,7 @@ impl<'p> FontLoader<'p> {
// Insert it into the storage and cache it by its info. // Insert it into the storage and cache it by its info.
let index = state.fonts.len(); let index = state.fonts.len();
state.info_cache.insert(info, index); state.info_cache.insert(info.clone(), index);
state.fonts.push((None, font)); state.fonts.push((None, font));
index index
@ -139,23 +138,6 @@ impl<'p> FontLoader<'p> {
let internal = state.inner_index[index]; let internal = state.inner_index[index];
Ref::map(state, |s| &s.fonts[internal].1) Ref::map(state, |s| &s.fonts[internal].1)
} }
/// Move the whole list of fonts out.
pub fn into_fonts(self) -> Vec<Font> {
// Sort the fonts by external index so that they are in the correct order.
// All fonts that were cached but not used by the outside are sorted to the back
// and are removed in the next step.
let mut fonts = self.state.into_inner().fonts;
fonts.sort_by_key(|&(maybe_index, _)| match maybe_index {
Some(index) => index,
None => std::usize::MAX,
});
// Remove the fonts that are not used from the outside.
fonts.into_iter().filter_map(|(maybe_index, font)| {
if maybe_index.is_some() { Some(font) } else { None }
}).collect()
}
} }
impl Debug for FontLoader<'_> { impl Debug for FontLoader<'_> {
@ -163,7 +145,6 @@ impl Debug for FontLoader<'_> {
let state = self.state.borrow(); let state = self.state.borrow();
f.debug_struct("FontLoader") f.debug_struct("FontLoader")
.field("providers", &self.providers.len()) .field("providers", &self.providers.len())
.field("infos", &self.infos)
.field("fonts", &state.fonts) .field("fonts", &state.fonts)
.field("query_cache", &state.query_cache) .field("query_cache", &state.query_cache)
.field("info_cache", &state.info_cache) .field("info_cache", &state.info_cache)

View File

@ -1,7 +1,6 @@
//! Block-style layouting of boxes. //! Block-style layouting of boxes.
use crate::doc::{Document, Page, LayoutAction}; use crate::doc::{Document, Page, LayoutAction};
use crate::font::Font;
use crate::size::{Size, Size2D}; use crate::size::{Size, Size2D};
use super::{ActionList, LayoutSpace, LayoutResult, LayoutError}; use super::{ActionList, LayoutSpace, LayoutResult, LayoutError};
@ -16,15 +15,14 @@ pub struct BoxLayout {
} }
impl BoxLayout { impl BoxLayout {
/// Convert this layout into a document given the list of fonts referenced by it. /// Convert this layout into a document.
pub fn into_doc(self, fonts: Vec<Font>) -> Document { pub fn into_doc(self) -> Document {
Document { Document {
pages: vec![Page { pages: vec![Page {
width: self.dimensions.x, width: self.dimensions.x,
height: self.dimensions.y, height: self.dimensions.y,
actions: self.actions, actions: self.actions,
}], }],
fonts,
} }
} }
} }

View File

@ -38,13 +38,11 @@
//! # */ //! # */
//! # let file = File::create("../target/typeset-doc-hello.pdf").unwrap(); //! # let file = File::create("../target/typeset-doc-hello.pdf").unwrap();
//! let exporter = PdfExporter::new(); //! let exporter = PdfExporter::new();
//! exporter.export(&document, file).unwrap(); //! exporter.export(&document, typesetter.loader(), file).unwrap();
//! ``` //! ```
use std::fmt::{self, Debug, Formatter};
use crate::doc::Document; use crate::doc::Document;
use crate::font::{Font, FontLoader, FontProvider}; use crate::font::{FontLoader, FontProvider};
use crate::func::Scope; use crate::func::Scope;
use crate::parsing::{parse, ParseContext, ParseResult, ParseError}; use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult}; use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult};
@ -69,9 +67,10 @@ pub mod syntax;
/// Transforms source code into typesetted documents. /// Transforms source code into typesetted documents.
/// ///
/// Can be configured through various methods. /// Can be configured through various methods.
#[derive(Debug)]
pub struct Typesetter<'p> { pub struct Typesetter<'p> {
/// Font providers. /// The font loader shared by all typesetting processes.
font_providers: Vec<Box<dyn FontProvider + 'p>>, loader: FontLoader<'p>,
/// The default text style. /// The default text style.
text_style: TextStyle, text_style: TextStyle,
/// The default page style. /// The default page style.
@ -83,9 +82,9 @@ impl<'p> Typesetter<'p> {
#[inline] #[inline]
pub fn new() -> Typesetter<'p> { pub fn new() -> Typesetter<'p> {
Typesetter { Typesetter {
loader: FontLoader::new(),
text_style: TextStyle::default(), text_style: TextStyle::default(),
page_style: PageStyle::default(), page_style: PageStyle::default(),
font_providers: vec![],
} }
} }
@ -104,21 +103,19 @@ impl<'p> Typesetter<'p> {
/// Add a font provider to the context of this typesetter. /// Add a font provider to the context of this typesetter.
#[inline] #[inline]
pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider { pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider {
self.font_providers.push(Box::new(provider)); self.loader.add_font_provider(provider);
} }
/// Parse source code into a syntax tree. /// Parse source code into a syntax tree.
#[inline]
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> { pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
let scope = Scope::with_std(); let scope = Scope::with_std();
parse(src, ParseContext { scope: &scope }) parse(src, ParseContext { scope: &scope })
} }
/// Layout a syntax tree and return the layout and the referenced font list. /// Layout a syntax tree and return the layout and the referenced font list.
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(BoxLayout, Vec<Font>)> { pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<BoxLayout> {
let loader = FontLoader::new(&self.font_providers);
let pages = layout(&tree, LayoutContext { let pages = layout(&tree, LayoutContext {
loader: &loader, loader: &self.loader,
style: &self.text_style, style: &self.text_style,
space: LayoutSpace { space: LayoutSpace {
dimensions: self.page_style.dimensions, dimensions: self.page_style.dimensions,
@ -126,28 +123,23 @@ impl<'p> Typesetter<'p> {
shrink_to_fit: false, shrink_to_fit: false,
}, },
})?; })?;
Ok((pages, loader.into_fonts())) Ok(pages)
} }
/// Typeset a portable document from source code. /// Typeset a portable document from source code.
#[inline]
pub fn typeset(&self, src: &str) -> Result<Document, TypesetError> { pub fn typeset(&self, src: &str) -> Result<Document, TypesetError> {
let tree = self.parse(src)?; let tree = self.parse(src)?;
let (layout, fonts) = self.layout(&tree)?; let layout = self.layout(&tree)?;
let document = layout.into_doc(fonts); let document = layout.into_doc();
Ok(document) Ok(document)
} }
/// A reference to the backing font loader.
pub fn loader(&self) -> &FontLoader<'p> {
&self.loader
}
} }
impl Debug for Typesetter<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Typesetter")
.field("page_style", &self.page_style)
.field("text_style", &self.text_style)
.field("font_providers", &self.font_providers.len())
.finish()
}
}
/// The general error type for typesetting. /// The general error type for typesetting.
pub enum TypesetError { pub enum TypesetError {
@ -193,7 +185,7 @@ mod test {
let path = format!("../target/typeset-unit-{}.pdf", name); let path = format!("../target/typeset-unit-{}.pdf", name);
let file = BufWriter::new(File::create(path).unwrap()); let file = BufWriter::new(File::create(path).unwrap());
let exporter = PdfExporter::new(); let exporter = PdfExporter::new();
exporter.export(&document, file).unwrap(); exporter.export(&document, typesetter.loader(), file).unwrap();
} }
#[test] #[test]

View File

@ -1,4 +1,3 @@
// -------------------------------------------------------------------------- //
[bold][Scene 5: _The Tower of London_] [bold][Scene 5: _The Tower of London_]
[italic][Enter Mortimer, brought in a chair, and Gaolers.] [italic][Enter Mortimer, brought in a chair, and Gaolers.]
@ -39,7 +38,6 @@
That so he might recover what was lost. That so he might recover what was lost.
// -------------------------------------------------------------------------- //
[italic][Enter Richard Plantagenet] [italic][Enter Richard Plantagenet]
*First Keeper.* My lord, your loving nephew now is come. *First Keeper.* My lord, your loving nephew now is come.