Reuse font loader across compilations 🔋
This commit is contained in:
parent
51faad45ad
commit
b96a7e0cf3
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
src/lib.rs
44
src/lib.rs
@ -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]
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user