Refactored loading and cache architecture
This commit is contained in:
parent
eabf28f081
commit
0bfee5b777
@ -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" }
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
43
src/cache.rs
43
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>,
|
||||
}
|
||||
|
47
src/env/image.rs
vendored
47
src/env/image.rs
vendored
@ -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
243
src/env/mod.rs
vendored
@ -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
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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)),
|
||||
|
@ -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
5
src/export/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
//! Exporting into external formats.
|
||||
|
||||
mod pdf;
|
||||
|
||||
pub use pdf::*;
|
@ -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.
|
126
src/font.rs
126
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 {
|
||||
|
127
src/image.rs
Normal file
127
src/image.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
31
src/lib.rs
31
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)
|
||||
}
|
||||
|
@ -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
43
src/loading/mod.rs
Normal 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
|
||||
}
|
||||
}
|
52
src/main.rs
52
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(())
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user