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}