mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +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 {
|
match result {
|
||||||
// Export the PDF / PNG.
|
// Export the PDF / PNG.
|
||||||
Ok(document) => {
|
Ok(document) => {
|
||||||
export(&document, command)?;
|
export(world, &document, command, watching)?;
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
||||||
tracing::info!("Compilation succeeded in {duration:?}");
|
tracing::info!("Compilation succeeded in {duration:?}");
|
||||||
@ -128,10 +128,19 @@ pub fn compile_once(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Export into the target format.
|
/// 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()? {
|
match command.output_format()? {
|
||||||
OutputFormat::Png => export_image(document, command, ImageExportFormat::Png),
|
OutputFormat::Png => {
|
||||||
OutputFormat::Svg => export_image(document, command, ImageExportFormat::Svg),
|
export_image(world, document, command, watching, ImageExportFormat::Png)
|
||||||
|
}
|
||||||
|
OutputFormat::Svg => {
|
||||||
|
export_image(world, document, command, watching, ImageExportFormat::Svg)
|
||||||
|
}
|
||||||
OutputFormat::Pdf => export_pdf(document, command),
|
OutputFormat::Pdf => export_pdf(document, command),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,8 +162,10 @@ enum ImageExportFormat {
|
|||||||
|
|
||||||
/// Export to one or multiple PNGs.
|
/// Export to one or multiple PNGs.
|
||||||
fn export_image(
|
fn export_image(
|
||||||
|
world: &mut SystemWorld,
|
||||||
document: &Document,
|
document: &Document,
|
||||||
command: &CompileCommand,
|
command: &CompileCommand,
|
||||||
|
watching: bool,
|
||||||
fmt: ImageExportFormat,
|
fmt: ImageExportFormat,
|
||||||
) -> StrResult<()> {
|
) -> StrResult<()> {
|
||||||
// Determine whether we have a `{n}` numbering.
|
// 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 width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize;
|
||||||
let mut storage;
|
let mut storage;
|
||||||
|
|
||||||
|
let cache = world.export_cache();
|
||||||
for (i, frame) in document.pages.iter().enumerate() {
|
for (i, frame) in document.pages.iter().enumerate() {
|
||||||
let path = if numbered {
|
let path = if numbered {
|
||||||
storage = string.replace("{n}", &format!("{:0width$}", i + 1));
|
storage = string.replace("{n}", &format!("{:0width$}", i + 1));
|
||||||
@ -178,6 +190,14 @@ fn export_image(
|
|||||||
} else {
|
} else {
|
||||||
output.as_path()
|
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 {
|
match fmt {
|
||||||
ImageExportFormat::Png => {
|
ImageExportFormat::Png => {
|
||||||
let pixmap =
|
let pixmap =
|
||||||
@ -188,7 +208,7 @@ fn export_image(
|
|||||||
}
|
}
|
||||||
ImageExportFormat::Svg => {
|
ImageExportFormat::Svg => {
|
||||||
let svg = typst::export::svg(frame);
|
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})"))?;
|
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,11 @@ use filetime::FileTime;
|
|||||||
use same_file::Handle;
|
use same_file::Handle;
|
||||||
use siphasher::sip128::{Hasher128, SipHasher13};
|
use siphasher::sip128::{Hasher128, SipHasher13};
|
||||||
use typst::diag::{FileError, FileResult, StrResult};
|
use typst::diag::{FileError, FileResult, StrResult};
|
||||||
|
use typst::doc::Frame;
|
||||||
use typst::eval::{eco_format, Bytes, Datetime, Library};
|
use typst::eval::{eco_format, Bytes, Datetime, Library};
|
||||||
use typst::font::{Font, FontBook};
|
use typst::font::{Font, FontBook};
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
|
use typst::util::hash128;
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
|
||||||
use crate::args::SharedArgs;
|
use crate::args::SharedArgs;
|
||||||
@ -42,6 +44,9 @@ pub struct SystemWorld {
|
|||||||
/// The current datetime if requested. This is stored here to ensure it is
|
/// The current datetime if requested. This is stored here to ensure it is
|
||||||
/// always the same within one compilation. Reset between compilations.
|
/// always the same within one compilation. Reset between compilations.
|
||||||
now: OnceCell<DateTime<Local>>,
|
now: OnceCell<DateTime<Local>>,
|
||||||
|
/// The export cache, used for caching output files in `typst watch`
|
||||||
|
/// sessions.
|
||||||
|
export_cache: ExportCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemWorld {
|
impl SystemWorld {
|
||||||
@ -81,6 +86,7 @@ impl SystemWorld {
|
|||||||
hashes: RefCell::default(),
|
hashes: RefCell::default(),
|
||||||
slots: RefCell::default(),
|
slots: RefCell::default(),
|
||||||
now: OnceCell::new(),
|
now: OnceCell::new(),
|
||||||
|
export_cache: ExportCache::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +128,11 @@ impl SystemWorld {
|
|||||||
pub fn lookup(&self, id: FileId) -> Source {
|
pub fn lookup(&self, id: FileId) -> Source {
|
||||||
self.source(id).expect("file id does not point to any source file")
|
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 {
|
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.
|
/// Read a file.
|
||||||
fn read(path: &Path) -> FileResult<Vec<u8>> {
|
fn read(path: &Path) -> FileResult<Vec<u8>> {
|
||||||
let f = |e| FileError::from_io(e, path);
|
let f = |e| FileError::from_io(e, path);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
|
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 unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
|
||||||
|
|
||||||
use super::{deflate, EmExt, PdfContext};
|
use super::{deflate, EmExt, PdfContext};
|
||||||
use crate::eval::Bytes;
|
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::util::SliceExt;
|
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 TrueType outlines, this returns the whole OpenType font.
|
||||||
/// - For a font with CFF outlines, this returns just the CFF font program.
|
/// - For a font with CFF outlines, this returns just the CFF font program.
|
||||||
#[comemo::memoize]
|
#[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 data = font.data();
|
||||||
let profile = subsetter::Profile::pdf(glyphs);
|
let profile = subsetter::Profile::pdf(glyphs);
|
||||||
let subsetted = subsetter::subset(data, font.index(), profile);
|
let subsetted = subsetter::subset(data, font.index(), profile);
|
||||||
@ -180,7 +180,7 @@ fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes {
|
|||||||
data = cff;
|
data = cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
deflate(data).into()
|
Arc::new(deflate(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produce a unique 6 letter tag for a glyph set.
|
/// Produce a unique 6 letter tag for a glyph set.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::{
|
use pdf_writer::types::{
|
||||||
@ -184,10 +185,16 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
|
|||||||
annotations.finish();
|
annotations.finish();
|
||||||
page_writer.finish();
|
page_writer.finish();
|
||||||
|
|
||||||
let data = deflate(&page.content);
|
let data = deflate_content(&page.content);
|
||||||
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
|
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.
|
/// Data for an exported page.
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
/// The indirect object id of the 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));
|
let svg = typst::export::svg_merged(&document.pages, Abs::pt(5.0));
|
||||||
fs::create_dir_all(svg_path.parent().unwrap()).unwrap();
|
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 let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
|
||||||
if canvas.width() != ref_pixmap.width()
|
if canvas.width() != ref_pixmap.width()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user