mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Cached export for incremental (#2400)
This commit is contained in:
parent
37a988af83
commit
c0dbb900e8
@ -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})"))?;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user