Refactored loading and cache architecture

This commit is contained in:
Laurenz 2021-05-28 12:44:44 +02:00
parent eabf28f081
commit 0bfee5b777
23 changed files with 558 additions and 515 deletions

View File

@ -23,7 +23,7 @@ opt-level = 2
[dependencies]
decorum = { version = "0.3.1", default-features = false, features = ["serialize-serde"] }
fxhash = "0.2.1"
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
miniz_oxide = "0.3"
pdf-writer = { path = "../pdf-writer" }
rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" }

View File

@ -1,15 +1,14 @@
use std::path::Path;
use std::rc::Rc;
use criterion::{criterion_group, criterion_main, Criterion};
use typst::cache::Cache;
use typst::env::{Env, FsLoader};
use typst::eval::eval;
use typst::exec::{exec, State};
use typst::exec::exec;
use typst::export::pdf;
use typst::layout::layout;
use typst::library;
use typst::loading::FsLoader;
use typst::parse::parse;
use typst::pdf;
use typst::typeset;
const FONT_DIR: &str = "../fonts";
@ -20,38 +19,39 @@ fn benchmarks(c: &mut Criterion) {
let mut loader = FsLoader::new();
loader.search_path(FONT_DIR);
let mut env = Env::new(loader);
let scope = library::new();
let state = State::default();
let mut cache = typst::cache::Cache::new(&loader);
let scope = typst::library::new();
let state = typst::exec::State::default();
for case in CASES {
let case = Path::new(case);
let name = case.file_stem().unwrap().to_string_lossy();
let src = std::fs::read_to_string(Path::new(TYP_DIR).join(case)).unwrap();
macro_rules! bench {
($step:literal: $($tts:tt)*) => {
c.bench_function(
&format!("{}-{}", $step, name),
|b| b.iter(|| $($tts)*)
);
($step:literal: $code:expr) => {
c.bench_function(&format!("{}-{}", $step, name), |b| {
b.iter(|| {
cache.layout.clear();
$code
});
});
};
}
// Prepare intermediate results and run warm.
let syntax_tree = parse(&src).output;
let expr_map = eval(&mut env, &syntax_tree, &scope).output;
let layout_tree = exec(&mut env, &syntax_tree, &expr_map, state.clone()).output;
let frames = layout(&mut env, &mut Cache::new(), &layout_tree);
// Prepare intermediate results, run warm and fill caches.
let src = std::fs::read_to_string(Path::new(TYP_DIR).join(case)).unwrap();
let parsed = Rc::new(parse(&src).output);
let evaluated = eval(&mut loader, &mut cache, parsed.clone(), &scope).output;
let executed = exec(&evaluated.template, state.clone()).output;
let layouted = layout(&mut loader, &mut cache, &executed);
// Bench!
bench!("parse": parse(&src));
bench!("eval": eval(&mut env, &syntax_tree, &scope));
bench!("exec": exec(&mut env, &syntax_tree, &expr_map, state.clone()));
bench!("layout": layout(&mut env, &mut Cache::new(), &layout_tree));
bench!("typeset": typeset(&mut env, &mut Cache::new(), &src, &scope, state.clone()));
bench!("pdf": pdf::export(&env, &frames));
bench!("eval": eval(&mut loader, &mut cache, parsed.clone(), &scope));
bench!("exec": exec(&evaluated.template, state.clone()));
bench!("layout": layout(&mut loader, &mut cache, &executed));
bench!("typeset": typeset(&mut loader, &mut cache, &src, &scope, state.clone()));
bench!("pdf": pdf(&cache, &layouted));
}
}

View File

@ -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>,
}

47
src/env/image.rs vendored
View File

@ -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()
}
}

243
src/env/mod.rs vendored
View File

@ -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
}
}

View File

@ -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(),
}
}

View File

@ -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)]

View File

@ -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)),

View File

@ -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()
}

5
src/export/mod.rs Normal file
View File

@ -0,0 +1,5 @@
//! Exporting into external formats.
mod pdf;
pub use pdf::*;

View File

@ -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.

View File

@ -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 {

127
src/image.rs Normal file
View File

@ -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
}
}

View File

@ -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};

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}

View File

@ -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,

43
src/loading/mod.rs Normal file
View File

@ -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
}
}

View File

@ -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(())
}

View File

@ -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.

View File

@ -15,18 +15,15 @@ use walkdir::WalkDir;
use typst::cache::Cache;
use typst::color;
use typst::diag::{Diag, DiagSet, Level, Pass};
use typst::env::{Env, FsLoader, ImageId};
use typst::diag::{Diag, DiagSet, Level};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::geom::{self, Length, Point, Sides, Size};
use typst::image::ImageId;
use typst::layout::{Element, Fill, Frame, Shape, Text};
use typst::library;
use typst::loading::FsLoader;
use typst::parse::{LineMap, Scanner};
use typst::pdf;
use typst::pretty::pretty;
use typst::syntax::{Location, Pos};
use typst::typeset;
const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref";
@ -63,10 +60,10 @@ fn main() {
println!("Running {} tests", len);
}
let mut loader = FsLoader::new();
let mut loader = typst::loading::FsLoader::new();
loader.search_path(FONT_DIR);
let mut env = Env::new(loader);
let mut cache = typst::cache::Cache::new(&loader);
let mut ok = true;
for src_path in filtered {
@ -77,7 +74,8 @@ fn main() {
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
ok &= test(
&mut env,
&mut loader,
&mut cache,
&src_path,
&png_path,
&ref_path,
@ -130,7 +128,8 @@ struct Panic {
}
fn test(
env: &mut Env,
loader: &mut FsLoader,
cache: &mut Cache,
src_path: &Path,
png_path: &Path,
ref_path: &Path,
@ -163,7 +162,7 @@ fn test(
}
} else {
let (part_ok, compare_here, part_frames) =
test_part(env, part, i, compare_ref, lines);
test_part(loader, cache, part, i, compare_ref, lines);
ok &= part_ok;
compare_ever |= compare_here;
frames.extend(part_frames);
@ -174,12 +173,12 @@ fn test(
if compare_ever {
if let Some(pdf_path) = pdf_path {
let pdf_data = pdf::export(&env, &frames);
let pdf_data = typst::export::pdf(cache, &frames);
fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap();
fs::write(pdf_path, pdf_data).unwrap();
}
let canvas = draw(&env, &frames, 2.0);
let canvas = draw(&cache, &frames, 2.0);
fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
canvas.save_png(png_path).unwrap();
@ -202,7 +201,8 @@ fn test(
}
fn test_part(
env: &mut Env,
loader: &mut FsLoader,
cache: &mut Cache,
src: &str,
i: usize,
compare_ref: bool,
@ -212,7 +212,8 @@ fn test_part(
let (local_compare_ref, ref_diags) = parse_metadata(src, &map);
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut scope = library::new();
// We hook up some extra test helpers into the global scope.
let mut scope = typst::library::new();
let panics = Rc::new(RefCell::new(vec![]));
register_helpers(&mut scope, Rc::clone(&panics));
@ -222,10 +223,9 @@ fn test_part(
state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY));
state.page.margins = Sides::splat(Some(Length::pt(10.0).into()));
let Pass { output: mut frames, diags } =
typeset(env, &mut Cache::new(), &src, &scope, state);
let mut pass = typst::typeset(loader, cache, &src, &scope, state);
if !compare_ref {
frames.clear();
pass.output.clear();
}
let mut ok = true;
@ -242,11 +242,11 @@ fn test_part(
ok = false;
}
if diags != ref_diags {
if pass.diags != ref_diags {
println!(" Subtest {} does not match expected diagnostics. ❌", i);
ok = false;
for diag in &diags {
for diag in &pass.diags {
if !ref_diags.contains(diag) {
print!(" Not annotated | ");
print_diag(diag, &map, lines);
@ -254,14 +254,14 @@ fn test_part(
}
for diag in &ref_diags {
if !diags.contains(diag) {
if !pass.diags.contains(diag) {
print!(" Not emitted | ");
print_diag(diag, &map, lines);
}
}
}
(ok, compare_ref, frames)
(ok, compare_ref, pass.output)
}
fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
@ -311,7 +311,7 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value {
let repr = pretty(args);
let repr = typst::pretty::pretty(args);
args.items.clear();
Value::template("args", move |ctx| {
let snapshot = ctx.state.clone();
@ -345,7 +345,7 @@ fn print_diag(diag: &Diag, map: &LineMap, lines: u32) {
println!("{}: {}-{}: {}", diag.level, start, end, diag.message);
}
fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
fn draw(cache: &Cache, frames: &[Frame], dpi: f32) -> Pixmap {
let pad = Length::pt(5.0);
let height = pad + frames.iter().map(|l| l.size.height + pad).sum::<Length>();
@ -393,13 +393,13 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
let ts = ts.pre_translate(x, y);
match *element {
Element::Text(ref text) => {
draw_text(&mut canvas, env, ts, text);
draw_text(&mut canvas, cache, ts, text);
}
Element::Geometry(ref shape, fill) => {
draw_geometry(&mut canvas, ts, shape, fill);
}
Element::Image(id, size) => {
draw_image(&mut canvas, env, ts, id, size);
draw_image(&mut canvas, cache, ts, id, size);
}
}
}
@ -410,8 +410,8 @@ fn draw(env: &Env, frames: &[Frame], dpi: f32) -> Pixmap {
canvas
}
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, text: &Text) {
let ttf = env.face(text.face_id).ttf();
fn draw_text(canvas: &mut Pixmap, cache: &Cache, ts: Transform, text: &Text) {
let ttf = cache.font.get(text.face_id).ttf();
let mut x = 0.0;
for glyph in &text.glyphs {
@ -480,8 +480,14 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, shape: &Shape, fill: Fill)
};
}
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, id: ImageId, size: Size) {
let img = env.image(id);
fn draw_image(
canvas: &mut Pixmap,
cache: &Cache,
ts: Transform,
id: ImageId,
size: Size,
) {
let img = cache.image.get(id);
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {