mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Parallel export (#2989)
This commit is contained in:
parent
f3c39ac84a
commit
08225e42d8
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2722,7 +2722,9 @@ dependencies = [
|
|||||||
"notify",
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
|
"parking_lot",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
|
"rayon",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"same-file",
|
"same-file",
|
||||||
|
@ -67,6 +67,7 @@ once_cell = "1"
|
|||||||
open = "5.0.1"
|
open = "5.0.1"
|
||||||
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
|
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
|
||||||
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
||||||
|
parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pdf-writer = "0.9.2"
|
pdf-writer = "0.9.2"
|
||||||
pixglyph = "0.2"
|
pixglyph = "0.2"
|
||||||
|
@ -37,7 +37,9 @@ inferno = { workspace = true }
|
|||||||
notify = { workspace = true }
|
notify = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
|
parking_lot = { workspace = true }
|
||||||
pathdiff = { workspace = true }
|
pathdiff = { workspace = true }
|
||||||
|
rayon = { workspace = true }
|
||||||
rustls = { workspace = true }
|
rustls = { workspace = true }
|
||||||
rustls-pemfile = { workspace = true }
|
rustls-pemfile = { workspace = true }
|
||||||
same-file = { workspace = true }
|
same-file = { workspace = true }
|
||||||
|
@ -4,7 +4,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use chrono::{Datelike, Timelike};
|
use chrono::{Datelike, Timelike};
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
||||||
use codespan_reporting::term::{self, termcolor};
|
use codespan_reporting::term::{self, termcolor};
|
||||||
use ecow::eco_format;
|
use ecow::{eco_format, EcoString};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
|
||||||
use termcolor::{ColorChoice, StandardStream};
|
use termcolor::{ColorChoice, StandardStream};
|
||||||
use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult};
|
use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult};
|
||||||
use typst::eval::Tracer;
|
use typst::eval::Tracer;
|
||||||
@ -214,10 +216,16 @@ fn export_image(
|
|||||||
// first page should be numbered "001" if there are between 100 and
|
// first page should be numbered "001" if there are between 100 and
|
||||||
// 999 pages.
|
// 999 pages.
|
||||||
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 cache = world.export_cache();
|
let cache = world.export_cache();
|
||||||
for (i, frame) in document.pages.iter().enumerate() {
|
|
||||||
|
// The results are collected in a `Vec<()>` which does not allocate.
|
||||||
|
document
|
||||||
|
.pages
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, frame)| {
|
||||||
|
let storage;
|
||||||
let path = if numbered {
|
let path = if numbered {
|
||||||
storage = string.replace("{n}", &format!("{:0width$}", i + 1));
|
storage = string.replace("{n}", &format!("{:0width$}", i + 1));
|
||||||
Path::new(&storage)
|
Path::new(&storage)
|
||||||
@ -229,7 +237,7 @@ fn export_image(
|
|||||||
// If the frame is in the cache, skip it.
|
// If the frame is in the cache, skip it.
|
||||||
// If the file does not exist, always create it.
|
// If the file does not exist, always create it.
|
||||||
if watching && cache.is_cached(i, frame) && path.exists() {
|
if watching && cache.is_cached(i, frame) && path.exists() {
|
||||||
continue;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match fmt {
|
match fmt {
|
||||||
@ -246,7 +254,10 @@ fn export_image(
|
|||||||
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
|
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<()>, EcoString>>()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -260,26 +271,27 @@ fn export_image(
|
|||||||
/// complexity and memory usage of such a cache.
|
/// complexity and memory usage of such a cache.
|
||||||
pub struct ExportCache {
|
pub struct ExportCache {
|
||||||
/// The hashes of last compilation's frames.
|
/// The hashes of last compilation's frames.
|
||||||
pub cache: Vec<u128>,
|
pub cache: RwLock<Vec<u128>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportCache {
|
impl ExportCache {
|
||||||
/// Creates a new export cache.
|
/// Creates a new export cache.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { cache: Vec::with_capacity(32) }
|
Self { cache: RwLock::new(Vec::with_capacity(32)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the entry is cached and appends the new hash to the
|
/// Returns true if the entry is cached and appends the new hash to the
|
||||||
/// cache (for the next compilation).
|
/// cache (for the next compilation).
|
||||||
pub fn is_cached(&mut self, i: usize, frame: &Frame) -> bool {
|
pub fn is_cached(&self, i: usize, frame: &Frame) -> bool {
|
||||||
let hash = typst::util::hash128(frame);
|
let hash = typst::util::hash128(frame);
|
||||||
|
|
||||||
if i >= self.cache.len() {
|
let mut cache = self.cache.upgradable_read();
|
||||||
self.cache.push(hash);
|
if i >= cache.len() {
|
||||||
|
cache.with_upgraded(|cache| cache.push(hash));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::mem::replace(&mut self.cache[i], hash) == hash
|
cache.with_upgraded(|cache| std::mem::replace(&mut cache[i], hash) == hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{OnceLock, RwLock};
|
use std::sync::OnceLock;
|
||||||
use std::{fs, mem};
|
use std::{fs, mem};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Local};
|
use chrono::{DateTime, Datelike, Local};
|
||||||
use comemo::Prehashed;
|
use comemo::Prehashed;
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use typst::diag::{FileError, FileResult, StrResult};
|
use typst::diag::{FileError, FileResult, StrResult};
|
||||||
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
@ -34,7 +35,7 @@ pub struct SystemWorld {
|
|||||||
/// Locations of and storage for lazily loaded fonts.
|
/// Locations of and storage for lazily loaded fonts.
|
||||||
fonts: Vec<FontSlot>,
|
fonts: Vec<FontSlot>,
|
||||||
/// Maps file ids to source files and buffers.
|
/// Maps file ids to source files and buffers.
|
||||||
slots: RwLock<HashMap<FileId, FileSlot>>,
|
slots: Mutex<HashMap<FileId, FileSlot>>,
|
||||||
/// 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: OnceLock<DateTime<Local>>,
|
now: OnceLock<DateTime<Local>>,
|
||||||
@ -89,7 +90,7 @@ impl SystemWorld {
|
|||||||
library: Prehashed::new(library),
|
library: Prehashed::new(library),
|
||||||
book: Prehashed::new(searcher.book),
|
book: Prehashed::new(searcher.book),
|
||||||
fonts: searcher.fonts,
|
fonts: searcher.fonts,
|
||||||
slots: RwLock::new(HashMap::new()),
|
slots: Mutex::new(HashMap::new()),
|
||||||
now: OnceLock::new(),
|
now: OnceLock::new(),
|
||||||
export_cache: ExportCache::new(),
|
export_cache: ExportCache::new(),
|
||||||
})
|
})
|
||||||
@ -114,7 +115,6 @@ impl SystemWorld {
|
|||||||
pub fn dependencies(&mut self) -> impl Iterator<Item = PathBuf> + '_ {
|
pub fn dependencies(&mut self) -> impl Iterator<Item = PathBuf> + '_ {
|
||||||
self.slots
|
self.slots
|
||||||
.get_mut()
|
.get_mut()
|
||||||
.unwrap()
|
|
||||||
.values()
|
.values()
|
||||||
.filter(|slot| slot.accessed())
|
.filter(|slot| slot.accessed())
|
||||||
.filter_map(|slot| system_path(&self.root, slot.id).ok())
|
.filter_map(|slot| system_path(&self.root, slot.id).ok())
|
||||||
@ -122,7 +122,7 @@ impl SystemWorld {
|
|||||||
|
|
||||||
/// Reset the compilation state in preparation of a new compilation.
|
/// Reset the compilation state in preparation of a new compilation.
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
for slot in self.slots.get_mut().unwrap().values_mut() {
|
for slot in self.slots.get_mut().values_mut() {
|
||||||
slot.reset();
|
slot.reset();
|
||||||
}
|
}
|
||||||
self.now.take();
|
self.now.take();
|
||||||
@ -140,8 +140,8 @@ impl SystemWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets access to the export cache.
|
/// Gets access to the export cache.
|
||||||
pub fn export_cache(&mut self) -> &mut ExportCache {
|
pub fn export_cache(&self) -> &ExportCache {
|
||||||
&mut self.export_cache
|
&self.export_cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ impl SystemWorld {
|
|||||||
where
|
where
|
||||||
F: FnOnce(&mut FileSlot) -> T,
|
F: FnOnce(&mut FileSlot) -> T,
|
||||||
{
|
{
|
||||||
let mut map = self.slots.write().unwrap();
|
let mut map = self.slots.lock();
|
||||||
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
|
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,6 +290,13 @@ fn deflate_memoized(content: &[u8]) -> Arc<Vec<u8>> {
|
|||||||
Arc::new(deflate(content))
|
Arc::new(deflate(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Memoized and deferred version of [`deflate`] specialized for a page's content
|
||||||
|
/// stream.
|
||||||
|
#[comemo::memoize]
|
||||||
|
fn deflate_deferred(content: Vec<u8>) -> Deferred<Vec<u8>> {
|
||||||
|
Deferred::new(move || deflate(&content))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a base64-encoded hash of the value.
|
/// Create a base64-encoded hash of the value.
|
||||||
fn hash_base64<T: Hash>(value: &T) -> String {
|
fn hash_base64<T: Hash>(value: &T) -> String {
|
||||||
base64::engine::general_purpose::STANDARD
|
base64::engine::general_purpose::STANDARD
|
||||||
|
@ -15,7 +15,7 @@ use typst::layout::{
|
|||||||
};
|
};
|
||||||
use typst::model::Destination;
|
use typst::model::Destination;
|
||||||
use typst::text::{Font, TextItem};
|
use typst::text::{Font, TextItem};
|
||||||
use typst::util::Numeric;
|
use typst::util::{Deferred, Numeric};
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
|
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
|
||||||
};
|
};
|
||||||
@ -23,7 +23,7 @@ use typst::visualize::{
|
|||||||
use crate::color::PaintEncode;
|
use crate::color::PaintEncode;
|
||||||
use crate::extg::ExtGState;
|
use crate::extg::ExtGState;
|
||||||
use crate::image::deferred_image;
|
use crate::image::deferred_image;
|
||||||
use crate::{deflate_memoized, AbsExt, EmExt, PdfContext};
|
use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
|
||||||
|
|
||||||
/// Construct page objects.
|
/// Construct page objects.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
@ -71,7 +71,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page)
|
|||||||
|
|
||||||
let page = Page {
|
let page = Page {
|
||||||
size,
|
size,
|
||||||
content: ctx.content.finish(),
|
content: deflate_deferred(ctx.content.finish()),
|
||||||
id: ctx.page_ref,
|
id: ctx.page_ref,
|
||||||
uses_opacities: ctx.uses_opacities,
|
uses_opacities: ctx.uses_opacities,
|
||||||
links: ctx.links,
|
links: ctx.links,
|
||||||
@ -198,8 +198,9 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
|
|||||||
annotations.finish();
|
annotations.finish();
|
||||||
page_writer.finish();
|
page_writer.finish();
|
||||||
|
|
||||||
let data = deflate_memoized(&page.content);
|
ctx.pdf
|
||||||
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
|
.stream(content_id, page.content.wait())
|
||||||
|
.filter(Filter::FlateDecode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the page labels.
|
/// Write the page labels.
|
||||||
@ -258,7 +259,7 @@ pub struct Page {
|
|||||||
/// The page's dimensions.
|
/// The page's dimensions.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The page's content stream.
|
/// The page's content stream.
|
||||||
pub content: Vec<u8>,
|
pub content: Deferred<Vec<u8>>,
|
||||||
/// Whether the page uses opacities.
|
/// Whether the page uses opacities.
|
||||||
pub uses_opacities: bool,
|
pub uses_opacities: bool,
|
||||||
/// Links in the PDF coordinate system.
|
/// Links in the PDF coordinate system.
|
||||||
|
@ -7,7 +7,7 @@ use typst::visualize::{Pattern, RelativeTo};
|
|||||||
|
|
||||||
use crate::color::PaintEncode;
|
use crate::color::PaintEncode;
|
||||||
use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms};
|
use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms};
|
||||||
use crate::{deflate_memoized, transform_to_array, PdfContext};
|
use crate::{transform_to_array, PdfContext};
|
||||||
|
|
||||||
/// Writes the actual patterns (tiling patterns) to the PDF.
|
/// Writes the actual patterns (tiling patterns) to the PDF.
|
||||||
/// This is performed once after writing all pages.
|
/// This is performed once after writing all pages.
|
||||||
@ -16,8 +16,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
|
|||||||
let tiling = ctx.alloc.bump();
|
let tiling = ctx.alloc.bump();
|
||||||
ctx.pattern_refs.push(tiling);
|
ctx.pattern_refs.push(tiling);
|
||||||
|
|
||||||
let content = deflate_memoized(content);
|
let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, content);
|
||||||
let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, &content);
|
|
||||||
tiling_pattern
|
tiling_pattern
|
||||||
.tiling_type(TilingType::ConstantSpacing)
|
.tiling_type(TilingType::ConstantSpacing)
|
||||||
.paint_type(PaintType::Colored)
|
.paint_type(PaintType::Colored)
|
||||||
@ -81,7 +80,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A pattern and its transform.
|
/// A pattern and its transform.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct PdfPattern {
|
pub struct PdfPattern {
|
||||||
/// The transform to apply to the gradient.
|
/// The transform to apply to the gradient.
|
||||||
pub transform: Transform,
|
pub transform: Transform,
|
||||||
@ -120,7 +119,7 @@ fn register_pattern(
|
|||||||
let pdf_pattern = PdfPattern {
|
let pdf_pattern = PdfPattern {
|
||||||
transform,
|
transform,
|
||||||
pattern: pattern.clone(),
|
pattern: pattern.clone(),
|
||||||
content: content.content,
|
content: content.content.wait().clone(),
|
||||||
resources: content.resources.into_iter().collect(),
|
resources: content.resources.into_iter().collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user