Repository: typst/typst
File: crates/typst-pdf/src/embed.rs (Rust)

Code before:
use crate::catalog::pdf_date;
use crate::{PdfAConformanceLevel, PdfChunk, WithGlobalRefs};
use ecow::EcoString;
use pdf_writer::{Finish, Name, Ref, Str, TextStr};
use std::collections::HashMap;
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::NativeElement;
use typst_library::pdf::embed::{Embed, EmbedElem, EmbeddedFileRelationship};
use typst_syntax::Span;

pub fn write_embedded_files(
    ctx: &WithGlobalRefs,
) -> SourceResult<(PdfChunk, HashMap<EcoString, Ref>)> {
    let mut chunk = PdfChunk::new();

    let elements = ctx.document.introspector.query(&EmbedElem::elem().select());
    if !elements.is_empty()
        && Some(PdfAConformanceLevel::A_2) == ctx.options.standards.pdfa
    {
        bail!(
            Span::detached(),
            "file embeddings are currently only supported for PDF/A-3"
        );
    }

    let mut embedded_files = HashMap::default();
    for elem in elements.iter() {
        let packed_elem = elem.to_packed::<EmbedElem>().unwrap();
        let embed = Embed::from_element(packed_elem);
        embedded_files.insert(embed.name().clone(), embed_file(ctx, &mut chunk, &embed));
    }

    Ok((chunk, embedded_files))
}

fn embed_file(ctx: &WithGlobalRefs, chunk: &mut PdfChunk, embed: &Embed) -> Ref {
    let embedded_file_stream_ref = chunk.alloc.bump();
    let file_spec_dict_ref = chunk.alloc.bump();

    let length = embed.data().len();

    let mut embedded_file =
        chunk.embedded_file(embedded_file_stream_ref, embed.data().as_ref());
    embedded_file.pair(Name(b"Length"), length as i32);
    if let Some(mime_type) = embed.mime_type() {
        embedded_file.subtype(Name(mime_type.as_bytes()));

Review comment (suggestion):
the span here should be that of the problematic `EmbedElem`

Code after:
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<EcoString, Ref>)> {
    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::<EmbedElem>().unwrap();
        if embed.resolved_path.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.resolved_path.clone(), id).is_some() {
            bail!(
                elem.span(),
                "duplicate embedded file for path `{}`", embed.resolved_path;
                hint: "embedded file paths must be unique",
            );