Cached export for incremental (#2400)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-10-17 20:53:01 +02:00 committed by GitHub
parent 37a988af83
commit c0dbb900e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 10 deletions

View File

@ -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})"))?;
}
}

View File

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

View File

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

View File

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

View File

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