From 0bfee5b7772338fd39bbf708d3e31ea7bcec859b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 28 May 2021 12:44:44 +0200 Subject: Refactored loading and cache architecture --- src/cache.rs | 43 ++-- src/env/fs.rs | 215 ------------------ src/env/image.rs | 47 ---- src/env/mod.rs | 243 --------------------- src/eval/mod.rs | 55 +++-- src/eval/value.rs | 14 +- src/exec/context.rs | 10 +- src/exec/mod.rs | 19 +- src/export/mod.rs | 5 + src/export/pdf.rs | 586 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/font.rs | 126 ++++++++++- src/image.rs | 127 +++++++++++ src/layout/frame.rs | 3 +- src/layout/mod.rs | 47 +++- src/layout/shaping.rs | 25 ++- src/lib.rs | 31 +-- src/library/image.rs | 25 ++- src/loading/fs.rs | 215 ++++++++++++++++++ src/loading/mod.rs | 43 ++++ src/main.rs | 52 ++--- src/pdf/mod.rs | 578 ------------------------------------------------- src/pretty.rs | 8 - 22 files changed, 1277 insertions(+), 1240 deletions(-) delete mode 100644 src/env/fs.rs delete mode 100644 src/env/image.rs delete mode 100644 src/env/mod.rs create mode 100644 src/export/mod.rs create mode 100644 src/export/pdf.rs create mode 100644 src/image.rs create mode 100644 src/loading/fs.rs create mode 100644 src/loading/mod.rs delete mode 100644 src/pdf/mod.rs (limited to 'src') diff --git a/src/cache.rs b/src/cache.rs index 4cf97ba6..aa9c10a0 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,34 +1,27 @@ -//! Caching for incremental compilation. +//! Caching of compilation artifacts. -use std::collections::HashMap; +use crate::font::FontCache; +use crate::image::ImageCache; +use crate::layout::LayoutCache; +use crate::loading::Loader; -use crate::layout::{Frame, Regions}; - -/// A cache for incremental compilation. -#[derive(Default, Debug, Clone)] +/// Caches compilation artifacts. pub struct Cache { - /// A map that holds the layouted nodes from past compilations. - pub frames: HashMap, + /// Caches parsed font faces. + pub font: FontCache, + /// Caches decoded images. + pub image: ImageCache, + /// Caches layouting artifacts. + pub layout: LayoutCache, } impl Cache { /// Create a new, empty cache. - pub fn new() -> Self { - Self::default() - } - - /// Clear the cache. - pub fn clear(&mut self) { - self.frames.clear(); + pub fn new(loader: &dyn Loader) -> Self { + Self { + font: FontCache::new(loader), + image: ImageCache::new(), + layout: LayoutCache::new(), + } } } - -/// Frames from past compilations and checks for their validity in future -/// compilations. -#[derive(Debug, Clone)] -pub struct FramesEntry { - /// The regions in which these frames are valid. - pub regions: Regions, - /// Cached frames for a node. - pub frames: Vec, -} diff --git a/src/env/fs.rs b/src/env/fs.rs deleted file mode 100644 index 969ee9e0..00000000 --- a/src/env/fs.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::collections::{hash_map::Entry, HashMap}; -use std::fs::File; -use std::io; -use std::path::{Path, PathBuf}; -use std::rc::Rc; - -use memmap2::Mmap; -use serde::{Deserialize, Serialize}; -use ttf_parser::{name_id, Face}; -use walkdir::WalkDir; - -use super::{Buffer, Loader}; -use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; - -/// Loads fonts and images from the local file system. -/// -/// _This is only available when the `fs` feature is enabled._ -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct FsLoader { - faces: Vec, - files: Vec, - #[serde(skip)] - cache: FileCache, -} - -/// Maps from paths to loaded file buffers. When the buffer is `None` the file -/// does not exist or couldn't be read. -type FileCache = HashMap>; - -impl FsLoader { - /// Create a new loader without any fonts. - pub fn new() -> Self { - Self { - faces: vec![], - files: vec![], - cache: HashMap::new(), - } - } - - /// Search for fonts in the operating system's font directories. - #[cfg(all(unix, not(target_os = "macos")))] - pub fn search_system(&mut self) { - self.search_path("/usr/share/fonts"); - self.search_path("/usr/local/share/fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(dir); - } - } - - /// Search for fonts in the operating system's font directories. - #[cfg(target_os = "macos")] - pub fn search_system(&mut self) { - self.search_path("/Library/Fonts"); - self.search_path("/Network/Library/Fonts"); - self.search_path("/System/Library/Fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_path(dir); - } - } - - /// Search for fonts in the operating system's font directories. - #[cfg(windows)] - pub fn search_system(&mut self) { - let windir = - std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); - - self.search_path(Path::new(&windir).join("Fonts")); - - if let Some(roaming) = dirs::config_dir() { - self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); - } - - if let Some(local) = dirs::cache_dir() { - self.search_path(local.join("Microsoft\\Windows\\Fonts")); - } - } - - /// Search for all fonts at a path. - /// - /// If the path is a directory, all contained fonts will be searched for - /// recursively. - pub fn search_path(&mut self, dir: impl AsRef) { - let walk = WalkDir::new(dir) - .follow_links(true) - .sort_by(|a, b| a.file_name().cmp(b.file_name())) - .into_iter() - .filter_map(|e| e.ok()); - - for entry in walk { - let path = entry.path(); - if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - match ext { - #[rustfmt::skip] - "ttf" | "otf" | "TTF" | "OTF" | - "ttc" | "otc" | "TTC" | "OTC" => { - self.search_file(path).ok(); - } - _ => {} - } - } - } - } - - /// Index the font faces in the file at the given path. - /// - /// The file may form a font collection and contain multiple font faces, - /// which will then all be indexed. - fn search_file(&mut self, path: impl AsRef) -> io::Result<()> { - let path = path.as_ref(); - let path = path.strip_prefix(".").unwrap_or(path); - - let file = File::open(path)?; - let mmap = unsafe { Mmap::map(&file)? }; - - for i in 0 .. ttf_parser::fonts_in_collection(&mmap).unwrap_or(1) { - let face = Face::from_slice(&mmap, i) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - - self.parse_face(path, &face, i)?; - } - - Ok(()) - } - - /// Parse a single face and insert it into the `families`. This either - /// merges with an existing family entry if they have the same trimmed - /// family name, or creates a new one. - fn parse_face(&mut self, path: &Path, face: &Face<'_>, index: u32) -> io::Result<()> { - fn find_name(face: &Face, name_id: u16) -> Option { - face.names().find_map(|entry| { - (entry.name_id() == name_id).then(|| entry.to_string()).flatten() - }) - } - - let family = find_name(face, name_id::TYPOGRAPHIC_FAMILY) - .or_else(|| find_name(face, name_id::FAMILY)) - .ok_or("unknown font family") - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; - - let variant = FontVariant { - style: match (face.is_italic(), face.is_oblique()) { - (false, false) => FontStyle::Normal, - (true, _) => FontStyle::Italic, - (_, true) => FontStyle::Oblique, - }, - weight: FontWeight::from_number(face.weight().to_number()), - stretch: FontStretch::from_number(face.width().to_number()), - }; - - // Merge with an existing entry for the same family name. - self.faces.push(FaceInfo { family, variant, index }); - self.files.push(path.to_owned()); - - Ok(()) - } - - /// Paths to font files, parallel to [`faces()`](Self::faces). - pub fn files(&self) -> &[PathBuf] { - &self.files - } -} - -impl Loader for FsLoader { - fn faces(&self) -> &[FaceInfo] { - &self.faces - } - - fn load_face(&mut self, idx: usize) -> Option { - load(&mut self.cache, &self.files[idx]) - } - - fn load_file(&mut self, path: &str) -> Option { - load(&mut self.cache, Path::new(path)) - } -} - -/// Load from the file system using a cache. -fn load(cache: &mut FileCache, path: &Path) -> Option { - match cache.entry(path.to_owned()) { - Entry::Occupied(entry) => entry.get().clone(), - Entry::Vacant(entry) => { - let buffer = std::fs::read(path).ok().map(Rc::new); - entry.insert(buffer).clone() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_index_font_dir() { - let mut loader = FsLoader::new(); - loader.search_path("fonts"); - - assert_eq!(loader.files, &[ - Path::new("fonts/EBGaramond-Bold.ttf"), - Path::new("fonts/EBGaramond-BoldItalic.ttf"), - Path::new("fonts/EBGaramond-Italic.ttf"), - Path::new("fonts/EBGaramond-Regular.ttf"), - Path::new("fonts/Inconsolata-Bold.ttf"), - Path::new("fonts/Inconsolata-Regular.ttf"), - Path::new("fonts/LatinModernMath.otf"), - Path::new("fonts/NotoSansArabic-Regular.ttf"), - Path::new("fonts/NotoSerifCJKsc-Regular.otf"), - Path::new("fonts/NotoSerifHebrew-Bold.ttf"), - Path::new("fonts/NotoSerifHebrew-Regular.ttf"), - Path::new("fonts/PTSans-Regular.ttf"), - Path::new("fonts/TwitterColorEmoji.ttf"), - ]); - } -} diff --git a/src/env/image.rs b/src/env/image.rs deleted file mode 100644 index 365ff312..00000000 --- a/src/env/image.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::io::Cursor; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, GenericImageView, ImageFormat}; - -/// A loaded image. -pub struct Image { - /// The original format the image was encoded in. - pub format: ImageFormat, - /// The decoded image. - pub buf: DynamicImage, -} - -impl Image { - /// Parse an image from raw data in a supported format. - /// - /// The image format is determined automatically. - pub fn parse(data: &[u8]) -> Option { - let cursor = Cursor::new(data); - let reader = ImageReader::new(cursor).with_guessed_format().ok()?; - let format = reader.format()?; - let buf = reader.decode().ok()?; - Some(Self { format, buf }) - } - - /// The width of the image. - pub fn width(&self) -> u32 { - self.buf.width() - } - - /// The height of the image. - pub fn height(&self) -> u32 { - self.buf.height() - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format) - .field("color", &self.buf.color()) - .field("width", &self.width()) - .field("height", &self.height()) - .finish() - } -} diff --git a/src/env/mod.rs b/src/env/mod.rs deleted file mode 100644 index 84be3e81..00000000 --- a/src/env/mod.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Font and image loading. - -#[cfg(feature = "fs")] -mod fs; -mod image; - -pub use self::image::*; -#[cfg(feature = "fs")] -pub use fs::*; - -use std::collections::{hash_map::Entry, HashMap}; -use std::rc::Rc; - -use serde::{Deserialize, Serialize}; - -use crate::font::{Face, FaceInfo, FontVariant}; - -/// Handles font and image loading. -pub struct Env { - /// The loader that serves the font face and file buffers. - loader: Box, - /// Faces indexed by [`FaceId`]. `None` if not yet loaded. - faces: Vec>, - /// Maps a family name to the ids of all faces that are part of the family. - families: HashMap>, - /// Loaded images indexed by [`ImageId`]. - images: Vec, - /// Maps from paths to loaded images. - paths: HashMap, - /// Callback for loaded font faces. - on_face_load: Option>, - /// Callback for loaded images. - on_image_load: Option>, -} - -impl Env { - /// Create an environment from a `loader`. - pub fn new(loader: impl Loader + 'static) -> Self { - let infos = loader.faces(); - - let mut faces = vec![]; - let mut families = HashMap::>::new(); - - for (i, info) in infos.iter().enumerate() { - let id = FaceId(i as u32); - faces.push(None); - families - .entry(info.family.to_lowercase()) - .and_modify(|vec| vec.push(id)) - .or_insert_with(|| vec![id]); - } - - Self { - loader: Box::new(loader), - faces, - families, - images: vec![], - paths: HashMap::new(), - on_face_load: None, - on_image_load: None, - } - } - - /// Create an empty environment for testing purposes. - pub fn blank() -> Self { - struct BlankLoader; - - impl Loader for BlankLoader { - fn faces(&self) -> &[FaceInfo] { - &[] - } - - fn load_face(&mut self, _: usize) -> Option { - None - } - - fn load_file(&mut self, _: &str) -> Option { - None - } - } - - Self::new(BlankLoader) - } - - /// Query for and load the font face from the given `family` that most - /// closely matches the given `variant`. - pub fn query_face(&mut self, family: &str, variant: FontVariant) -> Option { - // Check whether a family with this name exists. - let ids = self.families.get(family)?; - let infos = self.loader.faces(); - - let mut best = None; - let mut best_key = None; - - // Find the best matching variant of this font. - for &id in ids { - let current = infos[id.0 as usize].variant; - - // This is a perfect match, no need to search further. - if current == variant { - best = Some(id); - break; - } - - // If this is not a perfect match, we compute a key that we want to - // minimize among all variants. This key prioritizes style, then - // stretch distance and then weight distance. - let key = ( - current.style != variant.style, - current.stretch.distance(variant.stretch), - current.weight.distance(variant.weight), - ); - - if best_key.map_or(true, |b| key < b) { - best = Some(id); - best_key = Some(key); - } - } - - // Load the face if it's not already loaded. - let id = best?; - let idx = id.0 as usize; - let slot = &mut self.faces[idx]; - if slot.is_none() { - let index = infos[idx].index; - let buffer = self.loader.load_face(idx)?; - let face = Face::new(buffer, index)?; - if let Some(callback) = &self.on_face_load { - callback(id, &face); - } - *slot = Some(face); - } - - best - } - - /// Get a reference to a queried face. - /// - /// This panics if no face with this id was loaded. This function should - /// only be called with ids returned by [`query_face()`](Self::query_face). - #[track_caller] - pub fn face(&self, id: FaceId) -> &Face { - self.faces[id.0 as usize].as_ref().expect("font face was not loaded") - } - - /// Register a callback which is invoked when a font face was loaded. - pub fn on_face_load(&mut self, f: F) - where - F: Fn(FaceId, &Face) + 'static, - { - self.on_face_load = Some(Box::new(f)); - } - - /// Load and decode an image file from a path. - pub fn load_image(&mut self, path: &str) -> Option { - Some(match self.paths.entry(path.to_string()) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let buffer = self.loader.load_file(path)?; - let image = Image::parse(&buffer)?; - let id = ImageId(self.images.len() as u32); - if let Some(callback) = &self.on_image_load { - callback(id, &image); - } - self.images.push(image); - *entry.insert(id) - } - }) - } - - /// Get a reference to a loaded image. - /// - /// This panics if no image with this id was loaded. This function should - /// only be called with ids returned by [`load_image()`](Self::load_image). - #[track_caller] - pub fn image(&self, id: ImageId) -> &Image { - &self.images[id.0 as usize] - } - - /// Register a callback which is invoked when an image was loaded. - pub fn on_image_load(&mut self, f: F) - where - F: Fn(ImageId, &Image) + 'static, - { - self.on_image_load = Some(Box::new(f)); - } -} - -/// Loads fonts and images from a remote or local source. -pub trait Loader { - /// Descriptions of all font faces this loader serves. - fn faces(&self) -> &[FaceInfo]; - - /// Load the font face with the given index in [`faces()`](Self::faces). - fn load_face(&mut self, idx: usize) -> Option; - - /// Load a file from a path. - fn load_file(&mut self, path: &str) -> Option; -} - -/// A shared byte buffer. -pub type Buffer = Rc>; - -/// A unique identifier for a loaded font face. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct FaceId(u32); - -impl FaceId { - /// A blank initialization value. - pub const MAX: Self = Self(u32::MAX); - - /// Create a face id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} - -/// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ImageId(u32); - -impl ImageId { - /// Create an image id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { - self.0 - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index da7fca53..d841dbae 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -10,38 +10,50 @@ pub use capture::*; pub use scope::*; pub use value::*; -use std::collections::HashMap; use std::rc::Rc; +use crate::cache::Cache; use crate::color::Color; use crate::diag::{Diag, DiagSet, Pass}; -use crate::env::Env; use crate::geom::{Angle, Length, Relative}; +use crate::loading::Loader; use crate::syntax::visit::Visit; use crate::syntax::*; -/// Evaluate all nodes in a syntax tree. +/// Evaluated a parsed source file into a module. /// /// The `scope` consists of the base definitions that are present from the /// beginning (typically, the standard library). -pub fn eval(env: &mut Env, tree: &Tree, scope: &Scope) -> Pass { - let mut ctx = EvalContext::new(env, scope); +pub fn eval( + loader: &mut dyn Loader, + cache: &mut Cache, + tree: Rc, + base: &Scope, +) -> Pass { + let mut ctx = EvalContext::new(loader, cache, base); let map = tree.eval(&mut ctx); - Pass::new(map, ctx.diags) + let module = Module { + scope: ctx.scopes.top, + template: vec![TemplateNode::Tree { tree, map }], + }; + Pass::new(module, ctx.diags) } -/// A map from nodes to the values they evaluated to. -/// -/// The raw pointers point into the nodes contained in some [`Tree`]. Since the -/// lifetime is erased, the tree could go out of scope while the hash map still -/// lives. Although this could lead to lookup panics, it is not unsafe since the -/// pointers are never dereferenced. -pub type NodeMap = HashMap<*const Node, Value>; +/// An evaluated module, ready for importing or execution. +#[derive(Debug, Clone, PartialEq)] +pub struct Module { + /// The top-level definitions that were bound in this module. + pub scope: Scope, + /// The template defined by this module. + pub template: TemplateValue, +} /// The context for evaluation. pub struct EvalContext<'a> { - /// The environment from which resources are gathered. - pub env: &'a mut Env, + /// The loader from which resources (files and images) are loaded. + pub loader: &'a mut dyn Loader, + /// A cache for loaded resources. + pub cache: &'a mut Cache, /// The active scopes. pub scopes: Scopes<'a>, /// Evaluation diagnostics. @@ -49,11 +61,16 @@ pub struct EvalContext<'a> { } impl<'a> EvalContext<'a> { - /// Create a new execution context with a base scope. - pub fn new(env: &'a mut Env, scope: &'a Scope) -> Self { + /// Create a new evaluation context with a base scope. + pub fn new( + loader: &'a mut dyn Loader, + cache: &'a mut Cache, + base: &'a Scope, + ) -> Self { Self { - env, - scopes: Scopes::with_base(scope), + loader, + cache, + scopes: Scopes::with_base(base), diags: DiagSet::new(), } } diff --git a/src/eval/value.rs b/src/eval/value.rs index 0d87c28f..d10d734a 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -1,15 +1,15 @@ use std::any::Any; use std::cmp::Ordering; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Deref; use std::rc::Rc; -use super::{EvalContext, NodeMap}; +use super::EvalContext; use crate::color::{Color, RgbaColor}; use crate::exec::ExecContext; use crate::geom::{Angle, Length, Linear, Relative}; -use crate::syntax::{Span, Spanned, Tree}; +use crate::syntax::{Node, Span, Spanned, Tree}; /// A computational value. #[derive(Debug, Clone, PartialEq)] @@ -163,6 +163,14 @@ impl PartialEq for TemplateNode { } } +/// A map from nodes to the values they evaluated to. +/// +/// The raw pointers point into the nodes contained in some [`Tree`]. Since the +/// lifetime is erased, the tree could go out of scope while the hash map still +/// lives. Although this could lead to lookup panics, it is not unsafe since the +/// pointers are never dereferenced. +pub type NodeMap = HashMap<*const Node, Value>; + /// A reference-counted dynamic template node that can implement custom /// behaviour. #[derive(Clone)] diff --git a/src/exec/context.rs b/src/exec/context.rs index 93ffaf96..016b092a 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -2,7 +2,6 @@ use std::mem; use super::{Exec, FontFamily, State}; use crate::diag::{Diag, DiagSet, Pass}; -use crate::env::Env; use crate::eval::TemplateValue; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ @@ -11,9 +10,7 @@ use crate::layout::{ use crate::syntax::Span; /// The context for execution. -pub struct ExecContext<'a> { - /// The environment from which resources are gathered. - pub env: &'a mut Env, +pub struct ExecContext { /// The active execution state. pub state: State, /// Execution diagnostics. @@ -27,11 +24,10 @@ pub struct ExecContext<'a> { stack: StackBuilder, } -impl<'a> ExecContext<'a> { +impl ExecContext { /// Create a new execution context with a base state. - pub fn new(env: &'a mut Env, state: State) -> Self { + pub fn new(state: State) -> Self { Self { - env, diags: DiagSet::new(), tree: Tree { runs: vec![] }, page: Some(PageBuilder::new(&state, true)), diff --git a/src/exec/mod.rs b/src/exec/mod.rs index b6765d1e..643d5b44 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -9,29 +9,18 @@ pub use state::*; use std::rc::Rc; use crate::diag::Pass; -use crate::env::Env; use crate::eval::{NodeMap, TemplateFunc, TemplateNode, TemplateValue, Value}; use crate::layout; use crate::pretty::pretty; use crate::syntax::*; -/// Execute a syntax tree to produce a layout tree. -/// -/// The `map` shall be a node map computed for this tree with -/// [`eval`](crate::eval::eval). Note that `tree` must be the _exact_ same tree -/// as used for evaluation (no cloned version), because the node map depends on -/// the pointers being stable. +/// Execute a template to produce a layout tree. /// /// The `state` is the base state that may be updated over the course of /// execution. -pub fn exec( - env: &mut Env, - tree: &Tree, - map: &NodeMap, - state: State, -) -> Pass { - let mut ctx = ExecContext::new(env, state); - tree.exec_with_map(&mut ctx, &map); +pub fn exec(template: &TemplateValue, state: State) -> Pass { + let mut ctx = ExecContext::new(state); + template.exec(&mut ctx); ctx.finish() } diff --git a/src/export/mod.rs b/src/export/mod.rs new file mode 100644 index 00000000..5ed0abf9 --- /dev/null +++ b/src/export/mod.rs @@ -0,0 +1,5 @@ +//! Exporting into external formats. + +mod pdf; + +pub use pdf::*; diff --git a/src/export/pdf.rs b/src/export/pdf.rs new file mode 100644 index 00000000..0ca4df38 --- /dev/null +++ b/src/export/pdf.rs @@ -0,0 +1,586 @@ +//! Exporting into PDF documents. + +use std::cmp::Eq; +use std::collections::HashMap; +use std::hash::Hash; + +use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; +use miniz_oxide::deflate; +use pdf_writer::{ + CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str, + SystemInfo, UnicodeCmap, +}; +use ttf_parser::{name_id, GlyphId}; + +use crate::cache::Cache; +use crate::color::Color; +use crate::font::{Em, FaceId, VerticalFontMetric}; +use crate::geom::{self, Length, Size}; +use crate::image::{Image, ImageId}; +use crate::layout::{Element, Fill, Frame, Shape}; + +/// Export a collection of frames into a PDF document. +/// +/// This creates one page per frame. In addition to the frames, you need to pass +/// in the cache used during compilation such that things like fonts and images +/// can be included in the PDF. +/// +/// Returns the raw bytes making up the PDF document. +pub fn pdf(cache: &Cache, frames: &[Frame]) -> Vec { + PdfExporter::new(cache, frames).write() +} + +struct PdfExporter<'a> { + writer: PdfWriter, + frames: &'a [Frame], + cache: &'a Cache, + refs: Refs, + fonts: Remapper, + images: Remapper, +} + +impl<'a> PdfExporter<'a> { + fn new(cache: &'a Cache, frames: &'a [Frame]) -> Self { + let mut writer = PdfWriter::new(1, 7); + writer.set_indent(2); + + let mut fonts = Remapper::new(); + let mut images = Remapper::new(); + let mut alpha_masks = 0; + + for frame in frames { + for (_, element) in &frame.elements { + match *element { + Element::Text(ref shaped) => fonts.insert(shaped.face_id), + Element::Geometry(_, _) => {} + Element::Image(id, _) => { + let img = cache.image.get(id); + if img.buf.color().has_alpha() { + alpha_masks += 1; + } + images.insert(id); + } + } + } + } + + let refs = Refs::new(frames.len(), fonts.len(), images.len(), alpha_masks); + + Self { + writer, + frames, + cache, + refs, + fonts, + images, + } + } + + fn write(mut self) -> Vec { + self.write_structure(); + self.write_pages(); + self.write_fonts(); + self.write_images(); + self.writer.finish(self.refs.catalog) + } + + fn write_structure(&mut self) { + // The document catalog. + self.writer.catalog(self.refs.catalog).pages(self.refs.page_tree); + + // The root page tree. + let mut pages = self.writer.pages(self.refs.page_tree); + pages.kids(self.refs.pages()); + + let mut resources = pages.resources(); + let mut fonts = resources.fonts(); + for (refs, f) in self.refs.fonts().zip(self.fonts.pdf_indices()) { + let name = format!("F{}", f); + fonts.pair(Name(name.as_bytes()), refs.type0_font); + } + + drop(fonts); + + let mut images = resources.x_objects(); + for (id, im) in self.refs.images().zip(self.images.pdf_indices()) { + let name = format!("Im{}", im); + images.pair(Name(name.as_bytes()), id); + } + + drop(images); + drop(resources); + drop(pages); + + // The page objects (non-root nodes in the page tree). + for ((page_id, content_id), page) in + self.refs.pages().zip(self.refs.contents()).zip(self.frames) + { + self.writer + .page(page_id) + .parent(self.refs.page_tree) + .media_box(Rect::new( + 0.0, + 0.0, + page.size.width.to_pt() as f32, + page.size.height.to_pt() as f32, + )) + .contents(content_id); + } + } + + fn write_pages(&mut self) { + for (id, page) in self.refs.contents().zip(self.frames) { + self.write_page(id, &page); + } + } + + fn write_page(&mut self, id: Ref, page: &'a Frame) { + let mut content = Content::new(); + + // We only write font switching actions when the used face changes. To + // do that, we need to remember the active face. + let mut face = FaceId::MAX; + let mut size = Length::zero(); + let mut fill: Option = None; + + for (pos, element) in &page.elements { + let x = pos.x.to_pt() as f32; + let y = (page.size.height - pos.y).to_pt() as f32; + + match *element { + Element::Text(ref shaped) => { + if fill != Some(shaped.fill) { + write_fill(&mut content, shaped.fill); + fill = Some(shaped.fill); + } + + let mut text = content.text(); + + // Then, also check if we need to issue a font switching + // action. + if shaped.face_id != face || shaped.size != size { + face = shaped.face_id; + size = shaped.size; + + let name = format!("F{}", self.fonts.map(shaped.face_id)); + text.font(Name(name.as_bytes()), size.to_pt() as f32); + } + + // TODO: Respect individual glyph offsets. + text.matrix(1.0, 0.0, 0.0, 1.0, x, y); + text.show(Str(&shaped.encode_glyphs_be())); + } + + Element::Geometry(ref shape, fill) => { + content.save_state(); + write_fill(&mut content, fill); + + match *shape { + Shape::Rect(Size { width, height }) => { + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; + if w > 0.0 && h > 0.0 { + content.rect(x, y - h, w, h, false, true); + } + } + + Shape::Ellipse(size) => { + let path = geom::Path::ellipse(size); + write_path(&mut content, x, y, &path, false, true); + } + + Shape::Path(ref path) => { + write_path(&mut content, x, y, path, false, true) + } + } + + content.restore_state(); + } + + Element::Image(id, Size { width, height }) => { + let name = format!("Im{}", self.images.map(id)); + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; + + content.save_state(); + content.matrix(w, 0.0, 0.0, h, x, y - h); + content.x_object(Name(name.as_bytes())); + content.restore_state(); + } + } + } + + self.writer.stream(id, &content.finish()); + } + + fn write_fonts(&mut self) { + for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) { + let face = self.cache.font.get(face_id); + let ttf = face.ttf(); + + let name = ttf + .names() + .find(|entry| { + entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode() + }) + .and_then(|entry| entry.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let base_font = format!("ABCDEF+{}", name); + let base_font = Name(base_font.as_bytes()); + let cmap_name = Name(b"Custom"); + let system_info = SystemInfo { + registry: Str(b"Adobe"), + ordering: Str(b"Identity"), + supplement: 0, + }; + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::SERIF, name.contains("Serif")); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let global_bbox = ttf.global_bounding_box(); + let bbox = Rect::new( + face.to_em(global_bbox.x_min).to_pdf(), + face.to_em(global_bbox.y_min).to_pdf(), + face.to_em(global_bbox.x_max).to_pdf(), + face.to_em(global_bbox.y_max).to_pdf(), + ); + + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = face.vertical_metric(VerticalFontMetric::Ascender).to_pdf(); + let descender = face.vertical_metric(VerticalFontMetric::Descender).to_pdf(); + let cap_height = face.vertical_metric(VerticalFontMetric::CapHeight).to_pdf(); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); + + // Write the base font object referencing the CID font. + self.writer + .type0_font(refs.type0_font) + .base_font(base_font) + .encoding_predefined(Name(b"Identity-H")) + .descendant_font(refs.cid_font) + .to_unicode(refs.cmap); + + // Write the CID font referencing the font descriptor. + self.writer + .cid_font(refs.cid_font, CidFontType::Type2) + .base_font(base_font) + .system_info(system_info) + .font_descriptor(refs.font_descriptor) + .widths() + .individual(0, { + let num_glyphs = ttf.number_of_glyphs(); + (0 .. num_glyphs).map(|g| { + let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0); + face.to_em(x).to_pdf() + }) + }); + + // Write the font descriptor (contains metrics about the font). + self.writer + .font_descriptor(refs.font_descriptor) + .font_name(base_font) + .font_flags(flags) + .font_bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v) + .font_file2(refs.data); + + // Write the to-unicode character map, which maps glyph ids back to + // unicode codepoints to enable copying out of the PDF. + self.writer + .cmap(refs.cmap, &{ + let mut cmap = UnicodeCmap::new(cmap_name, system_info); + for subtable in ttf.character_mapping_subtables() { + subtable.codepoints(|n| { + if let Some(c) = std::char::from_u32(n) { + if let Some(g) = ttf.glyph_index(c) { + cmap.pair(g.0, c); + } + } + }) + } + cmap.finish() + }) + .name(cmap_name) + .system_info(system_info); + + // Write the face's bytes. + self.writer.stream(refs.data, face.buffer()); + } + } + + fn write_images(&mut self) { + let mut masks_seen = 0; + + for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) { + let img = self.cache.image.get(image_id); + let (width, height) = img.buf.dimensions(); + + // Add the primary image. + if let Ok((data, filter, color_space)) = encode_image(img) { + let mut image = self.writer.image(id, &data); + image.filter(filter); + image.width(width as i32); + image.height(height as i32); + image.color_space(color_space); + image.bits_per_component(8); + + // Add a second gray-scale image containing the alpha values if + // this image has an alpha channel. + if img.buf.color().has_alpha() { + let (alpha_data, alpha_filter) = encode_alpha(img); + let mask_id = self.refs.alpha_mask(masks_seen); + image.s_mask(mask_id); + drop(image); + + let mut mask = self.writer.image(mask_id, &alpha_data); + mask.filter(alpha_filter); + mask.width(width as i32); + mask.height(height as i32); + mask.color_space(ColorSpace::DeviceGray); + mask.bits_per_component(8); + + masks_seen += 1; + } + } else { + // TODO: Warn that image could not be encoded. + self.writer + .image(id, &[]) + .width(0) + .height(0) + .color_space(ColorSpace::DeviceGray) + .bits_per_component(1); + } + } + } +} + +/// Write a fill change into a content stream. +fn write_fill(content: &mut Content, fill: Fill) { + match fill { + Fill::Color(Color::Rgba(c)) => { + content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0); + } + } +} + +/// Write a path into a content stream. +fn write_path( + content: &mut Content, + x: f32, + y: f32, + path: &geom::Path, + stroke: bool, + fill: bool, +) { + let f = |length: Length| length.to_pt() as f32; + let mut builder = content.path(stroke, fill); + for elem in &path.0 { + match elem { + geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)), + geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)), + geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to( + x + f(p1.x), + y + f(p1.y), + x + f(p2.x), + y + f(p2.y), + x + f(p3.x), + y + f(p3.y), + ), + geom::PathElement::ClosePath => builder.close_path(), + }; + } +} + +/// The compression level for the deflating. +const DEFLATE_LEVEL: u8 = 6; + +/// Encode an image with a suitable filter. +/// +/// Skips the alpha channel as that's encoded separately. +fn encode_image(img: &Image) -> ImageResult<(Vec, Filter, ColorSpace)> { + let mut data = vec![]; + let (filter, space) = match (img.format, &img.buf) { + // 8-bit gray JPEG. + (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { + img.buf.write_to(&mut data, img.format)?; + (Filter::DctDecode, ColorSpace::DeviceGray) + } + + // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier). + (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => { + img.buf.write_to(&mut data, img.format)?; + (Filter::DctDecode, ColorSpace::DeviceRgb) + } + + // TODO: Encode flate streams with PNG-predictor? + + // 8-bit gray PNG. + (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => { + data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL); + (Filter::FlateDecode, ColorSpace::DeviceGray) + } + + // Anything else (including Rgb(a) PNGs). + (_, buf) => { + let (width, height) = buf.dimensions(); + let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); + for (_, _, Rgba([r, g, b, _])) in buf.pixels() { + pixels.push(r); + pixels.push(g); + pixels.push(b); + } + + data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); + (Filter::FlateDecode, ColorSpace::DeviceRgb) + } + }; + Ok((data, filter, space)) +} + +/// Encode an image's alpha channel if present. +fn encode_alpha(img: &Image) -> (Vec, Filter) { + let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); + let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); + (data, Filter::FlateDecode) +} + +/// We need to know exactly which indirect reference id will be used for which +/// objects up-front to correctly declare the document catalogue, page tree and +/// so on. These offsets are computed in the beginning and stored here. +struct Refs { + catalog: Ref, + page_tree: Ref, + pages_start: i32, + contents_start: i32, + fonts_start: i32, + images_start: i32, + alpha_masks_start: i32, + end: i32, +} + +struct FontRefs { + type0_font: Ref, + cid_font: Ref, + font_descriptor: Ref, + cmap: Ref, + data: Ref, +} + +impl Refs { + const OBJECTS_PER_FONT: usize = 5; + + fn new(frames: usize, fonts: usize, images: usize, alpha_masks: usize) -> Self { + let catalog = 1; + let page_tree = catalog + 1; + let pages_start = page_tree + 1; + let contents_start = pages_start + frames as i32; + let fonts_start = contents_start + frames as i32; + let images_start = fonts_start + (Self::OBJECTS_PER_FONT * fonts) as i32; + let alpha_masks_start = images_start + images as i32; + let end = alpha_masks_start + alpha_masks as i32; + + Self { + catalog: Ref::new(catalog), + page_tree: Ref::new(page_tree), + pages_start, + contents_start, + fonts_start, + images_start, + alpha_masks_start, + end, + } + } + + fn pages(&self) -> impl Iterator { + (self.pages_start .. self.contents_start).map(Ref::new) + } + + fn contents(&self) -> impl Iterator { + (self.contents_start .. self.images_start).map(Ref::new) + } + + fn fonts(&self) -> impl Iterator { + (self.fonts_start .. self.images_start) + .step_by(Self::OBJECTS_PER_FONT) + .map(|id| FontRefs { + type0_font: Ref::new(id), + cid_font: Ref::new(id + 1), + font_descriptor: Ref::new(id + 2), + cmap: Ref::new(id + 3), + data: Ref::new(id + 4), + }) + } + + fn images(&self) -> impl Iterator { + (self.images_start .. self.end).map(Ref::new) + } + + fn alpha_mask(&self, i: usize) -> Ref { + Ref::new(self.alpha_masks_start + i as i32) + } +} + +/// Used to assign new, consecutive PDF-internal indices to things. +struct Remapper { + /// Forwards from the old indices to the new pdf indices. + to_pdf: HashMap, + /// Backwards from the pdf indices to the old indices. + to_layout: Vec, +} + +impl Remapper +where + Index: Copy + Eq + Hash, +{ + fn new() -> Self { + Self { + to_pdf: HashMap::new(), + to_layout: vec![], + } + } + + fn len(&self) -> usize { + self.to_layout.len() + } + + fn insert(&mut self, index: Index) { + let to_layout = &mut self.to_layout; + self.to_pdf.entry(index).or_insert_with(|| { + let pdf_index = to_layout.len(); + to_layout.push(index); + pdf_index + }); + } + + fn map(&self, index: Index) -> usize { + self.to_pdf[&index] + } + + fn pdf_indices(&self) -> impl Iterator { + 0 .. self.to_pdf.len() + } + + fn layout_indices(&self) -> impl Iterator + '_ { + self.to_layout.iter().copied() + } +} + +/// Additional methods for [`Em`]. +trait EmExt { + /// Convert an em length to a number of PDF font units. + fn to_pdf(self) -> f32; +} + +impl EmExt for Em { + fn to_pdf(self) -> f32 { + 1000.0 * self.get() as f32 + } +} diff --git a/src/font.rs b/src/font.rs index dd81fa88..69a30900 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,11 +1,13 @@ //! Font handling. +use std::collections::HashMap; use std::fmt::{self, Debug, Display, Formatter}; use serde::{Deserialize, Serialize}; -use crate::env::Buffer; use crate::geom::Length; +use crate::loading::Buffer; +use crate::loading::Loader; /// A font face. pub struct Face { @@ -155,6 +157,128 @@ impl Em { } } +/// Caches parsed font faces. +pub struct FontCache { + faces: Vec>, + families: HashMap>, + on_load: Option>, +} + +impl FontCache { + /// Create a new, empty font cache. + pub fn new(loader: &dyn Loader) -> Self { + let mut faces = vec![]; + let mut families = HashMap::>::new(); + + for (i, info) in loader.faces().iter().enumerate() { + let id = FaceId(i as u32); + faces.push(None); + families + .entry(info.family.to_lowercase()) + .and_modify(|vec| vec.push(id)) + .or_insert_with(|| vec![id]); + } + + Self { faces, families, on_load: None } + } + + /// Query for and load the font face from the given `family` that most + /// closely matches the given `variant`. + pub fn select( + &mut self, + loader: &mut dyn Loader, + family: &str, + variant: FontVariant, + ) -> Option { + // Check whether a family with this name exists. + let ids = self.families.get(family)?; + let infos = loader.faces(); + + let mut best = None; + let mut best_key = None; + + // Find the best matching variant of this font. + for &id in ids { + let current = infos[id.0 as usize].variant; + + // This is a perfect match, no need to search further. + if current == variant { + best = Some(id); + break; + } + + // If this is not a perfect match, we compute a key that we want to + // minimize among all variants. This key prioritizes style, then + // stretch distance and then weight distance. + let key = ( + current.style != variant.style, + current.stretch.distance(variant.stretch), + current.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + // Load the face if it's not already loaded. + let id = best?; + let idx = id.0 as usize; + let slot = &mut self.faces[idx]; + if slot.is_none() { + let index = infos[idx].index; + let buffer = loader.load_face(idx)?; + let face = Face::new(buffer, index)?; + if let Some(callback) = &self.on_load { + callback(id, &face); + } + *slot = Some(face); + } + + best + } + + /// Get a reference to a loaded face. + /// + /// This panics if no face with this id was loaded. This function should + /// only be called with ids returned by [`select()`](Self::select). + #[track_caller] + pub fn get(&self, id: FaceId) -> &Face { + self.faces[id.0 as usize].as_ref().expect("font face was not loaded") + } + + /// Register a callback which is invoked each time a font face is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(FaceId, &Face) + 'static, + { + self.on_load = Some(Box::new(f)); + } +} + +/// A unique identifier for a loaded font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct FaceId(u32); + +impl FaceId { + /// A blank initialization value. + pub const MAX: Self = Self(u32::MAX); + + /// Create a face id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub fn into_raw(self) -> u32 { + self.0 + } +} + /// Properties of a single font face. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FaceInfo { diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 00000000..bdfc19a6 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,127 @@ +//! Image handling. + +use std::collections::{hash_map::Entry, HashMap}; +use std::fmt::{self, Debug, Formatter}; +use std::io::Cursor; + +use image::io::Reader as ImageReader; +use image::{DynamicImage, GenericImageView, ImageFormat}; +use serde::{Deserialize, Serialize}; + +use crate::loading::Loader; + +/// A loaded image. +pub struct Image { + /// The original format the image was encoded in. + pub format: ImageFormat, + /// The decoded image. + pub buf: DynamicImage, +} + +impl Image { + /// Parse an image from raw data in a supported format (PNG or JPEG). + /// + /// The image format is determined automatically. + pub fn parse(data: &[u8]) -> Option { + let cursor = Cursor::new(data); + let reader = ImageReader::new(cursor).with_guessed_format().ok()?; + let format = reader.format()?; + let buf = reader.decode().ok()?; + Some(Self { format, buf }) + } + + /// The width of the image. + pub fn width(&self) -> u32 { + self.buf.width() + } + + /// The height of the image. + pub fn height(&self) -> u32 { + self.buf.height() + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Image") + .field("format", &self.format) + .field("color", &self.buf.color()) + .field("width", &self.width()) + .field("height", &self.height()) + .finish() + } +} + +/// Caches decoded images. +pub struct ImageCache { + /// Loaded images indexed by [`ImageId`]. + images: Vec, + /// Maps from paths to loaded images. + paths: HashMap, + /// Callback for loaded images. + on_load: Option>, +} + +impl ImageCache { + /// Create a new, empty image cache. + pub fn new() -> Self { + Self { + images: vec![], + paths: HashMap::new(), + on_load: None, + } + } + + /// Load and decode an image file from a path. + pub fn load(&mut self, loader: &mut dyn Loader, path: &str) -> Option { + Some(match self.paths.entry(path.to_string()) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let buffer = loader.load_file(path)?; + let image = Image::parse(&buffer)?; + let id = ImageId(self.images.len() as u32); + if let Some(callback) = &self.on_load { + callback(id, &image); + } + self.images.push(image); + *entry.insert(id) + } + }) + } + + /// Get a reference to a loaded image. + /// + /// This panics if no image with this id was loaded. This function should + /// only be called with ids returned by [`load()`](Self::load). + #[track_caller] + pub fn get(&self, id: ImageId) -> &Image { + &self.images[id.0 as usize] + } + + /// Register a callback which is invoked each time an image is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(ImageId, &Image) + 'static, + { + self.on_load = Some(Box::new(f)); + } +} + +/// A unique identifier for a loaded image. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ImageId(u32); + +impl ImageId { + /// Create an image id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub fn into_raw(self) -> u32 { + self.0 + } +} diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 61a84d6d..6cecc7a3 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -1,6 +1,7 @@ use crate::color::Color; -use crate::env::{FaceId, ImageId}; +use crate::font::FaceId; use crate::geom::{Length, Path, Point, Size}; +use crate::image::ImageId; use serde::{Deserialize, Serialize}; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index bdcf5ec4..30776fa2 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -17,19 +17,20 @@ pub use shaping::*; pub use stack::*; use std::any::Any; +use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use decorum::N64; use fxhash::FxHasher64; -use crate::cache::{Cache, FramesEntry}; -use crate::env::Env; +use crate::cache::Cache; use crate::geom::*; +use crate::loading::Loader; /// Layout a tree into a collection of frames. -pub fn layout(env: &mut Env, cache: &mut Cache, tree: &Tree) -> Vec { - tree.layout(&mut LayoutContext { env, cache }) +pub fn layout(loader: &mut dyn Loader, cache: &mut Cache, tree: &Tree) -> Vec { + tree.layout(&mut LayoutContext { loader, cache }) } /// A tree of layout nodes. @@ -92,14 +93,14 @@ impl AnyNode { impl Layout for AnyNode { fn layout(&self, ctx: &mut LayoutContext, regions: &Regions) -> Vec { - if let Some(hit) = ctx.cache.frames.get(&self.hash) { + if let Some(hit) = ctx.cache.layout.frames.get(&self.hash) { if &hit.regions == regions { return hit.frames.clone(); } } let frames = self.node.layout(ctx, regions); - ctx.cache.frames.insert(self.hash, FramesEntry { + ctx.cache.layout.frames.insert(self.hash, FramesEntry { regions: regions.clone(), frames: frames.clone(), }); @@ -170,13 +171,39 @@ pub trait Layout { /// The context for layouting. pub struct LayoutContext<'a> { - /// The environment from which fonts are gathered. - pub env: &'a mut Env, - /// A cache which enables reuse of layout artifacts from past compilation - /// cycles. + /// The loader from which fonts are loaded. + pub loader: &'a mut dyn Loader, + /// A cache for loaded fonts and artifacts from past layouting. pub cache: &'a mut Cache, } +/// Caches layouting artifacts. +pub struct LayoutCache { + /// Maps from node hashes to the resulting frames and regions in which the + /// frames are valid. + pub frames: HashMap, +} + +impl LayoutCache { + /// Create a new, empty layout cache. + pub fn new() -> Self { + Self { frames: HashMap::new() } + } + + /// Clear the cache. + pub fn clear(&mut self) { + self.frames.clear(); + } +} + +/// Cached frames from past layouting. +pub struct FramesEntry { + /// The regions in which these frames are valid. + pub regions: Regions, + /// The cached frames for a node. + pub frames: Vec, +} + /// A sequence of regions to layout into. #[derive(Debug, Clone, PartialEq)] pub struct Regions { diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index f8ab7037..14ea8611 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -5,9 +5,8 @@ use std::ops::Range; use rustybuzz::UnicodeBuffer; use super::{Element, Frame, Glyph, LayoutContext, Text}; -use crate::env::FaceId; use crate::exec::FontProps; -use crate::font::Face; +use crate::font::{Face, FaceId}; use crate::geom::{Dir, Length, Point, Size}; use crate::util::SliceExt; @@ -215,10 +214,12 @@ fn shape_segment<'a>( let (face_id, fallback) = loop { // Try to load the next available font family. match families.next() { - Some(family) => match ctx.env.query_face(family, props.variant) { - Some(id) => break (id, true), - None => {} - }, + Some(family) => { + match ctx.cache.font.select(ctx.loader, family, props.variant) { + Some(id) => break (id, true), + None => {} + } + } // We're out of families, so we don't do any more fallback and just // shape the tofus with the first face we originally used. None => match first_face { @@ -242,7 +243,7 @@ fn shape_segment<'a>( }); // Shape! - let mut face = ctx.env.face(face_id); + let mut face = ctx.cache.font.get(face_id); let buffer = rustybuzz::shape(face.ttf(), &[], buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); @@ -317,7 +318,7 @@ fn shape_segment<'a>( first_face, ); - face = ctx.env.face(face_id); + face = ctx.cache.font.get(face_id); } i += 1; @@ -331,6 +332,8 @@ fn measure( glyphs: &[ShapedGlyph], props: &FontProps, ) -> (Size, Length) { + let cache = &mut ctx.cache.font; + let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); @@ -343,14 +346,14 @@ fn measure( // When there are no glyphs, we just use the vertical metrics of the // first available font. for family in props.families.iter() { - if let Some(face_id) = ctx.env.query_face(family, props.variant) { - expand_vertical(ctx.env.face(face_id)); + if let Some(face_id) = cache.select(ctx.loader, family, props.variant) { + expand_vertical(cache.get(face_id)); break; } } } else { for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { - let face = ctx.env.face(face_id); + let face = cache.get(face_id); expand_vertical(face); for glyph in group { diff --git a/src/lib.rs b/src/lib.rs index 8742aeb8..c435c2dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,8 @@ //! tree]. The structures describing the tree can be found in the [syntax] //! module. //! - **Evaluation:** The next step is to [evaluate] the syntax tree. This -//! computes the value of each node in document and stores them in a map from -//! node-pointers to values. -//! - **Execution:** Now, we can [execute] the parsed and evaluated "script". +//! computes the value of each node in the document and produces a [module]. +//! - **Execution:** Now, we can [execute] the parsed and evaluated module. //! This produces a [layout tree], a high-level, fully styled representation //! of the document. The nodes of this tree are self-contained and //! order-independent and thus much better suited for layouting than the @@ -23,10 +22,11 @@ //! [parsed]: parse::parse //! [syntax tree]: syntax::Tree //! [evaluate]: eval::eval +//! [module]: eval::Module //! [execute]: exec::exec //! [layout tree]: layout::Tree //! [layouted]: layout::layout -//! [PDF]: pdf +//! [PDF]: export::pdf #[macro_use] pub mod diag; @@ -34,42 +34,45 @@ pub mod diag; pub mod eval; pub mod cache; pub mod color; -pub mod env; pub mod exec; +pub mod export; pub mod font; pub mod geom; +pub mod image; pub mod layout; pub mod library; +pub mod loading; pub mod paper; pub mod parse; -pub mod pdf; pub mod pretty; pub mod syntax; pub mod util; +use std::rc::Rc; + use crate::cache::Cache; use crate::diag::Pass; -use crate::env::Env; use crate::eval::Scope; use crate::exec::State; use crate::layout::Frame; +use crate::loading::Loader; -/// Process source code directly into a collection of frames. +/// Process source code directly into a collection of layouted frames. pub fn typeset( - env: &mut Env, + loader: &mut dyn Loader, cache: &mut Cache, src: &str, - scope: &Scope, + base: &Scope, state: State, ) -> Pass> { let parsed = parse::parse(src); - let evaluated = eval::eval(env, &parsed.output, scope); - let executed = exec::exec(env, &parsed.output, &evaluated.output, state); - let frames = layout::layout(env, cache, &executed.output); + let evaluated = eval::eval(loader, cache, Rc::new(parsed.output), base); + let executed = exec::exec(&evaluated.output.template, state); + let layouted = layout::layout(loader, cache, &executed.output); let mut diags = parsed.diags; diags.extend(evaluated.diags); diags.extend(executed.diags); - Pass::new(frames, diags) + Pass::new(layouted, diags) } diff --git a/src/library/image.rs b/src/library/image.rs index b73c26a9..cd6a97d1 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -1,7 +1,7 @@ use ::image::GenericImageView; use super::*; -use crate::env::ImageId; +use crate::image::ImageId; use crate::layout::{AnyNode, Element, Frame, Layout, LayoutContext, Regions}; /// `image`: An image. @@ -18,21 +18,26 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let width = args.eat_named(ctx, "width"); let height = args.eat_named(ctx, "height"); + let mut node = None; + if let Some(path) = &path { + if let Some(id) = ctx.cache.image.load(ctx.loader, &path.v) { + let img = ctx.cache.image.get(id); + let dimensions = img.buf.dimensions(); + node = Some(ImageNode { id, dimensions, width, height }); + } else { + ctx.diag(error!(path.span, "failed to load image")); + } + } + Value::template("image", move |ctx| { - if let Some(path) = &path { - if let Some(id) = ctx.env.load_image(&path.v) { - let img = ctx.env.image(id); - let dimensions = img.buf.dimensions(); - ctx.push(ImageNode { id, dimensions, width, height }); - } else { - ctx.diag(error!(path.span, "failed to load image")); - } + if let Some(node) = node { + ctx.push(node); } }) } /// An image node. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Hash)] struct ImageNode { /// The id of the image file. id: ImageId, diff --git a/src/loading/fs.rs b/src/loading/fs.rs new file mode 100644 index 00000000..969ee9e0 --- /dev/null +++ b/src/loading/fs.rs @@ -0,0 +1,215 @@ +use std::collections::{hash_map::Entry, HashMap}; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use memmap2::Mmap; +use serde::{Deserialize, Serialize}; +use ttf_parser::{name_id, Face}; +use walkdir::WalkDir; + +use super::{Buffer, Loader}; +use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; + +/// Loads fonts and images from the local file system. +/// +/// _This is only available when the `fs` feature is enabled._ +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct FsLoader { + faces: Vec, + files: Vec, + #[serde(skip)] + cache: FileCache, +} + +/// Maps from paths to loaded file buffers. When the buffer is `None` the file +/// does not exist or couldn't be read. +type FileCache = HashMap>; + +impl FsLoader { + /// Create a new loader without any fonts. + pub fn new() -> Self { + Self { + faces: vec![], + files: vec![], + cache: HashMap::new(), + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(all(unix, not(target_os = "macos")))] + pub fn search_system(&mut self) { + self.search_path("/usr/share/fonts"); + self.search_path("/usr/local/share/fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_path(dir); + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(target_os = "macos")] + pub fn search_system(&mut self) { + self.search_path("/Library/Fonts"); + self.search_path("/Network/Library/Fonts"); + self.search_path("/System/Library/Fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_path(dir); + } + } + + /// Search for fonts in the operating system's font directories. + #[cfg(windows)] + pub fn search_system(&mut self) { + let windir = + std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); + + self.search_path(Path::new(&windir).join("Fonts")); + + if let Some(roaming) = dirs::config_dir() { + self.search_path(roaming.join("Microsoft\\Windows\\Fonts")); + } + + if let Some(local) = dirs::cache_dir() { + self.search_path(local.join("Microsoft\\Windows\\Fonts")); + } + } + + /// Search for all fonts at a path. + /// + /// If the path is a directory, all contained fonts will be searched for + /// recursively. + pub fn search_path(&mut self, dir: impl AsRef) { + let walk = WalkDir::new(dir) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()); + + for entry in walk { + let path = entry.path(); + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + match ext { + #[rustfmt::skip] + "ttf" | "otf" | "TTF" | "OTF" | + "ttc" | "otc" | "TTC" | "OTC" => { + self.search_file(path).ok(); + } + _ => {} + } + } + } + } + + /// Index the font faces in the file at the given path. + /// + /// The file may form a font collection and contain multiple font faces, + /// which will then all be indexed. + fn search_file(&mut self, path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + let path = path.strip_prefix(".").unwrap_or(path); + + let file = File::open(path)?; + let mmap = unsafe { Mmap::map(&file)? }; + + for i in 0 .. ttf_parser::fonts_in_collection(&mmap).unwrap_or(1) { + let face = Face::from_slice(&mmap, i) + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + self.parse_face(path, &face, i)?; + } + + Ok(()) + } + + /// Parse a single face and insert it into the `families`. This either + /// merges with an existing family entry if they have the same trimmed + /// family name, or creates a new one. + fn parse_face(&mut self, path: &Path, face: &Face<'_>, index: u32) -> io::Result<()> { + fn find_name(face: &Face, name_id: u16) -> Option { + face.names().find_map(|entry| { + (entry.name_id() == name_id).then(|| entry.to_string()).flatten() + }) + } + + let family = find_name(face, name_id::TYPOGRAPHIC_FAMILY) + .or_else(|| find_name(face, name_id::FAMILY)) + .ok_or("unknown font family") + .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + let variant = FontVariant { + style: match (face.is_italic(), face.is_oblique()) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, + }, + weight: FontWeight::from_number(face.weight().to_number()), + stretch: FontStretch::from_number(face.width().to_number()), + }; + + // Merge with an existing entry for the same family name. + self.faces.push(FaceInfo { family, variant, index }); + self.files.push(path.to_owned()); + + Ok(()) + } + + /// Paths to font files, parallel to [`faces()`](Self::faces). + pub fn files(&self) -> &[PathBuf] { + &self.files + } +} + +impl Loader for FsLoader { + fn faces(&self) -> &[FaceInfo] { + &self.faces + } + + fn load_face(&mut self, idx: usize) -> Option { + load(&mut self.cache, &self.files[idx]) + } + + fn load_file(&mut self, path: &str) -> Option { + load(&mut self.cache, Path::new(path)) + } +} + +/// Load from the file system using a cache. +fn load(cache: &mut FileCache, path: &Path) -> Option { + match cache.entry(path.to_owned()) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + let buffer = std::fs::read(path).ok().map(Rc::new); + entry.insert(buffer).clone() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_font_dir() { + let mut loader = FsLoader::new(); + loader.search_path("fonts"); + + assert_eq!(loader.files, &[ + Path::new("fonts/EBGaramond-Bold.ttf"), + Path::new("fonts/EBGaramond-BoldItalic.ttf"), + Path::new("fonts/EBGaramond-Italic.ttf"), + Path::new("fonts/EBGaramond-Regular.ttf"), + Path::new("fonts/Inconsolata-Bold.ttf"), + Path::new("fonts/Inconsolata-Regular.ttf"), + Path::new("fonts/LatinModernMath.otf"), + Path::new("fonts/NotoSansArabic-Regular.ttf"), + Path::new("fonts/NotoSerifCJKsc-Regular.otf"), + Path::new("fonts/NotoSerifHebrew-Bold.ttf"), + Path::new("fonts/NotoSerifHebrew-Regular.ttf"), + Path::new("fonts/PTSans-Regular.ttf"), + Path::new("fonts/TwitterColorEmoji.ttf"), + ]); + } +} diff --git a/src/loading/mod.rs b/src/loading/mod.rs new file mode 100644 index 00000000..818e7e3c --- /dev/null +++ b/src/loading/mod.rs @@ -0,0 +1,43 @@ +//! Resource loading. + +#[cfg(feature = "fs")] +mod fs; + +#[cfg(feature = "fs")] +pub use fs::*; + +use std::rc::Rc; + +use crate::font::FaceInfo; + +/// A shared byte buffer. +pub type Buffer = Rc>; + +/// Loads resources from a local or remote source. +pub trait Loader { + /// Descriptions of all font faces this loader serves. + fn faces(&self) -> &[FaceInfo]; + + /// Load the font face with the given index in [`faces()`](Self::faces). + fn load_face(&mut self, idx: usize) -> Option; + + /// Load a file from a path. + fn load_file(&mut self, path: &str) -> Option; +} + +/// A loader which serves nothing. +pub struct BlankLoader; + +impl Loader for BlankLoader { + fn faces(&self) -> &[FaceInfo] { + &[] + } + + fn load_face(&mut self, _: usize) -> Option { + None + } + + fn load_file(&mut self, _: &str) -> Option { + None + } +} diff --git a/src/main.rs b/src/main.rs index aef0f573..5370f6a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,6 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context}; -use typst::cache::Cache; -use typst::diag::Pass; -use typst::env::{Env, FsLoader}; -use typst::exec::State; -use typst::library; -use typst::parse::LineMap; -use typst::pdf; -use typst::typeset; - fn main() -> anyhow::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() < 2 || args.len() > 3 { @@ -35,35 +26,30 @@ fn main() -> anyhow::Result<()> { let src = fs::read_to_string(src_path).context("Failed to read from source file.")?; - let mut loader = FsLoader::new(); + let mut loader = typst::loading::FsLoader::new(); loader.search_path("fonts"); loader.search_system(); - let mut env = Env::new(loader); - let mut cache = Cache::new(); - let scope = library::new(); - let state = State::default(); - - let Pass { output: frames, diags } = - typeset(&mut env, &mut cache, &src, &scope, state); - if !diags.is_empty() { - let map = LineMap::new(&src); - for diag in diags { - let start = map.location(diag.span.start).unwrap(); - let end = map.location(diag.span.end).unwrap(); - println!( - "{}: {}:{}-{}: {}", - diag.level, - src_path.display(), - start, - end, - diag.message, - ); - } + let mut cache = typst::cache::Cache::new(&loader); + let scope = typst::library::new(); + let state = typst::exec::State::default(); + let pass = typst::typeset(&mut loader, &mut cache, &src, &scope, state); + let map = typst::parse::LineMap::new(&src); + for diag in pass.diags { + let start = map.location(diag.span.start).unwrap(); + let end = map.location(diag.span.end).unwrap(); + println!( + "{}: {}:{}-{}: {}", + diag.level, + src_path.display(), + start, + end, + diag.message, + ); } - let pdf_data = pdf::export(&env, &frames); - fs::write(&dest_path, pdf_data).context("Failed to write PDF file.")?; + let buffer = typst::export::pdf(&cache, &pass.output); + fs::write(&dest_path, buffer).context("Failed to write PDF file.")?; Ok(()) } diff --git a/src/pdf/mod.rs b/src/pdf/mod.rs deleted file mode 100644 index 9b353fce..00000000 --- a/src/pdf/mod.rs +++ /dev/null @@ -1,578 +0,0 @@ -//! Exporting into PDF documents. - -use std::cmp::Eq; -use std::collections::HashMap; -use std::hash::Hash; - -use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; -use miniz_oxide::deflate; -use pdf_writer::{ - CidFontType, ColorSpace, Content, Filter, FontFlags, Name, PdfWriter, Rect, Ref, Str, - SystemInfo, UnicodeCmap, -}; -use ttf_parser::{name_id, GlyphId}; - -use crate::color::Color; -use crate::env::{Env, FaceId, Image, ImageId}; -use crate::font::{Em, VerticalFontMetric}; -use crate::geom::{self, Length, Size}; -use crate::layout::{Element, Fill, Frame, Shape}; - -/// Export a collection of frames into a PDF document. -/// -/// This creates one page per frame. In addition to the frames, you need to pass -/// in the environment used for typesetting such that things like fonts and -/// images can be included in the PDF. -/// -/// Returns the raw bytes making up the PDF document. -pub fn export(env: &Env, frames: &[Frame]) -> Vec { - PdfExporter::new(env, frames).write() -} - -struct PdfExporter<'a> { - writer: PdfWriter, - frames: &'a [Frame], - env: &'a Env, - refs: Refs, - fonts: Remapper, - images: Remapper, -} - -impl<'a> PdfExporter<'a> { - fn new(env: &'a Env, frames: &'a [Frame]) -> Self { - let mut writer = PdfWriter::new(1, 7); - writer.set_indent(2); - - let mut fonts = Remapper::new(); - let mut images = Remapper::new(); - let mut alpha_masks = 0; - - for frame in frames { - for (_, element) in &frame.elements { - match *element { - Element::Text(ref shaped) => fonts.insert(shaped.face_id), - Element::Geometry(_, _) => {} - Element::Image(id, _) => { - let img = env.image(id); - if img.buf.color().has_alpha() { - alpha_masks += 1; - } - images.insert(id); - } - } - } - } - - let refs = Refs::new(frames.len(), fonts.len(), images.len(), alpha_masks); - - Self { writer, frames, env, refs, fonts, images } - } - - fn write(mut self) -> Vec { - self.write_structure(); - self.write_pages(); - self.write_fonts(); - self.write_images(); - self.writer.finish(self.refs.catalog) - } - - fn write_structure(&mut self) { - // The document catalog. - self.writer.catalog(self.refs.catalog).pages(self.refs.page_tree); - - // The root page tree. - let mut pages = self.writer.pages(self.refs.page_tree); - pages.kids(self.refs.pages()); - - let mut resources = pages.resources(); - let mut fonts = resources.fonts(); - for (refs, f) in self.refs.fonts().zip(self.fonts.pdf_indices()) { - let name = format!("F{}", f); - fonts.pair(Name(name.as_bytes()), refs.type0_font); - } - - drop(fonts); - - let mut images = resources.x_objects(); - for (id, im) in self.refs.images().zip(self.images.pdf_indices()) { - let name = format!("Im{}", im); - images.pair(Name(name.as_bytes()), id); - } - - drop(images); - drop(resources); - drop(pages); - - // The page objects (non-root nodes in the page tree). - for ((page_id, content_id), page) in - self.refs.pages().zip(self.refs.contents()).zip(self.frames) - { - self.writer - .page(page_id) - .parent(self.refs.page_tree) - .media_box(Rect::new( - 0.0, - 0.0, - page.size.width.to_pt() as f32, - page.size.height.to_pt() as f32, - )) - .contents(content_id); - } - } - - fn write_pages(&mut self) { - for (id, page) in self.refs.contents().zip(self.frames) { - self.write_page(id, &page); - } - } - - fn write_page(&mut self, id: Ref, page: &'a Frame) { - let mut content = Content::new(); - - // We only write font switching actions when the used face changes. To - // do that, we need to remember the active face. - let mut face = FaceId::MAX; - let mut size = Length::zero(); - let mut fill: Option = None; - - for (pos, element) in &page.elements { - let x = pos.x.to_pt() as f32; - let y = (page.size.height - pos.y).to_pt() as f32; - - match *element { - Element::Text(ref shaped) => { - if fill != Some(shaped.fill) { - write_fill(&mut content, shaped.fill); - fill = Some(shaped.fill); - } - - let mut text = content.text(); - - // Then, also check if we need to issue a font switching - // action. - if shaped.face_id != face || shaped.size != size { - face = shaped.face_id; - size = shaped.size; - - let name = format!("F{}", self.fonts.map(shaped.face_id)); - text.font(Name(name.as_bytes()), size.to_pt() as f32); - } - - // TODO: Respect individual glyph offsets. - text.matrix(1.0, 0.0, 0.0, 1.0, x, y); - text.show(Str(&shaped.encode_glyphs_be())); - } - - Element::Geometry(ref shape, fill) => { - content.save_state(); - write_fill(&mut content, fill); - - match *shape { - Shape::Rect(Size { width, height }) => { - let w = width.to_pt() as f32; - let h = height.to_pt() as f32; - if w > 0.0 && h > 0.0 { - content.rect(x, y - h, w, h, false, true); - } - } - - Shape::Ellipse(size) => { - let path = geom::Path::ellipse(size); - write_path(&mut content, x, y, &path, false, true); - } - - Shape::Path(ref path) => { - write_path(&mut content, x, y, path, false, true) - } - } - - content.restore_state(); - } - - Element::Image(id, Size { width, height }) => { - let name = format!("Im{}", self.images.map(id)); - let w = width.to_pt() as f32; - let h = height.to_pt() as f32; - - content.save_state(); - content.matrix(w, 0.0, 0.0, h, x, y - h); - content.x_object(Name(name.as_bytes())); - content.restore_state(); - } - } - } - - self.writer.stream(id, &content.finish()); - } - - fn write_fonts(&mut self) { - for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) { - let face = self.env.face(face_id); - let ttf = face.ttf(); - - let name = ttf - .names() - .find(|entry| { - entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode() - }) - .and_then(|entry| entry.to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - let base_font = format!("ABCDEF+{}", name); - let base_font = Name(base_font.as_bytes()); - let cmap_name = Name(b"Custom"); - let system_info = SystemInfo { - registry: Str(b"Adobe"), - ordering: Str(b"Identity"), - supplement: 0, - }; - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - face.to_em(global_bbox.x_min).to_pdf(), - face.to_em(global_bbox.y_min).to_pdf(), - face.to_em(global_bbox.x_max).to_pdf(), - face.to_em(global_bbox.y_max).to_pdf(), - ); - - let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = face.vertical_metric(VerticalFontMetric::Ascender).to_pdf(); - let descender = face.vertical_metric(VerticalFontMetric::Descender).to_pdf(); - let cap_height = face.vertical_metric(VerticalFontMetric::CapHeight).to_pdf(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the base font object referencing the CID font. - self.writer - .type0_font(refs.type0_font) - .base_font(base_font) - .encoding_predefined(Name(b"Identity-H")) - .descendant_font(refs.cid_font) - .to_unicode(refs.cmap); - - // Write the CID font referencing the font descriptor. - self.writer - .cid_font(refs.cid_font, CidFontType::Type2) - .base_font(base_font) - .system_info(system_info) - .font_descriptor(refs.font_descriptor) - .widths() - .individual(0, { - let num_glyphs = ttf.number_of_glyphs(); - (0 .. num_glyphs).map(|g| { - let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0); - face.to_em(x).to_pdf() - }) - }); - - // Write the font descriptor (contains metrics about the font). - self.writer - .font_descriptor(refs.font_descriptor) - .font_name(base_font) - .font_flags(flags) - .font_bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v) - .font_file2(refs.data); - - // Write the to-unicode character map, which maps glyph ids back to - // unicode codepoints to enable copying out of the PDF. - self.writer - .cmap(refs.cmap, &{ - let mut cmap = UnicodeCmap::new(cmap_name, system_info); - for subtable in ttf.character_mapping_subtables() { - subtable.codepoints(|n| { - if let Some(c) = std::char::from_u32(n) { - if let Some(g) = ttf.glyph_index(c) { - cmap.pair(g.0, c); - } - } - }) - } - cmap.finish() - }) - .name(cmap_name) - .system_info(system_info); - - // Write the face's bytes. - self.writer.stream(refs.data, face.buffer()); - } - } - - fn write_images(&mut self) { - let mut masks_seen = 0; - - for (id, image_id) in self.refs.images().zip(self.images.layout_indices()) { - let img = self.env.image(image_id); - let (width, height) = img.buf.dimensions(); - - // Add the primary image. - if let Ok((data, filter, color_space)) = encode_image(img) { - let mut image = self.writer.image(id, &data); - image.filter(filter); - image.width(width as i32); - image.height(height as i32); - image.color_space(color_space); - image.bits_per_component(8); - - // Add a second gray-scale image containing the alpha values if - // this image has an alpha channel. - if img.buf.color().has_alpha() { - let (alpha_data, alpha_filter) = encode_alpha(img); - let mask_id = self.refs.alpha_mask(masks_seen); - image.s_mask(mask_id); - drop(image); - - let mut mask = self.writer.image(mask_id, &alpha_data); - mask.filter(alpha_filter); - mask.width(width as i32); - mask.height(height as i32); - mask.color_space(ColorSpace::DeviceGray); - mask.bits_per_component(8); - - masks_seen += 1; - } - } else { - // TODO: Warn that image could not be encoded. - self.writer - .image(id, &[]) - .width(0) - .height(0) - .color_space(ColorSpace::DeviceGray) - .bits_per_component(1); - } - } - } -} - -/// Write a fill change into a content stream. -fn write_fill(content: &mut Content, fill: Fill) { - match fill { - Fill::Color(Color::Rgba(c)) => { - content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0); - } - } -} - -/// Write a path into a content stream. -fn write_path( - content: &mut Content, - x: f32, - y: f32, - path: &geom::Path, - stroke: bool, - fill: bool, -) { - let f = |length: Length| length.to_pt() as f32; - let mut builder = content.path(stroke, fill); - for elem in &path.0 { - match elem { - geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)), - geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)), - geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to( - x + f(p1.x), - y + f(p1.y), - x + f(p2.x), - y + f(p2.y), - x + f(p3.x), - y + f(p3.y), - ), - geom::PathElement::ClosePath => builder.close_path(), - }; - } -} - -/// The compression level for the deflating. -const DEFLATE_LEVEL: u8 = 6; - -/// Encode an image with a suitable filter. -/// -/// Skips the alpha channel as that's encoded separately. -fn encode_image(img: &Image) -> ImageResult<(Vec, Filter, ColorSpace)> { - let mut data = vec![]; - let (filter, space) = match (img.format, &img.buf) { - // 8-bit gray JPEG. - (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { - img.buf.write_to(&mut data, img.format)?; - (Filter::DctDecode, ColorSpace::DeviceGray) - } - - // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier). - (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => { - img.buf.write_to(&mut data, img.format)?; - (Filter::DctDecode, ColorSpace::DeviceRgb) - } - - // TODO: Encode flate streams with PNG-predictor? - - // 8-bit gray PNG. - (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => { - data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL); - (Filter::FlateDecode, ColorSpace::DeviceGray) - } - - // Anything else (including Rgb(a) PNGs). - (_, buf) => { - let (width, height) = buf.dimensions(); - let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); - for (_, _, Rgba([r, g, b, _])) in buf.pixels() { - pixels.push(r); - pixels.push(g); - pixels.push(b); - } - - data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); - (Filter::FlateDecode, ColorSpace::DeviceRgb) - } - }; - Ok((data, filter, space)) -} - -/// Encode an image's alpha channel if present. -fn encode_alpha(img: &Image) -> (Vec, Filter) { - let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); - (data, Filter::FlateDecode) -} - -/// We need to know exactly which indirect reference id will be used for which -/// objects up-front to correctly declare the document catalogue, page tree and -/// so on. These offsets are computed in the beginning and stored here. -struct Refs { - catalog: Ref, - page_tree: Ref, - pages_start: i32, - contents_start: i32, - fonts_start: i32, - images_start: i32, - alpha_masks_start: i32, - end: i32, -} - -struct FontRefs { - type0_font: Ref, - cid_font: Ref, - font_descriptor: Ref, - cmap: Ref, - data: Ref, -} - -impl Refs { - const OBJECTS_PER_FONT: usize = 5; - - fn new(frames: usize, fonts: usize, images: usize, alpha_masks: usize) -> Self { - let catalog = 1; - let page_tree = catalog + 1; - let pages_start = page_tree + 1; - let contents_start = pages_start + frames as i32; - let fonts_start = contents_start + frames as i32; - let images_start = fonts_start + (Self::OBJECTS_PER_FONT * fonts) as i32; - let alpha_masks_start = images_start + images as i32; - let end = alpha_masks_start + alpha_masks as i32; - - Self { - catalog: Ref::new(catalog), - page_tree: Ref::new(page_tree), - pages_start, - contents_start, - fonts_start, - images_start, - alpha_masks_start, - end, - } - } - - fn pages(&self) -> impl Iterator { - (self.pages_start .. self.contents_start).map(Ref::new) - } - - fn contents(&self) -> impl Iterator { - (self.contents_start .. self.images_start).map(Ref::new) - } - - fn fonts(&self) -> impl Iterator { - (self.fonts_start .. self.images_start) - .step_by(Self::OBJECTS_PER_FONT) - .map(|id| FontRefs { - type0_font: Ref::new(id), - cid_font: Ref::new(id + 1), - font_descriptor: Ref::new(id + 2), - cmap: Ref::new(id + 3), - data: Ref::new(id + 4), - }) - } - - fn images(&self) -> impl Iterator { - (self.images_start .. self.end).map(Ref::new) - } - - fn alpha_mask(&self, i: usize) -> Ref { - Ref::new(self.alpha_masks_start + i as i32) - } -} - -/// Used to assign new, consecutive PDF-internal indices to things. -struct Remapper { - /// Forwards from the old indices to the new pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the old indices. - to_layout: Vec, -} - -impl Remapper -where - Index: Copy + Eq + Hash, -{ - fn new() -> Self { - Self { - to_pdf: HashMap::new(), - to_layout: vec![], - } - } - - fn len(&self) -> usize { - self.to_layout.len() - } - - fn insert(&mut self, index: Index) { - let to_layout = &mut self.to_layout; - self.to_pdf.entry(index).or_insert_with(|| { - let pdf_index = to_layout.len(); - to_layout.push(index); - pdf_index - }); - } - - fn map(&self, index: Index) -> usize { - self.to_pdf[&index] - } - - fn pdf_indices(&self) -> impl Iterator { - 0 .. self.to_pdf.len() - } - - fn layout_indices(&self) -> impl Iterator + '_ { - self.to_layout.iter().copied() - } -} - -/// Additional methods for [`Em`]. -trait EmExt { - /// Convert an em length to a number of PDF font units. - fn to_pdf(self) -> f32; -} - -impl EmExt for Em { - fn to_pdf(self) -> f32 { - 1000.0 * self.get() as f32 - } -} diff --git a/src/pretty.rs b/src/pretty.rs index acdb46a0..bf475bf6 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -604,7 +604,6 @@ mod tests { use std::rc::Rc; use super::*; - use crate::env::Env; use crate::parse::parse; #[track_caller] @@ -726,13 +725,6 @@ mod tests { roundtrip("#for k, x in y {z}"); } - #[test] - fn test_pretty_print_with_map() { - let tree = parse("*[{1+2}[{4}]]*{2+3}").output; - let map = eval(&mut Env::blank(), &tree, &Default::default()).output; - assert_eq!(pretty_with_map(&tree, &map), "*[3[4]]*5"); - } - #[test] fn test_pretty_print_value() { // Simple values. -- cgit v1.2.3