summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-05-28 12:44:44 +0200
committerLaurenz <laurmaedje@gmail.com>2021-05-28 12:46:43 +0200
commit0bfee5b7772338fd39bbf708d3e31ea7bcec859b (patch)
tree5f76c7d0529d6c089e8e3383356692dfce09cffb /src
parenteabf28f08187bd9a10bbadbbaf9617e2bc1949aa (diff)
Refactored loading and cache architecture
Diffstat (limited to 'src')
-rw-r--r--src/cache.rs43
-rw-r--r--src/env/image.rs47
-rw-r--r--src/env/mod.rs243
-rw-r--r--src/eval/mod.rs55
-rw-r--r--src/eval/value.rs14
-rw-r--r--src/exec/context.rs10
-rw-r--r--src/exec/mod.rs19
-rw-r--r--src/export/mod.rs5
-rw-r--r--src/export/pdf.rs (renamed from src/pdf/mod.rs)32
-rw-r--r--src/font.rs126
-rw-r--r--src/image.rs127
-rw-r--r--src/layout/frame.rs3
-rw-r--r--src/layout/mod.rs47
-rw-r--r--src/layout/shaping.rs25
-rw-r--r--src/lib.rs31
-rw-r--r--src/library/image.rs25
-rw-r--r--src/loading/fs.rs (renamed from src/env/fs.rs)0
-rw-r--r--src/loading/mod.rs43
-rw-r--r--src/main.rs52
-rw-r--r--src/pretty.rs8
20 files changed, 496 insertions, 459 deletions
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<u64, FramesEntry>,
+ /// 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<Frame>,
-}
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<Self> {
- 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<dyn Loader>,
- /// Faces indexed by [`FaceId`]. `None` if not yet loaded.
- faces: Vec<Option<Face>>,
- /// Maps a family name to the ids of all faces that are part of the family.
- families: HashMap<String, Vec<FaceId>>,
- /// Loaded images indexed by [`ImageId`].
- images: Vec<Image>,
- /// Maps from paths to loaded images.
- paths: HashMap<String, ImageId>,
- /// Callback for loaded font faces.
- on_face_load: Option<Box<dyn Fn(FaceId, &Face)>>,
- /// Callback for loaded images.
- on_image_load: Option<Box<dyn Fn(ImageId, &Image)>>,
-}
-
-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::<String, Vec<FaceId>>::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<Buffer> {
- None
- }
-
- fn load_file(&mut self, _: &str) -> Option<Buffer> {
- 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<FaceId> {
- // 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<F>(&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<ImageId> {
- 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<F>(&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<Buffer>;
-
- /// Load a file from a path.
- fn load_file(&mut self, path: &str) -> Option<Buffer>;
-}
-
-/// A shared byte buffer.
-pub type Buffer = Rc<Vec<u8>>;
-
-/// 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<NodeMap> {
- let mut ctx = EvalContext::new(env, scope);
+pub fn eval(
+ loader: &mut dyn Loader,
+ cache: &mut Cache,
+ tree: Rc<Tree>,
+ base: &Scope,
+) -> Pass<Module> {
+ 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<layout::Tree> {
- let mut ctx = ExecContext::new(env, state);
- tree.exec_with_map(&mut ctx, &map);
+pub fn exec(template: &TemplateValue, state: State) -> Pass<layout::Tree> {
+ 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/pdf/mod.rs b/src/export/pdf.rs
index 9b353fce..0ca4df38 100644
--- a/src/pdf/mod.rs
+++ b/src/export/pdf.rs
@@ -12,34 +12,35 @@ use pdf_writer::{
};
use ttf_parser::{name_id, GlyphId};
+use crate::cache::Cache;
use crate::color::Color;
-use crate::env::{Env, FaceId, Image, ImageId};
-use crate::font::{Em, VerticalFontMetric};
+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 environment used for typesetting such that things like fonts and
-/// images can be included in the PDF.
+/// 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 export(env: &Env, frames: &[Frame]) -> Vec<u8> {
- PdfExporter::new(env, frames).write()
+pub fn pdf(cache: &Cache, frames: &[Frame]) -> Vec<u8> {
+ PdfExporter::new(cache, frames).write()
}
struct PdfExporter<'a> {
writer: PdfWriter,
frames: &'a [Frame],
- env: &'a Env,
+ cache: &'a Cache,
refs: Refs,
fonts: Remapper<FaceId>,
images: Remapper<ImageId>,
}
impl<'a> PdfExporter<'a> {
- fn new(env: &'a Env, frames: &'a [Frame]) -> Self {
+ fn new(cache: &'a Cache, frames: &'a [Frame]) -> Self {
let mut writer = PdfWriter::new(1, 7);
writer.set_indent(2);
@@ -53,7 +54,7 @@ impl<'a> PdfExporter<'a> {
Element::Text(ref shaped) => fonts.insert(shaped.face_id),
Element::Geometry(_, _) => {}
Element::Image(id, _) => {
- let img = env.image(id);
+ let img = cache.image.get(id);
if img.buf.color().has_alpha() {
alpha_masks += 1;
}
@@ -65,7 +66,14 @@ impl<'a> PdfExporter<'a> {
let refs = Refs::new(frames.len(), fonts.len(), images.len(), alpha_masks);
- Self { writer, frames, env, refs, fonts, images }
+ Self {
+ writer,
+ frames,
+ cache,
+ refs,
+ fonts,
+ images,
+ }
}
fn write(mut self) -> Vec<u8> {
@@ -207,7 +215,7 @@ impl<'a> PdfExporter<'a> {
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 face = self.cache.font.get(face_id);
let ttf = face.ttf();
let name = ttf
@@ -312,7 +320,7 @@ impl<'a> PdfExporter<'a> {
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 img = self.cache.image.get(image_id);
let (width, height) = img.buf.dimensions();
// Add the primary image.
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<Option<Face>>,
+ families: HashMap<String, Vec<FaceId>>,
+ on_load: Option<Box<dyn Fn(FaceId, &Face)>>,
+}
+
+impl FontCache {
+ /// Create a new, empty font cache.
+ pub fn new(loader: &dyn Loader) -> Self {
+ let mut faces = vec![];
+ let mut families = HashMap::<String, Vec<FaceId>>::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<FaceId> {
+ // 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<F>(&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<Self> {
+ 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<Image>,
+ /// Maps from paths to loaded images.
+ paths: HashMap<String, ImageId>,
+ /// Callback for loaded images.
+ on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
+}
+
+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<ImageId> {
+ 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<F>(&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<Frame> {
- tree.layout(&mut LayoutContext { env, cache })
+pub fn layout(loader: &mut dyn Loader, cache: &mut Cache, tree: &Tree) -> Vec<Frame> {
+ 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<Frame> {
- 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<u64, FramesEntry>,
+}
+
+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<Frame>,
+}
+
/// 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<Vec<Frame>> {
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/env/fs.rs b/src/loading/fs.rs
index 969ee9e0..969ee9e0 100644
--- a/src/env/fs.rs
+++ b/src/loading/fs.rs
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<Vec<u8>>;
+
+/// 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<Buffer>;
+
+ /// Load a file from a path.
+ fn load_file(&mut self, path: &str) -> Option<Buffer>;
+}
+
+/// A loader which serves nothing.
+pub struct BlankLoader;
+
+impl Loader for BlankLoader {
+ fn faces(&self) -> &[FaceInfo] {
+ &[]
+ }
+
+ fn load_face(&mut self, _: usize) -> Option<Buffer> {
+ None
+ }
+
+ fn load_file(&mut self, _: &str) -> Option<Buffer> {
+ 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/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]
@@ -727,13 +726,6 @@ mod tests {
}
#[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.
test_value(Value::None, "none");