Skip to main content

typst_bundle/
export.rs

1use comemo::{Track, Tracked};
2use ecow::EcoString;
3use indexmap::IndexMap;
4use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
5use rustc_hash::FxBuildHasher;
6use typst_html::{HtmlElement, HtmlOptions};
7use typst_layout::PagedDocument;
8use typst_library::diag::{At, ParallelCollectCombinedResult, SourceResult};
9use typst_library::foundations::Bytes;
10use typst_library::introspection::Location;
11use typst_library::model::{LateLinkResolver, PagedFormat};
12use typst_pdf::PdfOptions;
13use typst_render::RenderOptions;
14use typst_svg::SvgOptions;
15use typst_syntax::{Span, VirtualPath};
16
17use crate::{Bundle, BundleDocument, BundleFile};
18
19/// A raw mapping from paths to bytes.
20pub type VirtualFs = IndexMap<VirtualPath, Bytes, FxBuildHasher>;
21
22/// Exports a bundle into a raw virtual file system.
23#[typst_macros::time(name = "export bundle")]
24pub fn export(bundle: &Bundle, options: &BundleOptions) -> SourceResult<VirtualFs> {
25    bundle
26        .files
27        .par_iter()
28        .map(|(path, file)| {
29            let data = match file {
30                BundleFile::Document(doc) => {
31                    let link_resolver =
32                        LateLinkResolver::new(Some(path), bundle.introspector.as_ref());
33                    export_document(doc, options, link_resolver.track())
34                }
35                BundleFile::Asset(bytes) => Ok(bytes.clone()),
36            };
37            data.map(|data| (path.clone(), data))
38        })
39        .collect_combined_result()
40}
41
42/// Settings for bundle export.
43#[derive(Debug, Default)]
44pub struct BundleOptions {
45    /// Options for exporting HTML documents.
46    pub html: HtmlOptions,
47    /// Options for exporting PDF documents.
48    pub pdf: PdfOptions,
49    /// Options for exporting PNG documents.
50    pub png: RenderOptions,
51    /// Options for exporting SVG documents.
52    pub svg: SvgOptions,
53}
54
55/// Exports a single document.
56fn export_document(
57    doc: &BundleDocument,
58    options: &BundleOptions,
59    link_resolver: Tracked<LateLinkResolver>,
60) -> SourceResult<Bytes> {
61    match doc {
62        BundleDocument::Paged(doc, extras) => match extras.format {
63            PagedFormat::Pdf => {
64                export_pdf(doc, &options.pdf, &extras.anchors, link_resolver)
65            }
66            PagedFormat::Png => export_png(doc, &options.png),
67            PagedFormat::Svg => {
68                export_svg(doc, &options.svg, &extras.anchors, link_resolver)
69            }
70        },
71        BundleDocument::Html(doc) => {
72            export_html(doc.root(), &options.html, link_resolver)
73        }
74    }
75}
76
77/// Exports a PDF document.
78#[comemo::memoize]
79#[typst_macros::time(name = "export pdf")]
80fn export_pdf(
81    doc: &PagedDocument,
82    options: &PdfOptions,
83    anchors: &[(Location, EcoString)],
84    link_resolver: Tracked<LateLinkResolver>,
85) -> SourceResult<Bytes> {
86    typst_pdf::pdf_in_bundle(doc, options, anchors, link_resolver).map(Bytes::new)
87}
88
89/// Exports a PNG document.
90#[comemo::memoize]
91#[typst_macros::time(name = "export png")]
92fn export_png(doc: &PagedDocument, options: &RenderOptions) -> SourceResult<Bytes> {
93    typst_render::render(&doc.pages()[0], options)
94        .encode_png()
95        .map(Bytes::new)
96        .map_err(|_| "failed to encode PNG")
97        .at(Span::detached())
98}
99
100/// Exports an SVG document.
101#[comemo::memoize]
102#[typst_macros::time(name = "export svg")]
103fn export_svg(
104    doc: &PagedDocument,
105    options: &SvgOptions,
106    anchors: &[(Location, EcoString)],
107    link_resolver: Tracked<LateLinkResolver>,
108) -> SourceResult<Bytes> {
109    let anchors = anchors
110        .iter()
111        .filter_map(|(loc, name)| {
112            // We only support a single page at the moment and all anchor
113            // location should point into it, so it's safe to extract just the
114            // point using the document's introspector.
115            let point = doc.introspector().position(*loc)?.point;
116            Some((point, name.clone()))
117        })
118        .collect::<Vec<_>>();
119    Ok(Bytes::from_string(typst_svg::svg_in_bundle(
120        &doc.pages()[0],
121        options,
122        &anchors,
123        link_resolver,
124    )))
125}
126
127/// Exports an HTML document.
128///
129/// This function takes the root element rather than the document because it
130/// doesn't need the metadata or introspector and this way, it can be memoized.
131/// Bringing the HTML introspector across the memoization boundary is a little
132/// trickier than the paged one because the HTML document is mutated after being
133/// built (for linking), which means it's not 100% derived from the document.
134#[comemo::memoize]
135#[typst_macros::time(name = "export html")]
136fn export_html(
137    root: &HtmlElement,
138    options: &HtmlOptions,
139    link_resolver: Tracked<LateLinkResolver>,
140) -> SourceResult<Bytes> {
141    typst_html::html_in_bundle(root, options, link_resolver).map(Bytes::from_string)
142}