diff options
Diffstat (limited to 'crates/typst-cli/src')
| -rw-r--r-- | crates/typst-cli/src/compile.rs | 30 | ||||
| -rw-r--r-- | crates/typst-cli/src/world.rs | 43 |
2 files changed, 68 insertions, 5 deletions
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2962355e..6a5ca21e 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -85,7 +85,7 @@ pub fn compile_once( match result { // Export the PDF / PNG. Ok(document) => { - export(&document, command)?; + export(world, &document, command, watching)?; let duration = start.elapsed(); tracing::info!("Compilation succeeded in {duration:?}"); @@ -128,10 +128,19 @@ pub fn compile_once( } /// Export into the target format. -fn export(document: &Document, command: &CompileCommand) -> StrResult<()> { +fn export( + world: &mut SystemWorld, + document: &Document, + command: &CompileCommand, + watching: bool, +) -> StrResult<()> { match command.output_format()? { - OutputFormat::Png => export_image(document, command, ImageExportFormat::Png), - OutputFormat::Svg => export_image(document, command, ImageExportFormat::Svg), + OutputFormat::Png => { + export_image(world, document, command, watching, ImageExportFormat::Png) + } + OutputFormat::Svg => { + export_image(world, document, command, watching, ImageExportFormat::Svg) + } OutputFormat::Pdf => export_pdf(document, command), } } @@ -153,8 +162,10 @@ enum ImageExportFormat { /// Export to one or multiple PNGs. fn export_image( + world: &mut SystemWorld, document: &Document, command: &CompileCommand, + watching: bool, fmt: ImageExportFormat, ) -> StrResult<()> { // Determine whether we have a `{n}` numbering. @@ -171,6 +182,7 @@ fn export_image( let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; let mut storage; + let cache = world.export_cache(); for (i, frame) in document.pages.iter().enumerate() { let path = if numbered { storage = string.replace("{n}", &format!("{:0width$}", i + 1)); @@ -178,6 +190,14 @@ fn export_image( } else { output.as_path() }; + + // If we are not watching, don't use the cache. + // If the frame is in the cache, skip it. + // If the file does not exist, always create it. + if watching && cache.is_cached(i, frame) && path.exists() { + continue; + } + match fmt { ImageExportFormat::Png => { let pixmap = @@ -188,7 +208,7 @@ fn export_image( } ImageExportFormat::Svg => { let svg = typst::export::svg(frame); - fs::write(path, svg) + fs::write(path, svg.as_bytes()) .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; } } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index bd9ef414..500b64e5 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -10,9 +10,11 @@ use filetime::FileTime; use same_file::Handle; use siphasher::sip128::{Hasher128, SipHasher13}; use typst::diag::{FileError, FileResult, StrResult}; +use typst::doc::Frame; use typst::eval::{eco_format, Bytes, Datetime, Library}; use typst::font::{Font, FontBook}; use typst::syntax::{FileId, Source, VirtualPath}; +use typst::util::hash128; use typst::World; use crate::args::SharedArgs; @@ -42,6 +44,9 @@ pub struct SystemWorld { /// The current datetime if requested. This is stored here to ensure it is /// always the same within one compilation. Reset between compilations. now: OnceCell<DateTime<Local>>, + /// The export cache, used for caching output files in `typst watch` + /// sessions. + export_cache: ExportCache, } impl SystemWorld { @@ -81,6 +86,7 @@ impl SystemWorld { hashes: RefCell::default(), slots: RefCell::default(), now: OnceCell::new(), + export_cache: ExportCache::new(), }) } @@ -122,6 +128,11 @@ impl SystemWorld { pub fn lookup(&self, id: FileId) -> Source { self.source(id).expect("file id does not point to any source file") } + + /// Gets access to the export cache. + pub fn export_cache(&mut self) -> &mut ExportCache { + &mut self.export_cache + } } impl World for SystemWorld { @@ -326,6 +337,38 @@ impl PathHash { } } +/// Caches exported files so that we can avoid re-exporting them if they haven't +/// changed. +/// +/// This is done by having a list of size `files.len()` that contains the hashes +/// of the last rendered frame in each file. If a new frame is inserted, this +/// will invalidate the rest of the cache, this is deliberate as to decrease the +/// complexity and memory usage of such a cache. +pub struct ExportCache { + /// The hashes of last compilation's frames. + pub cache: Vec<u128>, +} + +impl ExportCache { + /// Creates a new export cache. + pub fn new() -> Self { + Self { cache: Vec::with_capacity(32) } + } + + /// Returns true if the entry is cached and appends the new hash to the + /// cache (for the next compilation). + pub fn is_cached(&mut self, i: usize, frame: &Frame) -> bool { + let hash = hash128(frame); + + if i >= self.cache.len() { + self.cache.push(hash); + return false; + } + + std::mem::replace(&mut self.cache[i], hash) == hash + } +} + /// Read a file. fn read(path: &Path) -> FileResult<Vec<u8>> { let f = |e| FileError::from_io(e, path); |
