summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com>2023-10-17 20:53:01 +0200
committerGitHub <noreply@github.com>2023-10-17 20:53:01 +0200
commitc0dbb900e859ea3a14632a59c76174fde8b50d51 (patch)
tree762a2b8eca63b4cafb51cd1cb110c1539d73df7f
parent37a988af83e54c9351eadd2e7f8a1c32441fafb4 (diff)
Cached export for incremental (#2400)
-rw-r--r--crates/typst-cli/src/compile.rs30
-rw-r--r--crates/typst-cli/src/world.rs43
-rw-r--r--crates/typst/src/export/pdf/font.rs6
-rw-r--r--crates/typst/src/export/pdf/page.rs9
-rw-r--r--tests/src/tests.rs2
5 files changed, 80 insertions, 10 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);
diff --git a/crates/typst/src/export/pdf/font.rs b/crates/typst/src/export/pdf/font.rs
index bd7cfb31..d9992704 100644
--- a/crates/typst/src/export/pdf/font.rs
+++ b/crates/typst/src/export/pdf/font.rs
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
+use std::sync::Arc;
use ecow::{eco_format, EcoString};
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
@@ -7,7 +8,6 @@ use ttf_parser::{name_id, GlyphId, Tag};
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
use super::{deflate, EmExt, PdfContext};
-use crate::eval::Bytes;
use crate::font::Font;
use crate::util::SliceExt;
@@ -168,7 +168,7 @@ pub fn write_fonts(ctx: &mut PdfContext) {
/// - For a font with TrueType outlines, this returns the whole OpenType font.
/// - For a font with CFF outlines, this returns just the CFF font program.
#[comemo::memoize]
-fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes {
+fn subset_font(font: &Font, glyphs: &[u16]) -> Arc<Vec<u8>> {
let data = font.data();
let profile = subsetter::Profile::pdf(glyphs);
let subsetted = subsetter::subset(data, font.index(), profile);
@@ -180,7 +180,7 @@ fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes {
data = cff;
}
- deflate(data).into()
+ Arc::new(deflate(data))
}
/// Produce a unique 6 letter tag for a glyph set.
diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs
index c36bed4e..5e6cd2a2 100644
--- a/crates/typst/src/export/pdf/page.rs
+++ b/crates/typst/src/export/pdf/page.rs
@@ -1,4 +1,5 @@
use std::num::NonZeroUsize;
+use std::sync::Arc;
use ecow::{eco_format, EcoString};
use pdf_writer::types::{
@@ -184,10 +185,16 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
annotations.finish();
page_writer.finish();
- let data = deflate(&page.content);
+ let data = deflate_content(&page.content);
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
}
+/// Memoized version of [`deflate`] specialized for a page's content stream.
+#[comemo::memoize]
+fn deflate_content(content: &[u8]) -> Arc<Vec<u8>> {
+ Arc::new(deflate(content))
+}
+
/// Data for an exported page.
pub struct Page {
/// The indirect object id of the page.
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index bdbfd397..1aed32d7 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -437,7 +437,7 @@ fn test(
let svg = typst::export::svg_merged(&document.pages, Abs::pt(5.0));
fs::create_dir_all(svg_path.parent().unwrap()).unwrap();
- std::fs::write(svg_path, svg).unwrap();
+ std::fs::write(svg_path, svg.as_bytes()).unwrap();
if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
if canvas.width() != ref_pixmap.width()