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 typeset::Typesetter;
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 mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("../fonts/fonts.toml").unwrap();
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(|| {
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);

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
//! Exporting into _PDF_ documents.
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
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 crate::doc::{Document, Page as DocPage, LayoutAction};
use crate::font::{Font, FontError};
use crate::font::{Font, FontLoader, FontError};
use crate::size::{Size, Size2D};
@ -26,8 +26,9 @@ impl PdfExporter {
/// Export a typesetted document into a writer. Returns how many bytes were written.
#[inline]
pub fn export<W: Write>(&self, document: &Document, target: W) -> PdfResult<usize> {
let mut engine = PdfEngine::new(document, target)?;
pub fn export<W: Write>(&self, document: &Document, loader: &FontLoader, target: W)
-> PdfResult<usize> {
let mut engine = PdfEngine::new(document, loader, target)?;
engine.write()
}
}
@ -38,6 +39,7 @@ struct PdfEngine<'d, W: Write> {
writer: PdfWriter<W>,
doc: &'d Document,
offsets: Offsets,
font_remap: HashMap<usize, usize>,
fonts: Vec<PdfFont>,
}
@ -53,41 +55,53 @@ struct Offsets {
impl<'d, W: Write> PdfEngine<'d, W> {
/// Create a new _PDF_ engine.
fn new(doc: &'d Document, 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 };
fn new(doc: &'d Document, loader: &FontLoader, target: W) -> PdfResult<PdfEngine<'d, W>> {
// Create a subsetted PDF font for each font in the document.
let mut font_remap = HashMap::new();
let fonts = {
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.
for page in &doc.pages {
for action in &page.actions {
match action {
LayoutAction::WriteText(string) => chars[font].extend(string.chars()),
LayoutAction::SetFont(id, _) => font = *id,
LayoutAction::WriteText(string) => {
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()
.enumerate()
.map(|(i, font)| PdfFont::new(font, &chars[i]))
// Collect the fonts into a vector in the order of the values in the remapping.
let mut order = font_remap.iter().map(|(&old, &new)| (old, new)).collect::<Vec<_>>();
order.sort_by_key(|&(_, new)| new);
order.into_iter()
.map(|(old, _)| PdfFont::new(&loader.get_with_index(old), &chars[&old]))
.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 {
writer: PdfWriter::new(target),
doc,
offsets,
font_remap,
fonts,
})
}
@ -151,7 +165,7 @@ impl<'d, W: Write> PdfEngine<'d, W> {
for action in &page.actions {
match action {
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) => {
// Flush the font if it is different from the current.
if let Some((id, size)) = next_font {

View File

@ -10,16 +10,14 @@ use super::{Font, FontInfo, FontClass, FontProvider};
/// Serves fonts matching queries.
pub struct FontLoader<'p> {
/// The font providers.
providers: Vec<&'p (dyn FontProvider + 'p)>,
/// The fonts available from each provider (indexed like `providers`).
infos: Vec<&'p [FontInfo]>,
providers: Vec<Box<dyn FontProvider + 'p>>,
/// The internal state. Uses interior mutability because the loader works behind
/// 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`).
struct FontLoaderState<'p> {
struct FontLoaderState {
/// The loaded fonts alongside their external indices. Some fonts may not
/// have external indices because they were loaded but did not contain the
/// 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.
query_cache: HashMap<FontQuery, usize>,
/// 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)
/// and maps to internal indices (the actual indices into the vector).
inner_index: Vec<usize>,
}
impl<'p> FontLoader<'p> {
/// Create a new font loader using a set of providers.
pub fn new<P: 'p>(providers: &'p [P]) -> FontLoader<'p> where P: AsRef<dyn FontProvider + 'p> {
let providers: Vec<_> = providers.iter().map(|p| p.as_ref()).collect();
let infos = providers.iter().map(|prov| prov.available()).collect();
/// Create a new font loader.
pub fn new() -> FontLoader<'p> {
FontLoader {
providers,
infos,
providers: vec![],
state: RefCell::new(FontLoaderState {
query_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.
pub fn get(&self, query: FontQuery) -> Option<(usize, Ref<Font>)> {
// 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.
for class in &query.fallback {
// For each class now go over all fonts from all font providers.
for (provider, infos) in self.providers.iter().zip(&self.infos) {
for info in infos.iter() {
for provider in &self.providers {
for info in provider.available().iter() {
let viable = info.classes.contains(class);
let matches = viable && query.classes.iter()
.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.
let index = state.fonts.len();
state.info_cache.insert(info, index);
state.info_cache.insert(info.clone(), index);
state.fonts.push((None, font));
index
@ -139,23 +138,6 @@ impl<'p> FontLoader<'p> {
let internal = state.inner_index[index];
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<'_> {
@ -163,7 +145,6 @@ impl Debug for FontLoader<'_> {
let state = self.state.borrow();
f.debug_struct("FontLoader")
.field("providers", &self.providers.len())
.field("infos", &self.infos)
.field("fonts", &state.fonts)
.field("query_cache", &state.query_cache)
.field("info_cache", &state.info_cache)

View File

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

View File

@ -38,13 +38,11 @@
//! # */
//! # let file = File::create("../target/typeset-doc-hello.pdf").unwrap();
//! 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::font::{Font, FontLoader, FontProvider};
use crate::font::{FontLoader, FontProvider};
use crate::func::Scope;
use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
use crate::layout::{layout, LayoutContext, LayoutSpace, LayoutError, LayoutResult};
@ -69,9 +67,10 @@ pub mod syntax;
/// Transforms source code into typesetted documents.
///
/// Can be configured through various methods.
#[derive(Debug)]
pub struct Typesetter<'p> {
/// Font providers.
font_providers: Vec<Box<dyn FontProvider + 'p>>,
/// The font loader shared by all typesetting processes.
loader: FontLoader<'p>,
/// The default text style.
text_style: TextStyle,
/// The default page style.
@ -83,9 +82,9 @@ impl<'p> Typesetter<'p> {
#[inline]
pub fn new() -> Typesetter<'p> {
Typesetter {
loader: FontLoader::new(),
text_style: TextStyle::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.
#[inline]
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.
#[inline]
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
let scope = Scope::with_std();
parse(src, ParseContext { scope: &scope })
}
/// Layout a syntax tree and return the layout and the referenced font list.
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(BoxLayout, Vec<Font>)> {
let loader = FontLoader::new(&self.font_providers);
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<BoxLayout> {
let pages = layout(&tree, LayoutContext {
loader: &loader,
loader: &self.loader,
style: &self.text_style,
space: LayoutSpace {
dimensions: self.page_style.dimensions,
@ -126,28 +123,23 @@ impl<'p> Typesetter<'p> {
shrink_to_fit: false,
},
})?;
Ok((pages, loader.into_fonts()))
Ok(pages)
}
/// Typeset a portable document from source code.
#[inline]
pub fn typeset(&self, src: &str) -> Result<Document, TypesetError> {
let tree = self.parse(src)?;
let (layout, fonts) = self.layout(&tree)?;
let document = layout.into_doc(fonts);
let layout = self.layout(&tree)?;
let document = layout.into_doc();
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.
pub enum TypesetError {
@ -193,7 +185,7 @@ mod test {
let path = format!("../target/typeset-unit-{}.pdf", name);
let file = BufWriter::new(File::create(path).unwrap());
let exporter = PdfExporter::new();
exporter.export(&document, file).unwrap();
exporter.export(&document, typesetter.loader(), file).unwrap();
}
#[test]

View File

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