use std::collections::BTreeMap; use ecow::EcoString; use pdf_writer::types::AssociationKind; use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::{NativeElement, Packed, StyleChain}; use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; use crate::catalog::{document_date, pdf_date}; use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; /// Query for all [`EmbedElem`] and write them and their file specifications. /// /// This returns a map of embedding names and references so that we can later /// add them to the catalog's `/Names` dictionary. pub fn write_embedded_files( ctx: &WithGlobalRefs, ) -> SourceResult<(PdfChunk, BTreeMap)> { let mut chunk = PdfChunk::new(); let mut embedded_files = BTreeMap::default(); let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); for elem in &elements { if !ctx.options.standards.embedded_files { // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, // which we don't currently check. bail!( elem.span(), "file embeddings are not currently supported for PDF/A-2"; hint: "PDF/A-3 supports arbitrary embedded files" ); } let embed = elem.to_packed::().unwrap(); if embed.path.derived.len() > Str::PDFA_LIMIT { bail!(embed.span(), "embedded file path is too long"); } let id = embed_file(ctx, &mut chunk, embed)?; if embedded_files.insert(embed.path.derived.clone(), id).is_some() { bail!( elem.span(), "duplicate embedded file for path `{}`", embed.path.derived; hint: "embedded file paths must be unique", ); } } Ok((chunk, embedded_files)) } /// Write the embedded file stream and its file specification. fn embed_file( ctx: &WithGlobalRefs, chunk: &mut PdfChunk, embed: &Packed, ) -> SourceResult { let embedded_file_stream_ref = chunk.alloc.bump(); let file_spec_dict_ref = chunk.alloc.bump(); let data = embed.data().as_slice(); let compressed = deflate(data); let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); embedded_file.filter(Filter::FlateDecode); if let Some(mime_type) = embed.mime_type(StyleChain::default()) { if mime_type.len() > Name::PDFA_LIMIT { bail!(embed.span(), "embedded file MIME type is too long"); } embedded_file.subtype(Name(mime_type.as_bytes())); } else if ctx.options.standards.pdfa { bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); } let mut params = embedded_file.params(); params.size(data.len() as i32); let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { params.modification_date(pdf_date); } else if ctx.options.standards.pdfa { bail!( embed.span(), "the document must have a date when embedding files in PDF/A-3"; hint: "`set document(date: none)` must not be used in this case" ); } params.finish(); embedded_file.finish(); let mut file_spec = chunk.file_spec(file_spec_dict_ref); file_spec.path(Str(embed.path.derived.as_bytes())); file_spec.unic_file(TextStr(&embed.path.derived)); file_spec .insert(Name(b"EF")) .dict() .pair(Name(b"F"), embedded_file_stream_ref) .pair(Name(b"UF"), embedded_file_stream_ref); if ctx.options.standards.pdfa { // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. file_spec.association_kind(match embed.relationship(StyleChain::default()) { Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, None => AssociationKind::Unspecified, }); } if let Some(description) = embed.description(StyleChain::default()) { if description.len() > Str::PDFA_LIMIT { bail!(embed.span(), "embedded file description is too long"); } file_spec.description(TextStr(description)); } Ok(file_spec_dict_ref) }