Rename pdf.embed to pdf.attach (#6705)

This commit is contained in:
Laurenz 2025-08-07 12:19:47 +02:00 committed by GitHub
parent 1790a27de8
commit c49b9640a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 98 additions and 69 deletions

View File

@ -6,8 +6,8 @@ use ecow::{EcoString, eco_format};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst::foundations::{ use typst::foundations::{
AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, StyleChain, Styles, AutoValue, CastInfo, Func, Label, NativeElement, NoneValue, ParamInfo, Repr,
Type, Value, fields_on, repr, StyleChain, Styles, Type, Value, fields_on, repr,
}; };
use typst::layout::{Alignment, Dir, PagedDocument}; use typst::layout::{Alignment, Dir, PagedDocument};
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
@ -852,6 +852,7 @@ fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static
(Some("raw"), "syntaxes") => &["sublime-syntax"], (Some("raw"), "syntaxes") => &["sublime-syntax"],
(Some("raw"), "theme") => &["tmtheme"], (Some("raw"), "theme") => &["tmtheme"],
(Some("embed"), "path") => &[], (Some("embed"), "path") => &[],
(Some("attach"), "path") if *func == typst::pdf::AttachElem::ELEM => &[],
(None, "path") => &[], (None, "path") => &[],
_ => return None, _ => return None,
}) })
@ -1820,6 +1821,8 @@ mod tests {
.with_source("content/a.typ", "#image()") .with_source("content/a.typ", "#image()")
.with_source("content/b.typ", "#csv(\"\")") .with_source("content/b.typ", "#csv(\"\")")
.with_source("content/c.typ", "#include \"\"") .with_source("content/c.typ", "#include \"\"")
.with_source("content/d.typ", "#pdf.attach(\"\")")
.with_source("content/e.typ", "#math.attach(\"\")")
.with_asset_at("assets/tiger.jpg", "tiger.jpg") .with_asset_at("assets/tiger.jpg", "tiger.jpg")
.with_asset_at("assets/rhino.png", "rhino.png") .with_asset_at("assets/rhino.png", "rhino.png")
.with_asset_at("data/example.csv", "example.csv"); .with_asset_at("data/example.csv", "example.csv");
@ -1828,15 +1831,20 @@ mod tests {
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")]) .must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
.must_exclude([q!("assets/tiger.jpg")]); .must_exclude([q!("assets/tiger.jpg")]);
test(&world, ("content/c.typ", -2))
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
.must_exclude([q!("c.typ")]);
test(&world, ("content/a.typ", -2)) test(&world, ("content/a.typ", -2))
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")]) .must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
.must_exclude([q!("../data/example.csv"), q!("b.typ")]); .must_exclude([q!("../data/example.csv"), q!("b.typ")]);
test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]); test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
test(&world, ("content/c.typ", -2))
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
.must_exclude([q!("c.typ")]);
test(&world, ("content/d.typ", -2))
.must_include([q!("../assets/tiger.jpg"), q!("../data/example.csv")]);
test(&world, ("content/e.typ", -2)).must_exclude([q!("data/example.csv")]);
} }
#[test] #[test]

View File

@ -23,7 +23,7 @@ use typst_library::model::{
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem, LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem, Works, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem, Works,
}; };
use typst_library::pdf::EmbedElem; use typst_library::pdf::AttachElem;
use typst_library::text::{ use typst_library::text::{
DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName, DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName,
OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem, OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem,
@ -103,7 +103,7 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register(Paged, EQUATION_RULE); rules.register(Paged, EQUATION_RULE);
// PDF. // PDF.
rules.register(Paged, EMBED_RULE); rules.register(Paged, ATTACH_RULE);
} }
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, styles| { const STRONG_RULE: ShowFn<StrongElem> = |elem, _, styles| {
@ -846,4 +846,4 @@ const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
} }
}; };
const EMBED_RULE: ShowFn<EmbedElem> = |_, _, _| Ok(Content::empty()); const ATTACH_RULE: ShowFn<AttachElem> = |_, _, _| Ok(Content::empty());

View File

@ -446,6 +446,15 @@ impl PartialEq<&'static NativeFuncData> for Func {
} }
} }
impl PartialEq<Element> for Func {
fn eq(&self, other: &Element) -> bool {
match &self.repr {
Repr::Element(elem) => elem == other,
_ => false,
}
}
}
impl From<Repr> for Func { impl From<Repr> for Func {
fn from(repr: Repr) -> Self { fn from(repr: Repr) -> Self {
Self { repr, span: Span::detached() } Self { repr, span: Span::detached() }

View File

@ -6,18 +6,18 @@ use crate::diag::At;
use crate::foundations::{Bytes, Cast, Derived, elem}; use crate::foundations::{Bytes, Cast, Derived, elem};
use crate::introspection::Locatable; use crate::introspection::Locatable;
/// A file that will be embedded into the output PDF. /// A file that will be attached to the output PDF.
/// ///
/// This can be used to distribute additional files that are related to the PDF /// This can be used to distribute additional files associated with the PDF
/// within it. PDF readers will display the files in a file listing. /// within it. PDF readers will display the files in a file listing.
/// ///
/// Some international standards use this mechanism to embed machine-readable /// Some international standards use this mechanism to attach machine-readable
/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content /// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content
/// of the PDF. /// of the PDF.
/// ///
/// # Example /// # Example
/// ```typ /// ```typ
/// #pdf.embed( /// #pdf.attach(
/// "experiment.csv", /// "experiment.csv",
/// relationship: "supplement", /// relationship: "supplement",
/// mime-type: "text/csv", /// mime-type: "text/csv",
@ -27,11 +27,11 @@ use crate::introspection::Locatable;
/// ///
/// # Notes /// # Notes
/// - This element is ignored if exporting to a format other than PDF. /// - This element is ignored if exporting to a format other than PDF.
/// - File embeddings are not currently supported for PDF/A-2, even if the /// - File attachments are not currently supported for PDF/A-2, even if the
/// embedded file conforms to PDF/A-1 or PDF/A-2. /// attached file conforms to PDF/A-1 or PDF/A-2.
#[elem(Locatable)] #[elem(keywords = ["embed"], Locatable)]
pub struct EmbedElem { pub struct AttachElem {
/// The [path]($syntax/#paths) of the file to be embedded. /// The [path]($syntax/#paths) of the file to be attached.
/// ///
/// Must always be specified, but is only read from if no data is provided /// Must always be specified, but is only read from if no data is provided
/// in the following argument. /// in the following argument.
@ -61,21 +61,21 @@ pub struct EmbedElem {
)] )]
pub data: Bytes, pub data: Bytes,
/// The relationship of the embedded file to the document. /// The relationship of the attached file to the document.
/// ///
/// Ignored if export doesn't target PDF/A-3. /// Ignored if export doesn't target PDF/A-3.
pub relationship: Option<EmbeddedFileRelationship>, pub relationship: Option<AttachedFileRelationship>,
/// The MIME type of the embedded file. /// The MIME type of the attached file.
pub mime_type: Option<EcoString>, pub mime_type: Option<EcoString>,
/// A description for the embedded file. /// A description for the attached file.
pub description: Option<EcoString>, pub description: Option<EcoString>,
} }
/// The relationship of an embedded file with the document. /// The relationship of an attached file with the document.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum EmbeddedFileRelationship { pub enum AttachedFileRelationship {
/// The PDF document was created from the source file. /// The PDF document was created from the source file.
Source, Source,
/// The file was used to derive a visual presentation in the PDF. /// The file was used to derive a visual presentation in the PDF.

View File

@ -1,15 +1,21 @@
//! PDF-specific functionality. //! PDF-specific functionality.
mod embed; mod attach;
pub use self::embed::*; pub use self::attach::*;
use crate::foundations::{Module, Scope}; use crate::foundations::{Deprecation, Element, Module, Scope};
/// Hook up all `pdf` definitions. /// Hook up all `pdf` definitions.
pub fn module() -> Module { pub fn module() -> Module {
let mut pdf = Scope::deduplicating(); let mut pdf = Scope::deduplicating();
pdf.start_category(crate::Category::Pdf); pdf.start_category(crate::Category::Pdf);
pdf.define_elem::<EmbedElem>(); pdf.define_elem::<AttachElem>();
pdf.define("embed", Element::of::<AttachElem>()).deprecated(
// Remember to remove "embed" from `path_completion` when removing this.
Deprecation::new()
.with_message("the name `embed` is deprecated, use `attach` instead")
.with_until("0.15.0"),
);
Module::new("pdf", pdf) Module::new("pdf", pdf)
} }

View File

@ -5,40 +5,40 @@ use krilla::embed::{AssociationKind, EmbeddedFile};
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::{NativeElement, StyleChain}; use typst_library::foundations::{NativeElement, StyleChain};
use typst_library::layout::PagedDocument; use typst_library::layout::PagedDocument;
use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; use typst_library::pdf::{AttachElem, AttachedFileRelationship};
pub(crate) fn embed_files( pub(crate) fn attach_files(
typst_doc: &PagedDocument, typst_doc: &PagedDocument,
document: &mut Document, document: &mut Document,
) -> SourceResult<()> { ) -> SourceResult<()> {
let elements = typst_doc.introspector.query(&EmbedElem::ELEM.select()); let elements = typst_doc.introspector.query(&AttachElem::ELEM.select());
for elem in &elements { for elem in &elements {
let embed = elem.to_packed::<EmbedElem>().unwrap(); let elem = elem.to_packed::<AttachElem>().unwrap();
let span = embed.span(); let span = elem.span();
let derived_path = &embed.path.derived; let derived_path = &elem.path.derived;
let path = derived_path.to_string(); let path = derived_path.to_string();
let mime_type = embed let mime_type = elem
.mime_type .mime_type
.get_ref(StyleChain::default()) .get_ref(StyleChain::default())
.as_ref() .as_ref()
.map(|s| s.to_string()); .map(|s| s.to_string());
let description = embed let description = elem
.description .description
.get_ref(StyleChain::default()) .get_ref(StyleChain::default())
.as_ref() .as_ref()
.map(|s| s.to_string()); .map(|s| s.to_string());
let association_kind = match embed.relationship.get(StyleChain::default()) { let association_kind = match elem.relationship.get(StyleChain::default()) {
None => AssociationKind::Unspecified, None => AssociationKind::Unspecified,
Some(e) => match e { Some(e) => match e {
EmbeddedFileRelationship::Source => AssociationKind::Source, AttachedFileRelationship::Source => AssociationKind::Source,
EmbeddedFileRelationship::Data => AssociationKind::Data, AttachedFileRelationship::Data => AssociationKind::Data,
EmbeddedFileRelationship::Alternative => AssociationKind::Alternative, AttachedFileRelationship::Alternative => AssociationKind::Alternative,
EmbeddedFileRelationship::Supplement => AssociationKind::Supplement, AttachedFileRelationship::Supplement => AssociationKind::Supplement,
}, },
}; };
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone()); let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(elem.data.clone());
let compress = should_compress(&embed.data); let compress = should_compress(&elem.data);
let file = EmbeddedFile { let file = EmbeddedFile {
path, path,
@ -51,7 +51,7 @@ pub(crate) fn embed_files(
}; };
if document.embed_file(file).is_none() { if document.embed_file(file).is_none() {
bail!(span, "attempted to embed file {derived_path} twice"); bail!(span, "attempted to attach file {derived_path} twice");
} }
} }

View File

@ -25,7 +25,7 @@ use typst_library::visualize::{Geometry, Paint};
use typst_syntax::Span; use typst_syntax::Span;
use crate::PdfOptions; use crate::PdfOptions;
use crate::embed::embed_files; use crate::attach::attach_files;
use crate::image::handle_image; use crate::image::handle_image;
use crate::link::handle_link; use crate::link::handle_link;
use crate::metadata::build_metadata; use crate::metadata::build_metadata;
@ -63,7 +63,7 @@ pub fn convert(
); );
convert_pages(&mut gc, &mut document)?; convert_pages(&mut gc, &mut document)?;
embed_files(typst_document, &mut document)?; attach_files(typst_document, &mut document)?;
document.set_outline(build_outline(&gc)); document.set_outline(build_outline(&gc));
document.set_metadata(build_metadata(&gc)); document.set_metadata(build_metadata(&gc));
@ -546,19 +546,19 @@ fn convert_error(
} }
} }
ValidationError::EmbeddedFile(e, s) => { ValidationError::EmbeddedFile(e, s) => {
// We always set the span for embedded files, so it cannot be detached. // We always set the span for attached files, so it cannot be detached.
let span = to_span(*s); let span = to_span(*s);
match e { match e {
EmbedError::Existence => { EmbedError::Existence => {
error!( error!(
span, "{prefix} document contains an embedded file"; span, "{prefix} document contains an attached file";
hint: "embedded files are not supported in this export mode" hint: "file attachments are not supported in this export mode"
) )
} }
EmbedError::MissingDate => { EmbedError::MissingDate => {
error!( error!(
span, "{prefix} document date is missing"; span, "{prefix} document date is missing";
hint: "the document must have a date when embedding files"; hint: "the document must have a date when attaching files";
hint: "`set document(date: none)` must not be used in this case" hint: "`set document(date: none)` must not be used in this case"
) )
} }

View File

@ -1,7 +1,7 @@
//! Exporting Typst documents to PDF. //! Exporting Typst documents to PDF.
mod attach;
mod convert; mod convert;
mod embed;
mod image; mod image;
mod link; mod link;
mod metadata; mod metadata;

View File

@ -23,8 +23,8 @@ description: Changes in Typst 0.13.0
would be displayed in italics would be displayed in italics
- You can now specify which charset should be [covered]($text.font) by which - You can now specify which charset should be [covered]($text.font) by which
font family font family
- The [`pdf.embed`] function lets you embed arbitrary files in the exported - The [`pdf.embed`]($pdf.attach) function lets you embed arbitrary files in the
PDF exported PDF
- HTML export is currently under active development. The feature is still _very_ - HTML export is currently under active development. The feature is still _very_
incomplete, but already available for experimentation behind a feature flag. incomplete, but already available for experimentation behind a feature flag.
@ -231,7 +231,8 @@ description: Changes in Typst 0.13.0
- A shebang `#!` at the very start of a file is now ignored - A shebang `#!` at the very start of a file is now ignored
## PDF export ## PDF export
- Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF - Added [`pdf.embed`]($pdf.attach) function for embedding arbitrary files in the
exported PDF
- Added support for PDF/A-3b export - Added support for PDF/A-3b export
- The PDF timestamp will now contain the timezone by default - The PDF timestamp will now contain the timezone by default

View File

@ -28,9 +28,9 @@ Currently, Typst supports these PDF/A output profiles:
- PDF/A-3b: The basic conformance level of ISO 19005-3. This version of PDF/A is - PDF/A-3b: The basic conformance level of ISO 19005-3. This version of PDF/A is
based on PDF 1.7 and results in archivable PDF files that can contain based on PDF 1.7 and results in archivable PDF files that can contain
arbitrary other related files as [attachments]($pdf.embed). The only arbitrary other related files as [attachments]($pdf.attach). The only
difference between it and PDF/A-2b is the capability to embed difference between it and PDF/A-2b is the capability to attach
non-PDF/A-conformant files within. non-PDF/A-conformant files.
When choosing between exporting PDF/A and regular PDF, keep in mind that PDF/A When choosing between exporting PDF/A and regular PDF, keep in mind that PDF/A
files contain additional metadata, and that some readers will prevent the user files contain additional metadata, and that some readers will prevent the user

View File

@ -1,18 +1,18 @@
// Test file embeddings. The tests here so far are unsatisfactory because we // Test file attachments. The tests here so far are unsatisfactory because we
// have no PDF testing infrastructure. That should be improved in the future. // have no PDF testing infrastructure. That should be improved in the future.
--- pdf-embed --- --- pdf-attach ---
#pdf.embed("/assets/text/hello.txt") #pdf.attach("/assets/text/hello.txt")
#pdf.embed( #pdf.attach(
"/assets/data/details.toml", "/assets/data/details.toml",
relationship: "supplement", relationship: "supplement",
mime-type: "application/toml", mime-type: "application/toml",
description: "Information about a secret project", description: "Information about a secret project",
) )
--- pdf-embed-bytes --- --- pdf-attach-bytes ---
#pdf.embed("hello.txt", read("/assets/text/hello.txt", encoding: none)) #pdf.attach("hello.txt", read("/assets/text/hello.txt", encoding: none))
#pdf.embed( #pdf.attach(
"a_file_name.txt", "a_file_name.txt",
read("/assets/text/hello.txt", encoding: none), read("/assets/text/hello.txt", encoding: none),
relationship: "supplement", relationship: "supplement",
@ -20,8 +20,8 @@
description: "A description", description: "A description",
) )
--- pdf-embed-invalid-relationship --- --- pdf-attach-invalid-relationship ---
#pdf.embed( #pdf.attach(
"/assets/text/hello.txt", "/assets/text/hello.txt",
// Error: 17-23 expected "source", "data", "alternative", "supplement", or none // Error: 17-23 expected "source", "data", "alternative", "supplement", or none
relationship: "test", relationship: "test",
@ -29,6 +29,11 @@
description: "A test file", description: "A test file",
) )
--- pdf-embed-invalid-data --- --- pdf-attach-invalid-data ---
// Error: 38-45 expected bytes, found string // Error: 39-46 expected bytes, found string
#pdf.embed("/assets/text/hello.txt", "hello") #pdf.attach("/assets/text/hello.txt", "hello")
--- pdf-embed-deprecated ---
// Warning: 6-11 the name `embed` is deprecated, use `attach` instead
// Hint: 6-11 it will be removed in Typst 0.15.0
#pdf.embed("/assets/text/hello.txt")