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};
19
20use ecow::eco_format;
21use krilla::configure::Validator;
22use serde::{Deserialize, Serialize};
23use typst_library::diag::{SourceResult, StrResult, bail};
24use typst_library::foundations::Smart;
25use typst_library::layout::{PageRanges, PagedDocument};
26
27/// Export a document into a PDF file.
28///
29/// Returns the raw bytes making up the PDF file.
30#[typst_macros::time(name = "pdf")]
31pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u8>> {
32    convert::convert(document, options)
33}
34
35/// Generate the document tag tree and display it in a human readable form.
36#[doc(hidden)]
37pub fn pdf_tags(document: &PagedDocument, options: &PdfOptions) -> SourceResult<String> {
38    convert::tag_tree(document, options)
39}
40
41/// Settings for PDF export.
42#[derive(Debug)]
43pub struct PdfOptions<'a> {
44    /// If not `Smart::Auto`, shall be a string that uniquely and stably
45    /// identifies the document. It should not change between compilations of
46    /// the same document.  **If you cannot provide such a stable identifier,
47    /// just pass `Smart::Auto` rather than trying to come up with one.** The
48    /// CLI, for example, does not have a well-defined notion of a long-lived
49    /// project and as such just passes `Smart::Auto`.
50    ///
51    /// If an `ident` is given, the hash of it will be used to create a PDF
52    /// document identifier (the identifier itself is not leaked). If `ident` is
53    /// `Auto`, a hash of the document's title and author is used instead (which
54    /// is reasonably unique and stable).
55    pub ident: Smart<&'a str>,
56    /// If not `None`, shall be the creation timestamp of the document. It will
57    /// only be used if `set document(date: ..)` is `auto`.
58    pub timestamp: Option<Timestamp>,
59    /// Specifies which ranges of pages should be exported in the PDF. When
60    /// `None`, all pages should be exported.
61    pub page_ranges: Option<PageRanges>,
62    /// A list of PDF standards that Typst will enforce conformance with.
63    pub standards: PdfStandards,
64    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
65    /// document is written to provide a baseline of accessibility. In some
66    /// circumstances, for example when trying to reduce the size of a document,
67    /// it can be desirable to disable tagged PDF.
68    pub tagged: bool,
69}
70
71impl PdfOptions<'_> {
72    /// Whether the current export mode is PDF/UA-1, and in the future maybe
73    /// PDF/UA-2.
74    pub(crate) fn is_pdf_ua(&self) -> bool {
75        self.standards.config.validator() == Validator::UA1
76    }
77}
78
79impl Default for PdfOptions<'_> {
80    fn default() -> Self {
81        Self {
82            ident: Smart::Auto,
83            timestamp: None,
84            page_ranges: None,
85            standards: PdfStandards::default(),
86            tagged: true,
87        }
88    }
89}
90
91/// Encapsulates a list of compatible PDF standards.
92#[derive(Clone)]
93pub struct PdfStandards {
94    pub(crate) config: krilla::configure::Configuration,
95}
96
97impl PdfStandards {
98    /// Validates a list of PDF standards for compatibility and returns their
99    /// encapsulated representation.
100    pub fn new(list: &[PdfStandard]) -> StrResult<Self> {
101        use krilla::configure::{Configuration, PdfVersion, Validator};
102
103        let mut version: Option<PdfVersion> = None;
104        let mut set_version = |v: PdfVersion| -> StrResult<()> {
105            if let Some(prev) = version {
106                bail!(
107                    "PDF cannot conform to {} and {} at the same time",
108                    prev.as_str(),
109                    v.as_str()
110                );
111            }
112            version = Some(v);
113            Ok(())
114        };
115
116        let mut validator = None;
117        let mut set_validator = |v: Validator| -> StrResult<()> {
118            if validator.is_some() {
119                bail!("Typst currently only supports one PDF substandard at a time");
120            }
121            validator = Some(v);
122            Ok(())
123        };
124
125        for standard in list {
126            match standard {
127                PdfStandard::V_1_4 => set_version(PdfVersion::Pdf14)?,
128                PdfStandard::V_1_5 => set_version(PdfVersion::Pdf15)?,
129                PdfStandard::V_1_6 => set_version(PdfVersion::Pdf16)?,
130                PdfStandard::V_1_7 => set_version(PdfVersion::Pdf17)?,
131                PdfStandard::V_2_0 => set_version(PdfVersion::Pdf20)?,
132                PdfStandard::A_1b => set_validator(Validator::A1_B)?,
133                PdfStandard::A_1a => set_validator(Validator::A1_A)?,
134                PdfStandard::A_2b => set_validator(Validator::A2_B)?,
135                PdfStandard::A_2u => set_validator(Validator::A2_U)?,
136                PdfStandard::A_2a => set_validator(Validator::A2_A)?,
137                PdfStandard::A_3b => set_validator(Validator::A3_B)?,
138                PdfStandard::A_3u => set_validator(Validator::A3_U)?,
139                PdfStandard::A_3a => set_validator(Validator::A3_A)?,
140                PdfStandard::A_4 => set_validator(Validator::A4)?,
141                PdfStandard::A_4f => set_validator(Validator::A4F)?,
142                PdfStandard::A_4e => set_validator(Validator::A4E)?,
143                PdfStandard::Ua_1 => set_validator(Validator::UA1)?,
144            }
145        }
146
147        let config = match (version, validator) {
148            (Some(version), Some(validator)) => {
149                Configuration::new_with(validator, version).ok_or_else(|| {
150                    eco_format!(
151                        "{} is not compatible with {}",
152                        version.as_str(),
153                        validator.as_str()
154                    )
155                })?
156            }
157            (Some(version), None) => Configuration::new_with_version(version),
158            (None, Some(validator)) => Configuration::new_with_validator(validator),
159            (None, None) => Configuration::new_with_version(PdfVersion::Pdf17),
160        };
161
162        Ok(Self { config })
163    }
164}
165
166impl Debug for PdfStandards {
167    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
168        f.pad("PdfStandards(..)")
169    }
170}
171
172impl Default for PdfStandards {
173    fn default() -> Self {
174        use krilla::configure::{Configuration, PdfVersion};
175        Self {
176            config: Configuration::new_with_version(PdfVersion::Pdf17),
177        }
178    }
179}
180
181/// A PDF standard that Typst can enforce conformance with.
182///
183/// Support for more standards is planned.
184#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
185#[allow(non_camel_case_types)]
186#[non_exhaustive]
187pub enum PdfStandard {
188    /// PDF 1.4.
189    #[serde(rename = "1.4")]
190    V_1_4,
191    /// PDF 1.5.
192    #[serde(rename = "1.5")]
193    V_1_5,
194    /// PDF 1.5.
195    #[serde(rename = "1.6")]
196    V_1_6,
197    /// PDF 1.7.
198    #[serde(rename = "1.7")]
199    V_1_7,
200    /// PDF 2.0.
201    #[serde(rename = "2.0")]
202    V_2_0,
203    /// PDF/A-1b.
204    #[serde(rename = "a-1b")]
205    A_1b,
206    /// PDF/A-1a.
207    #[serde(rename = "a-1a")]
208    A_1a,
209    /// PDF/A-2b.
210    #[serde(rename = "a-2b")]
211    A_2b,
212    /// PDF/A-2u.
213    #[serde(rename = "a-2u")]
214    A_2u,
215    /// PDF/A-2a.
216    #[serde(rename = "a-2a")]
217    A_2a,
218    /// PDF/A-3b.
219    #[serde(rename = "a-3b")]
220    A_3b,
221    /// PDF/A-3u.
222    #[serde(rename = "a-3u")]
223    A_3u,
224    /// PDF/A-3a.
225    #[serde(rename = "a-3a")]
226    A_3a,
227    /// PDF/A-4.
228    #[serde(rename = "a-4")]
229    A_4,
230    /// PDF/A-4f.
231    #[serde(rename = "a-4f")]
232    A_4f,
233    /// PDF/A-4e.
234    #[serde(rename = "a-4e")]
235    A_4e,
236    /// PDF/UA-1.
237    #[serde(rename = "ua-1")]
238    Ua_1,
239}