Skip to main content

typst_pdf/
lib.rs

1//! Exporting Typst documents to PDF.
2
3mod attach;
4mod convert;
5mod image;
6mod link;
7mod metadata;
8mod outline;
9mod page;
10mod paint;
11mod shape;
12mod tags;
13mod text;
14mod util;
15
16pub use self::metadata::{Timestamp, Timezone};
17
18use std::fmt::{self, Debug, Formatter};
19use std::hash::{Hash, Hasher};
20
21use comemo::Tracked;
22use ecow::{EcoString, eco_format};
23use krilla::configure::Accessibility;
24use serde::{Deserialize, Serialize};
25use typst_layout::PagedDocument;
26use typst_library::diag::{HintedStrResult, HintedString, SourceResult, StrResult, bail};
27use typst_library::foundations::Smart;
28use typst_library::introspection::Location;
29use typst_library::layout::PageRanges;
30use typst_library::model::LateLinkResolver;
31
32/// Export a document into a PDF file.
33///
34/// Returns the raw bytes making up the PDF file.
35#[typst_macros::time(name = "pdf")]
36pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u8>> {
37    convert::convert(document, options, &[], None)
38}
39
40/// Export a document into a PDF file as part of a bundle.
41///
42/// Takes additional `anchor` locations that will be serialized as named
43/// destinations. This enables other documents in the bundle to link into the
44/// resulting PDF. Also takes a `link_resolver` for resolving cross-document
45/// links.
46#[typst_macros::time(name = "pdf in bundle")]
47pub fn pdf_in_bundle(
48    document: &PagedDocument,
49    options: &PdfOptions,
50    anchors: &[(Location, EcoString)],
51    link_resolver: Tracked<LateLinkResolver>,
52) -> SourceResult<Vec<u8>> {
53    convert::convert(document, options, anchors, Some(link_resolver))
54}
55
56/// Settings for PDF export.
57#[derive(Debug, Hash)]
58pub struct PdfOptions {
59    /// If not `Smart::Auto`, shall be a string that uniquely and stably
60    /// identifies the document. It should not change between compilations of
61    /// the same document.  **If you cannot provide such a stable identifier,
62    /// just pass `Smart::Auto` rather than trying to come up with one.** The
63    /// CLI, for example, does not have a well-defined notion of a long-lived
64    /// project and as such just passes `Smart::Auto`.
65    ///
66    /// If an `ident` is given, the hash of it will be used to create a PDF
67    /// document identifier (the identifier itself is not leaked). If `ident` is
68    /// `Auto`, a hash of the document's title and author is used instead (which
69    /// is reasonably unique and stable).
70    pub ident: Smart<String>,
71    /// Configures the `/Creator` metadata in the resulting PDF. When set to
72    /// `Smart::Auto`, defaults to `Typst $version`.
73    pub creator: Smart<Option<String>>,
74    /// If not `None`, shall be the creation timestamp of the document. It will
75    /// only be used if `set document(date: ..)` is `auto`.
76    pub timestamp: Option<Timestamp>,
77    /// Specifies which ranges of pages should be exported in the PDF. When
78    /// `None`, all pages should be exported.
79    pub page_ranges: Option<PageRanges>,
80    /// A list of PDF standards that Typst will enforce conformance with.
81    pub standards: PdfStandards,
82    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
83    /// document is written to provide a baseline of accessibility. In some
84    /// circumstances, for example when trying to reduce the size of a document,
85    /// it can be desirable to disable tagged PDF.
86    pub tagged: bool,
87    /// Whether to format the PDF in a human-readable way.
88    pub pretty: bool,
89}
90
91impl PdfOptions {
92    /// Returns the accessibility validator. Returns `Some` for PDF/UA-1, and in
93    /// the future maybe PDF/UA-2.
94    pub(crate) fn accessibility_validator(&self) -> Option<Accessibility> {
95        self.standards.config.validators().accessibility()
96    }
97}
98
99impl Default for PdfOptions {
100    fn default() -> Self {
101        Self {
102            ident: Smart::Auto,
103            creator: Smart::Auto,
104            timestamp: None,
105            page_ranges: None,
106            standards: PdfStandards::default(),
107            tagged: true,
108            pretty: false,
109        }
110    }
111}
112
113/// Encapsulates a list of compatible PDF standards.
114#[derive(Clone)]
115pub struct PdfStandards {
116    pub(crate) config: krilla::configure::Configuration,
117}
118
119impl PdfStandards {
120    /// Validates a list of PDF standards for compatibility and returns their
121    /// encapsulated representation.
122    pub fn new(list: &[PdfStandard]) -> HintedStrResult<Self> {
123        use krilla::configure::{
124            Accessibility, Archival, ConfigurationBuilder, ConfigurationError, PdfVersion,
125        };
126
127        use crate::util::ValidatorsExt;
128
129        let mut version: Option<PdfVersion> = None;
130        let mut set_version = |v: PdfVersion| -> StrResult<()> {
131            if let Some(prev) = version {
132                bail!(
133                    "PDF cannot conform to {} and {} at the same time",
134                    prev.as_str(),
135                    v.as_str(),
136                );
137            }
138            version = Some(v);
139            Ok(())
140        };
141
142        let mut archival_validator = None;
143        let mut set_archival_validator = |a: Archival| -> StrResult<()> {
144            if archival_validator.is_some() {
145                bail!("choose at most one PDF/A standard");
146            }
147            archival_validator = Some(a);
148            Ok(())
149        };
150
151        let mut accessibility_validator = None;
152        let mut set_accessibility_validator = |ua: Accessibility| -> StrResult<()> {
153            if accessibility_validator.is_some() {
154                bail!("choose at most one PDF/UA standard");
155            }
156            accessibility_validator = Some(ua);
157            Ok(())
158        };
159
160        for standard in list {
161            match standard {
162                PdfStandard::V_1_4 => set_version(PdfVersion::Pdf14)?,
163                PdfStandard::V_1_5 => set_version(PdfVersion::Pdf15)?,
164                PdfStandard::V_1_6 => set_version(PdfVersion::Pdf16)?,
165                PdfStandard::V_1_7 => set_version(PdfVersion::Pdf17)?,
166                PdfStandard::V_2_0 => set_version(PdfVersion::Pdf20)?,
167                PdfStandard::A_1b => set_archival_validator(Archival::A1_B)?,
168                PdfStandard::A_1a => set_archival_validator(Archival::A1_A)?,
169                PdfStandard::A_2b => set_archival_validator(Archival::A2_B)?,
170                PdfStandard::A_2u => set_archival_validator(Archival::A2_U)?,
171                PdfStandard::A_2a => set_archival_validator(Archival::A2_A)?,
172                PdfStandard::A_3b => set_archival_validator(Archival::A3_B)?,
173                PdfStandard::A_3u => set_archival_validator(Archival::A3_U)?,
174                PdfStandard::A_3a => set_archival_validator(Archival::A3_A)?,
175                PdfStandard::A_4 => set_archival_validator(Archival::A4)?,
176                PdfStandard::A_4f => set_archival_validator(Archival::A4F)?,
177                PdfStandard::A_4e => set_archival_validator(Archival::A4E)?,
178                PdfStandard::Ua_1 => set_accessibility_validator(Accessibility::UA1)?,
179            }
180        }
181
182        let mut builder = ConfigurationBuilder::new();
183
184        if let Some(version) = version {
185            builder = builder.with_version(version)
186        }
187
188        if let Some(archival_validator) = archival_validator {
189            builder = builder.with_archival_validator(archival_validator)
190        }
191
192        if let Some(accessibility_validator) = accessibility_validator {
193            builder = builder.with_accessibility_validator(accessibility_validator)
194        }
195
196        let config = builder.finish().map_err(|e| {
197            let (message, validators) = match e {
198                ConfigurationError::NoOverlappingValidatorsRange(validators) => {
199                    let list = validators.to_and_list();
200                    let message = eco_format!(
201                        "{list} are mutually incompatible because \
202                         they do not have any overlapping PDF versions"
203                    );
204                    (message, validators)
205                }
206                ConfigurationError::VersionDoesNotMatchValidatorsRange(
207                    version,
208                    validators,
209                ) => {
210                    let list = validators.to_and_list();
211                    let message =
212                        eco_format!("{} is not compatible with {list}", version.as_str());
213                    (message, validators)
214                }
215            };
216            HintedString::new(message)
217                .with_hints(validators.into_iter().map(version_hint))
218        })?;
219
220        Ok(Self { config })
221    }
222}
223
224/// A hint specifying which PDF version a validator is compatible with.
225fn version_hint(validator: krilla::configure::Validator) -> EcoString {
226    let min = validator.min();
227    let max = validator.max();
228    if let Some(min) = min {
229        if min == max {
230            eco_format!("{} requires version {}", validator.as_str(), min.as_str())
231        } else {
232            eco_format!(
233                "{} requires a version between {} and {}",
234                validator.as_str(),
235                min.as_str(),
236                max.as_str()
237            )
238        }
239    } else {
240        eco_format!("{} requires at least {}", validator.as_str(), max.as_str())
241    }
242}
243
244impl Debug for PdfStandards {
245    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
246        f.pad("PdfStandards(..)")
247    }
248}
249
250impl Default for PdfStandards {
251    fn default() -> Self {
252        use krilla::configure::{ConfigurationBuilder, PdfVersion};
253        Self {
254            config: ConfigurationBuilder::new()
255                .with_version(PdfVersion::Pdf17)
256                .finish()
257                .unwrap(),
258        }
259    }
260}
261
262// Could be turned into a derive if krilla's `Configuration` ever implements
263// `Hash`.
264impl Hash for PdfStandards {
265    fn hash<H: Hasher>(&self, state: &mut H) {
266        (self.config.version() as usize).hash(state);
267        for validator in self.config.validators() {
268            validator.hash(state);
269        }
270    }
271}
272
273/// A PDF standard that Typst can enforce conformance with.
274///
275/// Support for more standards is planned.
276#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
277#[allow(non_camel_case_types)]
278#[non_exhaustive]
279pub enum PdfStandard {
280    /// PDF 1.4.
281    #[serde(rename = "1.4")]
282    V_1_4,
283    /// PDF 1.5.
284    #[serde(rename = "1.5")]
285    V_1_5,
286    /// PDF 1.5.
287    #[serde(rename = "1.6")]
288    V_1_6,
289    /// PDF 1.7.
290    #[serde(rename = "1.7")]
291    V_1_7,
292    /// PDF 2.0.
293    #[serde(rename = "2.0")]
294    V_2_0,
295    /// PDF/A-1b.
296    #[serde(rename = "a-1b")]
297    A_1b,
298    /// PDF/A-1a.
299    #[serde(rename = "a-1a")]
300    A_1a,
301    /// PDF/A-2b.
302    #[serde(rename = "a-2b")]
303    A_2b,
304    /// PDF/A-2u.
305    #[serde(rename = "a-2u")]
306    A_2u,
307    /// PDF/A-2a.
308    #[serde(rename = "a-2a")]
309    A_2a,
310    /// PDF/A-3b.
311    #[serde(rename = "a-3b")]
312    A_3b,
313    /// PDF/A-3u.
314    #[serde(rename = "a-3u")]
315    A_3u,
316    /// PDF/A-3a.
317    #[serde(rename = "a-3a")]
318    A_3a,
319    /// PDF/A-4.
320    #[serde(rename = "a-4")]
321    A_4,
322    /// PDF/A-4f.
323    #[serde(rename = "a-4f")]
324    A_4f,
325    /// PDF/A-4e.
326    #[serde(rename = "a-4e")]
327    A_4e,
328    /// PDF/UA-1.
329    #[serde(rename = "ua-1")]
330    Ua_1,
331}