typst_library/pdf/
embed.rs

1use ecow::EcoString;
2use typst_library::foundations::Target;
3use typst_syntax::Spanned;
4
5use crate::diag::{warning, At, SourceResult};
6use crate::engine::Engine;
7use crate::foundations::{
8    elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
9};
10use crate::introspection::Locatable;
11use crate::World;
12
13/// A file that will be embedded into the output PDF.
14///
15/// This can be used to distribute additional files that are related to the PDF
16/// within it. PDF readers will display the files in a file listing.
17///
18/// Some international standards use this mechanism to embed machine-readable
19/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content
20/// of the PDF.
21///
22/// # Example
23/// ```typ
24/// #pdf.embed(
25///   "experiment.csv",
26///   relationship: "supplement",
27///   mime-type: "text/csv",
28///   description: "Raw Oxygen readings from the Arctic experiment",
29/// )
30/// ```
31///
32/// # Notes
33/// - This element is ignored if exporting to a format other than PDF.
34/// - File embeddings are not currently supported for PDF/A-2, even if the
35///   embedded file conforms to PDF/A-1 or PDF/A-2.
36#[elem(Show, Locatable)]
37pub struct EmbedElem {
38    /// The [path]($syntax/#paths) of the file to be embedded.
39    ///
40    /// Must always be specified, but is only read from if no data is provided
41    /// in the following argument.
42    #[required]
43    #[parse(
44        let Spanned { v: path, span } =
45            args.expect::<Spanned<EcoString>>("path")?;
46        let id = span.resolve_path(&path).at(span)?;
47        // The derived part is the project-relative resolved path.
48        let resolved = id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into();
49        Derived::new(path.clone(), resolved)
50    )]
51    #[borrowed]
52    pub path: Derived<EcoString, EcoString>,
53
54    /// Raw file data, optionally.
55    ///
56    /// If omitted, the data is read from the specified path.
57    #[positional]
58    // Not actually required as an argument, but always present as a field.
59    // We can't distinguish between the two at the moment.
60    #[required]
61    #[parse(
62        match args.find::<Bytes>()? {
63            Some(data) => data,
64            None => engine.world.file(id).at(span)?,
65        }
66    )]
67    pub data: Bytes,
68
69    /// The relationship of the embedded file to the document.
70    ///
71    /// Ignored if export doesn't target PDF/A-3.
72    pub relationship: Option<EmbeddedFileRelationship>,
73
74    /// The MIME type of the embedded file.
75    #[borrowed]
76    pub mime_type: Option<EcoString>,
77
78    /// A description for the embedded file.
79    #[borrowed]
80    pub description: Option<EcoString>,
81}
82
83impl Show for Packed<EmbedElem> {
84    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
85        if TargetElem::target_in(styles) == Target::Html {
86            engine
87                .sink
88                .warn(warning!(self.span(), "embed was ignored during HTML export"));
89        }
90        Ok(Content::empty())
91    }
92}
93
94/// The relationship of an embedded file with the document.
95#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
96pub enum EmbeddedFileRelationship {
97    /// The PDF document was created from the source file.
98    Source,
99    /// The file was used to derive a visual presentation in the PDF.
100    Data,
101    /// An alternative representation of the document.
102    Alternative,
103    /// Additional resources for the document.
104    Supplement,
105}