summaryrefslogtreecommitdiff
path: root/crates/typst-pdf/src/embed.rs
blob: 3ba2ac0766b1cb7d616eddd5a72fc1c5d5941f7e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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.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<EmbedElem>,
) -> SourceResult<Ref> {
    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)
}